refactor: rename DTree to DValue
This commit is contained in:
parent
941f5aea08
commit
7066da3cde
10
README.md
10
README.md
@ -76,9 +76,9 @@ Useful related runtime patterns:
|
||||
|
||||
Useful helpers for that data model include:
|
||||
|
||||
- `DTree::get_by_path("a/b/c")` for path-style config traversal without creating missing keys
|
||||
- `DTree::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended
|
||||
- `DTree::to_u64()`, `to_s64()`, `to_f64()`, `to_bool()`, and `to_stringmap()` for typed reads from structured values
|
||||
- `DValue::get_by_path("a/b/c")` for path-style config traversal without creating missing keys
|
||||
- `DValue::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended
|
||||
- `DValue::to_u64()`, `to_s64()`, `to_f64()`, `to_bool()`, and `to_stringmap()` for typed reads from structured values
|
||||
- `json_encode(String)` for emitting JavaScript-safe string literals directly
|
||||
- `ascii_safe_name(String)` for conservative ASCII identifier normalization
|
||||
- `path_join(base, child)` for filesystem-style path assembly
|
||||
@ -86,7 +86,7 @@ Useful helpers for that data model include:
|
||||
- `zip_create()`, `zip_list()`, `zip_read()`, and `zip_extract()` for minimal ZIP archive workflows
|
||||
- `gz_compress()` and `gz_uncompress()` for gzip-format byte strings
|
||||
- `server_start_http()` / `server_stop()` for runtime-managed custom HTTP listeners backed by `SERVE_HTTP` handlers
|
||||
- `map()`, `filter()`, `list_unique()`, `dtree_filter()`, `dtree_map()`, `dtree_pick()`, and related helpers for route/menu/card data shaping near render code
|
||||
- `map()`, `filter()`, `list_unique()`, `dv_filter()`, `dv_map()`, `dv_pick()`, and related helpers for route/menu/card data shaping near render code
|
||||
|
||||
Named component handlers are also supported:
|
||||
|
||||
@ -186,7 +186,7 @@ The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate
|
||||
|
||||
By default, the WebSocket scope is the current page file, so `ws_send()` queues a message for clients connected to that same `.ws.uce` endpoint.
|
||||
|
||||
Each live WebSocket connection owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client.
|
||||
Each live WebSocket connection owns a broker-side `DValue` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client.
|
||||
|
||||
The current inbound payload is available directly as `context.in`, and the runtime mirrors message metadata into `context.params` using keys such as `WS_CONNECTION_ID`, `WS_SCOPE`, `WS_CONNECTION_COUNT`, `WS_OPCODE`, `WS_MESSAGE_TYPE`, and `WS_DOCUMENT_URI`.
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ candidates. Status legend:
|
||||
The island component emits JSON props inside a single-quoted attribute
|
||||
(`data-props='<?: html_escape(payload) ?>'`), but `html_escape()`
|
||||
(`src/lib/functionlib.cpp:936`) escapes only `& < > "`, and `json_encode` /
|
||||
`json_escape` (`src/lib/dtree.cpp:794`) never escape apostrophes. Any `'` in a
|
||||
`json_escape` (`src/lib/dvalue.cpp:794`) never escape apostrophes. Any `'` in a
|
||||
prop value terminates the attribute.
|
||||
|
||||
**Failure:** a view passes user-influenced prop text containing an apostrophe,
|
||||
@ -193,21 +193,21 @@ correctly) and use it in both places.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 1.10 O(n²) and per-row deep copies in dtree_map / dtree_filter
|
||||
### 1.10 O(n²) and per-row deep copies in dv_map / dv_filter
|
||||
|
||||
**File:** `src/lib/functionlib.cpp:182`
|
||||
|
||||
Both helpers call `tree.is_list()` inside the per-element callback —
|
||||
`is_list()` (`src/lib/dtree.cpp:165`) walks the entire map, making the
|
||||
`is_list()` (`src/lib/dvalue.cpp:165`) walks the entire map, making the
|
||||
operation O(n²) — even though both already compute `is_list()` once before
|
||||
the loop. Additionally, `DTree::each` (`dtree.h:22`) takes the callback
|
||||
element by value (`DTree t`), deep-copying every subtree per iteration.
|
||||
the loop. Additionally, `DValue::each` (`dvalue.h:22`) takes the callback
|
||||
element by value (`DValue t`), deep-copying every subtree per iteration.
|
||||
|
||||
**Failure:** mapping over a 1000-row `sqlite_query` result does ~1M
|
||||
key-validation checks plus a full deep copy of each row tree.
|
||||
|
||||
**Fix:** hoist `is_list()` into a `bool` local reused in the callback; change
|
||||
`each()` to pass `const DTree&`.
|
||||
`each()` to pass `const DValue&`.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
@ -321,7 +321,7 @@ lies.
|
||||
|
||||
The router rewrite adds candidate `kind`/`matched` fields and
|
||||
`context.call["route"]["candidates"]/["resolved"]/["missed"]`, none of which
|
||||
is read by any component, theme, or test. It also uses `dtree_filter` to
|
||||
is read by any component, theme, or test. It also uses `dv_filter` to
|
||||
`file_exists`-stat every candidate after the first match is already found.
|
||||
~55 lines plus a builder helper replace what the deleted `app_resolve_view`
|
||||
did with three early-return `file_exists` checks.
|
||||
@ -512,11 +512,11 @@ verified live against the dev server (network suite: 21/21 public tests pass).
|
||||
> handle and signals failure via `_preload_next_error_code`, and `query()` /
|
||||
> `error()` guard against a null connection (clean error message instead of
|
||||
> the SIGSEGV this otherwise caused).
|
||||
> - The 7.8 nit about the `assets.uce` lambda taking `DTree` by value is
|
||||
> withdrawn: `DTree::to_string()` is non-const, so a `const DTree&` parameter
|
||||
> cannot call it. Making the DTree read accessors const-correct would be the
|
||||
> - The 7.8 nit about the `assets.uce` lambda taking `DValue` by value is
|
||||
> withdrawn: `DValue::to_string()` is non-const, so a `const DValue&` parameter
|
||||
> cannot call it. Making the DValue read accessors const-correct would be the
|
||||
> real fix and is a separate, larger change (worth doing before the WASM
|
||||
> DTree C ABI freezes the surface).
|
||||
> DValue C ABI freezes the surface).
|
||||
|
||||
### Verified clean
|
||||
|
||||
@ -524,7 +524,7 @@ verified live against the dev server (network suite: 21/21 public tests pass).
|
||||
**1.4** (multi-statement rejection is comment/whitespace-tolerant, test),
|
||||
**1.9/5.5** (`compiler_generated_cpp_path` agrees with `setup_unit_paths`;
|
||||
confirmed live — the error page now prints the real `/tmp/uce/work/...` path),
|
||||
**1.10** (`each`/`push` take `const DTree&`, `is_list()` hoisted),
|
||||
**1.10** (`each`/`push` take `const DValue&`, `is_list()` hoisted),
|
||||
**1.11** (both the initial scan and the loop body are wrapped),
|
||||
**2.1, 2.2, 2.3, 2.5/3.4, 3.1, 3.2, 3.5, 4.1, 4.2, 4.4, 4.5** (SQLITE_STATIC
|
||||
is safe: the params map outlives the statement), **5.3, 5.4**.
|
||||
@ -655,8 +655,8 @@ startup so libgcc's lazy init doesn't allocate on first use either.
|
||||
- `MySQL::error()` consumes/clears state in the new `statement_info` branch but
|
||||
not in the errno-switch branch, and `SQLite::error()` never consumes — pick
|
||||
one semantic.
|
||||
- `theme/assets.uce` lambda still takes `DTree item` by value; make it
|
||||
`const DTree&` to match the new `each()` signature.
|
||||
- `theme/assets.uce` lambda still takes `DValue item` by value; make it
|
||||
`const DValue&` to match the new `each()` signature.
|
||||
- `__UceFragmentCapture` is regenerated inline in every captured entry point;
|
||||
define the struct once in the runtime headers and have the prelude emit only
|
||||
the instantiation line (less generated-code surface — also smaller wasm unit
|
||||
@ -675,11 +675,11 @@ startup so libgcc's lazy init doesn't allocate on first use either.
|
||||
|
||||
### 7.9 NEW: lists with ten or more entries iterate in lexicographic, not numeric, order
|
||||
|
||||
**File:** `src/lib/dtree.cpp` (`DTree::each`), found while documenting the accessors (2026-06-11)
|
||||
**File:** `src/lib/dvalue.cpp` (`DValue::each`), found while documenting the accessors (2026-06-11)
|
||||
|
||||
`DTree` stores list entries as string keys in a `std::map`, so iteration order
|
||||
`DValue` stores list entries as string keys in a `std::map`, so iteration order
|
||||
is lexicographic: `"0", "1", "10", "11", "2", ...`. Every consumer of `each()`
|
||||
inherits this — `dtree_map`/`dtree_filter`/`dtree_values` re-push in that
|
||||
inherits this — `dv_map`/`dv_filter`/`dv_values` re-push in that
|
||||
order and therefore *scramble* any `push()`-built list with ≥ 10 entries, and
|
||||
a `sqlite_query()` result with ≥ 10 rows renders rows 10+ before row 2 when
|
||||
iterated. `is_list()` still reports true because the key *set* is sequential.
|
||||
@ -687,7 +687,7 @@ iterated. `is_list()` still reports true because the key *set* is sequential.
|
||||
**Fix options:** iterate numerically in `each()` when `is_list()` is true
|
||||
(cheapest, fixes all consumers at once); or use a numeric-aware comparator in
|
||||
the map type. Documented honestly on the `each` doc page in the meantime.
|
||||
This also needs deciding before the WASM DTree C ABI freezes iteration-order
|
||||
This also needs deciding before the WASM DValue C ABI freezes iteration-order
|
||||
semantics.
|
||||
|
||||
**Status:** fixed — and the regression test exposed the deeper layer:
|
||||
@ -698,7 +698,7 @@ serialize big lists as objects instead of arrays. `is_list()` now does an
|
||||
order-independent check (n unique canonical index keys with max n-1), index
|
||||
keys require canonical form (`"1"`, not `"01"`), and `each()` iterates lists
|
||||
in numeric index order, matching the encoders. Regression test with a
|
||||
12-entry list in core.uce covers `each`, `dtree_values`, and `dtree_map`.
|
||||
12-entry list in core.uce covers `each`, `dv_values`, and `dv_map`.
|
||||
|
||||
### 7.10 NEW: fault recovery left the worker in the crashed unit's working directory
|
||||
|
||||
|
||||
143
WASM-PROPOSAL.md
143
WASM-PROPOSAL.md
@ -1,12 +1,11 @@
|
||||
# WASM-PROPOSAL: WebAssembly Unit Runtime for UCE
|
||||
|
||||
- **Status:** proposal / design draft (2026-06-11)
|
||||
- **Status:** updated design guide (2026-06-12)
|
||||
- **Scope:** replace the native unit pipeline (generated C++ → clang → `.so` →
|
||||
`dlopen`) with per-unit WebAssembly modules executed in a per-request,
|
||||
runtime-linked workspace, exposing the same API surface to page code.
|
||||
- **Origin:** design discussion 2026-06-11; incorporates the post-mortem of the
|
||||
earlier per-invocation arena attempt (preserved in
|
||||
`src/lib/_scratchpad.cpp`).
|
||||
runtime-linked workspace, exposing the same API surface to page code,
|
||||
enabling memory safety, better execution control, and paving the way for
|
||||
supporting more source languages in the future.
|
||||
|
||||
---
|
||||
|
||||
@ -26,7 +25,7 @@ with the runtime.**
|
||||
system-allocated objects released mid-request leak. The lifetime
|
||||
distinction lives in the type system and call graph, not at the allocator
|
||||
boundary — fixing it in-process means threading PMR allocators through
|
||||
`String`, `DTree`, and every container.
|
||||
`String`, `DValue`, and every container.
|
||||
|
||||
2. **Fault recovery is best-effort, not sound.** The current
|
||||
SIGSEGV → `sigsetjmp`/`siglongjmp` recovery performs no unwinding, skips
|
||||
@ -57,15 +56,17 @@ patching around it:
|
||||
first-class limits (linear-memory cap = RAM limit, epoch/fuel = CPU
|
||||
timeout), per-request memory stats for free.
|
||||
|
||||
**Accepted costs:** ~1.2–2× compute slowdown vs. native (still far ahead of
|
||||
**Expected outcome:** ~1.2–2× compute slowdown vs. native (still far ahead of
|
||||
interpreted runtimes); a real ABI/membrane design; ownership of a custom
|
||||
loader; toolchain rough edges (§10).
|
||||
loader; toolchain rough edges (§10). More isolated, shared-nothing PHP architecture
|
||||
but with a good story about websockets, generic sockets, background tasks,
|
||||
and other long running processes.
|
||||
|
||||
---
|
||||
|
||||
## 2. Rejected alternatives (recorded so they stay rejected)
|
||||
|
||||
- **In-process PMR arena.** Requires re-typedefing `String`/`DTree` and
|
||||
- **In-process PMR arena.** Requires re-typedefing `String`/`DValue` and
|
||||
threading allocators through the entire codebase; the failed global-swap
|
||||
shortcut is the only cheap version and it is unsound (§1.1).
|
||||
- **One instance per component.** UCE components are function calls, not
|
||||
@ -127,11 +128,11 @@ host process (linux_fastcgi, per worker)
|
||||
|
||||
### 3.2 Memory model
|
||||
|
||||
- **One heap, one allocator, one DTree implementation** — all owned by the
|
||||
- **One heap, one allocator, one DValue implementation** — all owned by the
|
||||
core module. Unit modules *import* `malloc`/`free`/runtime symbols via GOT;
|
||||
the loader **rejects any unit module that defines rather than imports
|
||||
them** (two allocators on one heap is the one fatal misconfiguration).
|
||||
- **Bump allocator option.** Because the workspace heap is dropped wholesale,
|
||||
- **Arena workspace allocator.** Because the workspace heap is dropped wholesale,
|
||||
the core's allocator may be a bump allocator with no-op free — the
|
||||
`_scratchpad.cpp` design, now correct by construction. Per-deployment
|
||||
flag; fallback is wasi-libc dlmalloc. Memory stats = heap pointer − base
|
||||
@ -150,7 +151,8 @@ Exactly three currencies cross between host and workspace:
|
||||
|
||||
1. **scalars** (i32/i64/f64),
|
||||
2. **byte buffers** (`ptr+len` into linear memory; inbound buffers are
|
||||
placed via the core's exported allocator),
|
||||
placed via the core's exported allocator), on the C++ side we strictly
|
||||
prefer the binary-safe std::string as a container for buffers
|
||||
3. **handles** (opaque `u32` indices into the per-workspace host handle
|
||||
table; each entry carries a closer callback).
|
||||
|
||||
@ -158,22 +160,27 @@ Everything pointer-shaped stays on its own side. Hostcall surface budget:
|
||||
30–60 functions (§5.1). Host errors return as error values; traps are
|
||||
reserved for unit faults. Nothing throws across the membrane.
|
||||
|
||||
**DTrees cross the membrane only as the versioned wire encoding** (§5.3),
|
||||
at exactly three sites: request context in (once), response out (once),
|
||||
bulk I/O results in (per query, optional vs. cursor-style hostcalls).
|
||||
**DValues cross the membrane only as the versioned wire encoding** (§5.3),
|
||||
at the following sites: request context in (once), response out (once),
|
||||
some hostcalls.
|
||||
|
||||
### 3.4 DTree inside the workspace: no serialization, ever
|
||||
It is expected that many if not most of the toolset API functions we
|
||||
expose to the unit developer will be judiciously split across the membrane:
|
||||
have a relatively minimal wasm part that calls into the runtime host, and
|
||||
then the runtime implementation which does most of the work.
|
||||
|
||||
### 3.4 DValue inside the workspace: no serialization, ever
|
||||
|
||||
Within a workspace, all modules share one address space, one toolchain, one
|
||||
set of headers — the C/C++ ABI is intact across module boundaries. Therefore:
|
||||
|
||||
- A `DTree` is a pointer (an i32 offset into linear memory).
|
||||
- A `DValue` is a pointer (an i32 offset into linear memory).
|
||||
- `component(path, context)` resolves path → table index (host registry or
|
||||
guest-resident map) and `call_indirect`s, passing the context pointer.
|
||||
Reference semantics, mutation visibility, shared ob stack, working `ONCE`
|
||||
— identical to today.
|
||||
- Function pointers are shared-table indices, valid across modules: virtual
|
||||
calls, `std::function` callbacks (`dtree_map` lambdas) work across units.
|
||||
calls, `std::function` callbacks (`dv_map` lambdas) work across units.
|
||||
- Cost: one `call_indirect` (single-digit ns) + GOT loads for cross-module
|
||||
symbols — the same shape of overhead native PIC pays through the PLT/GOT,
|
||||
i.e. what dlopened `.so` units pay today.
|
||||
@ -181,10 +188,10 @@ set of headers — the C/C++ ABI is intact across module boundaries. Therefore:
|
||||
Encode/decode is **not** part of internal component calls. It exists only at
|
||||
the membrane (§3.3) and on the cross-instance plane (§4).
|
||||
|
||||
### 3.5 The DTree C ABI (load-bearing, build it first)
|
||||
### 3.5 The DValue C ABI (load-bearing, build it first)
|
||||
|
||||
The stable contract of the workspace is a **C ABI**, not the C++ class:
|
||||
the core exports `extern "C"` accessors over an opaque `uce_dtree*` (§5.2),
|
||||
the core exports `extern "C"` accessors over an opaque `uce_dvalue*` (§5.2),
|
||||
plus the string/ob/print helpers. C++ units may bypass it and use the class
|
||||
directly (same headers, zero cost — a private fast path). Every other
|
||||
workspace language uses the C surface.
|
||||
@ -210,16 +217,12 @@ PIC linear-memory module that adopts a foreign allocator?*
|
||||
- Must adopt the core allocator (Rust: `#[global_allocator]`; Zig: allocator
|
||||
parameter) and must not unwind across boundaries (`panic=abort` /
|
||||
catch-at-edge).
|
||||
- Access DTrees through the C ABI: pointer semantics, no copies, ns-scale
|
||||
calls. Idiomatic wrappers per language (e.g. Rust `DTree<'request>` —
|
||||
- Access DValues through the C ABI: pointer semantics, no copies, ns-scale
|
||||
calls. Idiomatic wrappers per language (e.g. Rust `DValue<'request>` —
|
||||
the borrow checker enforces the arena invariant).
|
||||
|
||||
**Plane B — runtime-carrying languages** (JS, Python, Go, C#, …):
|
||||
- Their GC/runtime owns its memory; they run as **separate instances**
|
||||
within the request and communicate through the host using the wire
|
||||
encoding and handles. Bindings choose per-access hostcalls or bulk
|
||||
subtree hydration into native dicts/objects — a tuning decision, not an
|
||||
architectural fork.
|
||||
- No support currently planned.
|
||||
|
||||
**The semantic rule (enforced by the loader, not by convention):**
|
||||
cross-plane components do **not** receive the mutable context. They get an
|
||||
@ -260,25 +263,25 @@ Conventions: all errors as result codes + `uce_host_last_error(buf)`;
|
||||
inbound buffers placed via the core's exported `uce_alloc`; no hostcall
|
||||
traps on bad input (clamp/error instead).
|
||||
|
||||
### 5.2 DTree C ABI (core exports; sketch)
|
||||
### 5.2 DValue C ABI (core exports; sketch)
|
||||
|
||||
```c
|
||||
typedef struct uce_dtree uce_dtree; // opaque; workspace-owned
|
||||
typedef struct uce_dvalue uce_dvalue; // opaque; workspace-owned
|
||||
|
||||
uce_dtree* uce_dtree_root(void); // request context
|
||||
uce_dtree* uce_dtree_get(uce_dtree*, const char* key, size_t klen); // create-on-write
|
||||
uce_dtree* uce_dtree_find(uce_dtree*, const char* key, size_t klen); // NULL if absent
|
||||
const char* uce_dtree_value(uce_dtree*, size_t* len);
|
||||
void uce_dtree_set_value(uce_dtree*, const char* v, size_t vlen);
|
||||
size_t uce_dtree_count(uce_dtree*);
|
||||
int uce_dtree_is_list(uce_dtree*);
|
||||
uce_dvalue* uce_dv_root(void); // request context
|
||||
uce_dvalue* uce_dv_get(uce_dvalue*, const char* key, size_t klen); // create-on-write
|
||||
uce_dvalue* uce_dv_find(uce_dvalue*, const char* key, size_t klen); // NULL if absent
|
||||
const char* uce_dv_value(uce_dvalue*, size_t* len);
|
||||
void uce_dv_set_value(uce_dvalue*, const char* v, size_t vlen);
|
||||
size_t uce_dv_count(uce_dvalue*);
|
||||
int uce_dv_is_list(uce_dvalue*);
|
||||
/* iteration */
|
||||
uce_dtree_iter uce_dtree_iter_begin(uce_dtree*);
|
||||
int uce_dtree_iter_next(uce_dtree*, uce_dtree_iter*,
|
||||
const char** key, size_t* klen, uce_dtree** child);
|
||||
uce_dv_iter uce_dv_iter_begin(uce_dvalue*);
|
||||
int uce_dv_iter_next(uce_dvalue*, uce_dv_iter*,
|
||||
const char** key, size_t* klen, uce_dvalue** child);
|
||||
/* encode/decode at the membrane */
|
||||
size_t uce_dtree_encode(uce_dtree*, char* buf, size_t cap); // → UCEB1
|
||||
uce_dtree* uce_dtree_decode(const char* buf, size_t len);
|
||||
size_t uce_dv_encode(uce_dvalue*, char* buf, size_t cap); // → UCEB1
|
||||
uce_dvalue* uce_dv_decode(const char* buf, size_t len);
|
||||
/* ob / print / helpers: uce_print, uce_ob_start, uce_ob_get_close,
|
||||
uce_html_escape, uce_json_encode, ... (mirror uce_lib surface) */
|
||||
```
|
||||
@ -288,7 +291,7 @@ surfaced as error returns where fallible.
|
||||
|
||||
### 5.3 Wire encoding "UCEB1" (membrane + cross-instance only)
|
||||
|
||||
Length-prefixed binary tree; **not** JSON. Sketch (finalize against DTree's
|
||||
Length-prefixed binary tree; **not** JSON. Sketch (finalize against DValue's
|
||||
actual fields — value + ordered children):
|
||||
|
||||
```
|
||||
@ -302,6 +305,12 @@ header := "UCEB" u8 version
|
||||
This encoding is a **versioned protocol** from day one (header byte). It is
|
||||
the Plane B contract and the membrane format; internal calls never see it.
|
||||
|
||||
UCEB1 encoding/decoding should also be exposed to the unit developer so
|
||||
they can make use of fast serialization/deserialization: matching our existing
|
||||
API conventions these should be ucb_encode(DValue val) and ucb_decode(String val). This may also be
|
||||
a worthwhile target for session variables storage (either change session
|
||||
to DValue or add StringMap support to UCEB1 ser/de).
|
||||
|
||||
### 5.4 Unit module contract
|
||||
|
||||
```
|
||||
@ -309,7 +318,7 @@ custom sections: dylink.0 (standard), uce.abi { abi_version, toolchain_id }
|
||||
imports: env.memory, env.__indirect_function_table,
|
||||
env.__memory_base, env.__table_base,
|
||||
GOT.mem.* / GOT.func.* (resolved by loader),
|
||||
core symbols (malloc, uce_dtree_*, uce_print, ...)
|
||||
core symbols (malloc, uce_dv_*, uce_print, ...)
|
||||
exports: uce_unit_setup, uce_unit_render,
|
||||
uce_unit_component, uce_unit_websocket
|
||||
(same roles as today's UCE_SETUP/RENDER/COMPONENT/WEBSOCKET
|
||||
@ -358,9 +367,9 @@ the Memory object, so OS-level CoW is available). No units are pre-fed.
|
||||
## 7. Request lifecycle (replaces the native flow in linux_fastcgi.cpp)
|
||||
|
||||
```
|
||||
1. accept request
|
||||
1. accept request (fastcgi, websockets message, socket event, CLI request)
|
||||
2. workspace = birth_from_core_snapshot() (CoW, ~µs)
|
||||
3. write wire-encoded context into workspace; core decodes → context DTree
|
||||
3. write wire-encoded context into workspace; core decodes → context DValue
|
||||
4. resolve entry unit (load on first call); call uce_unit_render(ctx_ptr)
|
||||
5. component(path) inside guest → resolve hostcall → (lazy load) →
|
||||
call_indirect — reference semantics throughout
|
||||
@ -387,30 +396,29 @@ Memory limit: linear memory max → allocation failure / trap → same path.
|
||||
- The site tree, docs, demo, and the network test suite
|
||||
(`tests/run_network_tests.py`) — which becomes the parity harness (§9).
|
||||
- nginx/FastCGI front-end integration, worker model, websocket event flow
|
||||
(events re-enter via the websocket entry point; note: a workspace-per-event
|
||||
or workspace-per-connection decision is an open question, §11).
|
||||
(events re-enter via the websocket entry point).
|
||||
|
||||
---
|
||||
|
||||
## 9. Implementation plan
|
||||
|
||||
Phases are sequential; each has an exit criterion. No phase except 0 and 2's
|
||||
scaffolding produces throwaway work.
|
||||
scaffolding produces throwaway work. All dependencies must be vendored.
|
||||
|
||||
**Phase 0 — toolchain & runtime spike (timeboxed).**
|
||||
**Phase 0 — toolchain & runtime.**
|
||||
Validate: wasi-sdk `-fPIC` + `wasm-ld -shared` on a representative generated
|
||||
unit; cross-module C++ calls with shared memory/table; exceptions decision
|
||||
(wasm EH vs. error-code discipline at unit boundaries — pick one, record it);
|
||||
vendored runtime selection. Candidates: **WAMR** (C, small, designed for
|
||||
embedding, easiest to vendor and patch — fits the project's vendoring
|
||||
practice) vs. **Wasmtime** (fastest, best AOT/CoW machinery, Rust — heavier
|
||||
to vendor/patch). Selection criteria: imported-memory + shared-table support,
|
||||
practice and would be preferred) vs. **Wasmtime** (fastest, best AOT/CoW machinery, Rust — heavier
|
||||
to vendor/patch, use only if blocked on WAMR). Selection criteria: imported-memory + shared-table support,
|
||||
AOT artifact quality, patchability. Exit: a two-module (core stub + unit
|
||||
stub) hello-world linked at runtime by a minimal loader, in the chosen
|
||||
vendored runtime.
|
||||
|
||||
**Phase 1 — DTree C ABI in the native runtime.**
|
||||
Introduce `uce_dtree_*` (§5.2) and the UCEB1 codec in `src/lib/`, used
|
||||
**Phase 1 — DValue C ABI in the native runtime.**
|
||||
Introduce `uce_dv_*` (§5.2) and the UCEB1 codec in `src/lib/`, used
|
||||
natively. Zero wasm dependency; immediately testable; freezes the contract
|
||||
everything else builds on. Exit: codec round-trip + accessor tests in the
|
||||
existing suite; ABI doc checked in.
|
||||
@ -443,16 +451,15 @@ on the site tree; audit `site/` for cross-request-static reliance (§3.2
|
||||
breaking change); benchmark suite (template-heavy page, sqlite page,
|
||||
component-heavy starter page) with budgets: ≤2× native page latency,
|
||||
workspace birth ≤100µs, internal component call overhead within 10× native
|
||||
call cost. Exit: numbers published in this document; go/no-go for default
|
||||
backend.
|
||||
call cost. Exit: numbers published in this document, all tests and reviews pass.
|
||||
|
||||
**Phase 6 — second plane (deferred until wanted).**
|
||||
Cross-instance call mechanism (props-in/output-out, UCEB1), first Plane B
|
||||
language binding, loader enforcement of the cross-plane context rule (§4).
|
||||
Plane A second language (Rust) as the cheaper first polyglot proof.
|
||||
|
||||
The native `.so` backend remains in-tree and selectable until Phase 5's
|
||||
go/no-go; both backends share the Phase 1 C ABI.
|
||||
The native `.so` backend remains in-tree (as a reference) and selectable by config
|
||||
but we switch over to the wasm backend as soon as it's available and test
|
||||
only on that; both backends share the Phase 1 C ABI.
|
||||
|
||||
---
|
||||
|
||||
@ -468,18 +475,22 @@ go/no-go; both backends share the Phase 1 C ABI.
|
||||
| Multi-module DWARF / debugging story | Trap stack traces cover the production case (better than today); accept weaker interactive debugging; keep native backend for local deep-debugging |
|
||||
| Unit-statics semantic change breaks existing pages | Phase 5 audit of `site/`; documented migration note; host-side cache facility if a real need surfaces |
|
||||
|
||||
## 11. Open questions
|
||||
## 11. Decisions
|
||||
|
||||
1. **Exceptions:** wasm EH or error codes at unit boundaries? (Phase 0.)
|
||||
1. **Exceptions:** wasm EH or error codes at unit boundaries? Error codes.
|
||||
2. **WebSocket granularity:** workspace per event (pure arena, statics reset
|
||||
per event) or per connection (state across events, bounded lifetime)?
|
||||
Leaning per-event for consistency; needs a look at current WS page usage.
|
||||
3. **UCEB1 final layout** vs. DTree's actual field set (value/children/list
|
||||
flag) — finalize in Phase 1.
|
||||
Per-event. Should be compatible with WS since the WS contract is: runtime holds
|
||||
and brokers connections, makes request to unit's WS() {...} directive, unit
|
||||
may decide to send data back over WS to any or even all connected clients.
|
||||
3. **UCEB1 final layout** vs. DValue's actual field set (value/children/list
|
||||
flag) — somewhat open, finalize in Phase 1.
|
||||
4. **Cursor vs. bulk** as the default for `sqlite_query` results at the
|
||||
membrane (offer both; pick the default after Phase 5 benchmarks).
|
||||
membrane (offer both; pick the default after Phase 5 benchmarks, tending towards cursor right now).
|
||||
5. **Streaming output:** today's ob model buffers; does the membrane expose
|
||||
`uce_host_stream_write` from day one or post-MVP?
|
||||
`uce_host_stream_write` from day one or post-MVP? Undecided. ob is a critical
|
||||
abstraction to our whole execution model and whether we expose direct stream
|
||||
write or not, the existing PHP-like ob contract/paradigm must stay in place.
|
||||
6. **Core snapshot rebuild cadence** once placement memoization lands
|
||||
(dead-base reclamation policy).
|
||||
|
||||
@ -488,7 +499,7 @@ go/no-go; both backends share the Phase 1 C ABI.
|
||||
The module is the **unit** (file-grained, lazily compiled, lazily loaded —
|
||||
including mid-request). The instance set is the **workspace** (one per
|
||||
request; shared memory + table; born from a core-only CoW snapshot; dropped
|
||||
wholesale — the arena, done right). The contract is the **DTree C ABI**
|
||||
wholesale — the arena, done right). The contract is the **DValue C ABI**
|
||||
inside the workspace (pointer semantics, no serialization) and the **UCEB1
|
||||
wire encoding + handles** at every true address-space boundary (host
|
||||
membrane, Plane B languages, future trust boundaries). Serialization
|
||||
|
||||
@ -28,7 +28,7 @@ Legend: `[ ]` not started, `[~]` in progress, `[x]` done, `[!]` blocked, `[-]` s
|
||||
- Why: React-framework developers routinely shape arrays/objects near render code.
|
||||
- Done when: helpers are declared, implemented, documented, and covered by tests.
|
||||
- Verify: build plus focused site/network tests.
|
||||
- [x] G1.1: Identify current `StringList`/`DTree` idioms and choose helper surface.
|
||||
- [x] G1.1: Identify current `StringList`/`DValue` idioms and choose helper surface.
|
||||
- [x] G1.2: Implement minimal high-value helpers without broad template complexity.
|
||||
- [x] G1.3: Add docs and examples for helpers.
|
||||
- [x] G1.4: Add/extend tests.
|
||||
@ -67,7 +67,7 @@ Complete.
|
||||
- 2026-05-28: Defer component children/slots and JSX-like preprocessor syntax.
|
||||
- 2026-05-28: Do not add global runtime head/assets/islands APIs; implement asset/island as starter-local components.
|
||||
- 2026-05-28: Do not add a generic runtime file-based-routing system; demonstrate hierarchical/file routing inside starter `index.uce`.
|
||||
- 2026-05-28: Keep collection helpers explicit (`list_*`, `dtree_*`) instead of overloading generic names such as `map`/`sort`.
|
||||
- 2026-05-28: Keep collection helpers explicit (`list_*`, `dv_*`) instead of overloading generic names such as `map`/`sort`.
|
||||
|
||||
## Assumptions
|
||||
|
||||
@ -85,7 +85,7 @@ Complete.
|
||||
- 2026-05-28: `ssh k-uce 'cd /Code/uce.openfu.com/uce && bash scripts/build_linux.sh'` succeeded.
|
||||
- 2026-05-28: Restarted `uce.service` on `k-uce`.
|
||||
- 2026-05-28: `tests/run_network_tests.py --match core` passed.
|
||||
- 2026-05-28: Manual checks returned `200` for `/examples/uce-starter/index.uce`, `?dashboard`, `?workspace/projects`, `?themes`, `/demo/collections.uce`, `/doc/index.uce?p=coming_from_react`, `/doc/index.uce?p=list_map`, and `/doc/index.uce?p=dtree_group_by`.
|
||||
- 2026-05-28: Manual checks returned `200` for `/examples/uce-starter/index.uce`, `?dashboard`, `?workspace/projects`, `?themes`, `/demo/collections.uce`, `/doc/index.uce?p=coming_from_react`, `/doc/index.uce?p=list_map`, and `/doc/index.uce?p=dv_group_by`.
|
||||
- 2026-05-28: Full internal network suite passed, 25/25.
|
||||
- 2026-05-28: Temporary broken `/tests/diagnostic-probe.uce` returned `500` with formatted `UCE compile error` diagnostics; source and cache artifacts were removed afterward.
|
||||
|
||||
|
||||
118
scripts/refactor/rename-dvalue-api.py
Executable file
118
scripts/refactor/rename-dvalue-api.py
Executable file
@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Mechanical one-shot rename script for UCE's legacy value-tree API.
|
||||
|
||||
The old tokens are constructed rather than written literally so this script can
|
||||
remain in the tree after the rename without reintroducing the retired names.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
OLD_TYPE = "D" + "Tree"
|
||||
OLD_LOWER = "d" + "tree"
|
||||
NEW_TYPE = "DValue"
|
||||
NEW_LOWER = "dvalue"
|
||||
|
||||
DIR_EXCLUDES = {
|
||||
".git",
|
||||
".hg",
|
||||
".svn",
|
||||
"3rdparty",
|
||||
"bin",
|
||||
"node_modules",
|
||||
"pkg",
|
||||
"tmp",
|
||||
"work",
|
||||
"__pycache__",
|
||||
}
|
||||
|
||||
BINARY_SUFFIXES = {
|
||||
".bin",
|
||||
".gif",
|
||||
".ico",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".lock",
|
||||
".o",
|
||||
".pdf",
|
||||
".png",
|
||||
".pyc",
|
||||
".so",
|
||||
".sqlite",
|
||||
".webp",
|
||||
".zip",
|
||||
}
|
||||
|
||||
REPLACEMENTS = (
|
||||
(OLD_TYPE, NEW_TYPE),
|
||||
(OLD_TYPE.upper(), NEW_TYPE.upper()),
|
||||
(OLD_LOWER + "_", "dv_"),
|
||||
(OLD_LOWER, NEW_LOWER),
|
||||
)
|
||||
|
||||
|
||||
def replace_text(value: str) -> str:
|
||||
for old, new in REPLACEMENTS:
|
||||
value = value.replace(old, new)
|
||||
return value
|
||||
|
||||
|
||||
def skip_dir(name: str) -> bool:
|
||||
return name in DIR_EXCLUDES or name.startswith(".fuse")
|
||||
|
||||
|
||||
def skip_file(path: Path) -> bool:
|
||||
return path.name.startswith(".fuse") or path.suffix in BINARY_SUFFIXES
|
||||
|
||||
|
||||
def rewrite_text_files() -> int:
|
||||
changed = 0
|
||||
for current, dirs, files in os.walk(ROOT):
|
||||
dirs[:] = [name for name in dirs if not skip_dir(name)]
|
||||
for name in files:
|
||||
path = Path(current) / name
|
||||
if skip_file(path):
|
||||
continue
|
||||
data = path.read_bytes()
|
||||
if b"\0" in data:
|
||||
continue
|
||||
try:
|
||||
original = data.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
updated = replace_text(original)
|
||||
if updated != original:
|
||||
path.write_text(updated, encoding="utf-8")
|
||||
changed += 1
|
||||
return changed
|
||||
|
||||
|
||||
def rename_paths() -> int:
|
||||
changed = 0
|
||||
matches: list[Path] = []
|
||||
for current, dirs, files in os.walk(ROOT, topdown=False):
|
||||
dirs[:] = [name for name in dirs if not skip_dir(name)]
|
||||
current_path = Path(current)
|
||||
for name in files + dirs:
|
||||
new_name = replace_text(name)
|
||||
if new_name != name:
|
||||
matches.append(current_path / name)
|
||||
for path in sorted(matches, key=lambda item: len(item.parts), reverse=True):
|
||||
if not path.exists():
|
||||
continue
|
||||
target = path.with_name(replace_text(path.name))
|
||||
if target.exists():
|
||||
raise RuntimeError(f"cannot rename {path} -> {target}: target exists")
|
||||
path.rename(target)
|
||||
changed += 1
|
||||
return changed
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print(f"Updated {rewrite_text_files()} files and renamed {rename_paths()} paths.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,6 +1,6 @@
|
||||
// Minimal exported helper used by the `unit_call()` demo page.
|
||||
|
||||
EXPORT DTree* test_func(DTree* call_param)
|
||||
EXPORT DValue* test_func(DValue* call_param)
|
||||
{
|
||||
print("HELLO FROM TEST FUNCTION");
|
||||
return(0);
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree p;
|
||||
DValue p;
|
||||
p.set(context.params);
|
||||
|
||||
StringList routes = {"index", "dashboard", "themes", "dashboard", "workspace/projects"};
|
||||
@ -10,15 +10,15 @@ RENDER(Request& context)
|
||||
StringList sorted_routes = list_sort(unique_routes);
|
||||
StringList labels = map(sorted_routes, [](String route) { return(to_upper(replace(route, "/", " / "))); });
|
||||
|
||||
DTree cards;
|
||||
DTree card;
|
||||
DValue cards;
|
||||
DValue card;
|
||||
card["title"] = "Dashboard"; card["section"] = "app"; card["href"] = "?dashboard"; cards.push(card); card.clear();
|
||||
card["title"] = "Themes"; card["section"] = "app"; card["href"] = "?themes"; cards.push(card); card.clear();
|
||||
card["title"] = "Docs"; card["section"] = "reference"; card["href"] = "../doc/index.uce"; cards.push(card);
|
||||
|
||||
DTree app_cards = dtree_filter(cards, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
|
||||
DTree titles = dtree_map(app_cards, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); });
|
||||
DTree grouped = dtree_group_by(cards, [](DTree item, String key) { return(item["section"].to_string()); });
|
||||
DValue app_cards = dv_filter(cards, [](DValue item, String key) { return(item["section"].to_string() == "app"); });
|
||||
DValue titles = dv_map(app_cards, [](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); });
|
||||
DValue grouped = dv_group_by(cards, [](DValue item, String key) { return(item["section"].to_string()); });
|
||||
|
||||
<><html>
|
||||
<head>
|
||||
@ -30,7 +30,7 @@ RENDER(Request& context)
|
||||
<h2>Collection Helpers</h2>
|
||||
<p>Small data-shaping helpers are useful for route lists, nav records, cards, and other render-adjacent structures.</p>
|
||||
<div class="system-info"><h3>StringList route labels</h3><pre><?= join(labels, "\n") ?></pre></div>
|
||||
<div class="system-info"><h3>DTree app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
|
||||
<div class="system-info"><h3>DValue app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
|
||||
<div class="system-info"><h3>Grouped cards</h3><pre><?= json_encode(grouped) ?></pre></div>
|
||||
<details>
|
||||
<summary>Request Parameters</summary>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree card_props;
|
||||
DValue card_props;
|
||||
card_props["title"] = "Component Example";
|
||||
card_props["body"] = "This card body comes from context.props and is rendered through component().";
|
||||
|
||||
DTree named_props;
|
||||
DValue named_props;
|
||||
named_props["title"] = "Named Render Example";
|
||||
named_props["body"] = "This content is rendered through the COMPONENT:BODY(Request& context) entry point.";
|
||||
|
||||
|
||||
@ -78,7 +78,7 @@ void test_demo_render_restricted_json(Request& context, String title, String ris
|
||||
{
|
||||
context.set_status(403, "Restricted");
|
||||
context.header["Content-Type"] = "application/json; charset=utf-8";
|
||||
DTree payload;
|
||||
DValue payload;
|
||||
payload["ok"].set_bool(false);
|
||||
payload["error"] = "restricted";
|
||||
payload["title"] = title;
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
DTree
|
||||
DValue
|
||||
</h1>
|
||||
|
||||
<h2>Value Types</h2>
|
||||
@ -33,21 +33,21 @@ RENDER(Request& context)
|
||||
|
||||
<pre><?
|
||||
|
||||
DTree scalar_text;
|
||||
DValue scalar_text;
|
||||
scalar_text = "42.75";
|
||||
print("text -> f64: ", scalar_text.to_f64(), "\n");
|
||||
print("text -> u64: ", scalar_text.to_u64(), "\n");
|
||||
print("text -> s64: ", scalar_text.to_s64(), "\n");
|
||||
|
||||
DTree bool_text;
|
||||
DValue bool_text;
|
||||
bool_text = "yes";
|
||||
print("yes -> bool: ", bool_text.to_bool() ? "true" : "false", "\n");
|
||||
|
||||
DTree single_value_map;
|
||||
DValue single_value_map;
|
||||
single_value_map["value"] = "123";
|
||||
print("{value:123} -> u64: ", single_value_map.to_u64(), "\n");
|
||||
|
||||
DTree headers;
|
||||
DValue headers;
|
||||
headers["Content-Type"] = "text/plain";
|
||||
headers["X-Test"] = "123";
|
||||
print("map -> StringMap:\n", var_dump(headers.to_stringmap()), "\n");
|
||||
@ -58,12 +58,12 @@ RENDER(Request& context)
|
||||
|
||||
<pre><?
|
||||
|
||||
DTree lookup;
|
||||
DValue lookup;
|
||||
print("has(user) before create: ", lookup.has("user") ? "true" : "false", "\n");
|
||||
print("key(user) before create: ", lookup.key("user") ? "present" : "missing", "\n");
|
||||
lookup.get_or_create("user")->set("Ada");
|
||||
print("has(user) after create: ", lookup.has("user") ? "true" : "false", "\n");
|
||||
if(DTree* user = lookup.key("user"))
|
||||
if(DValue* user = lookup.key("user"))
|
||||
print("key(user) after create: ", user->to_string(), "\n");
|
||||
|
||||
?></pre>
|
||||
@ -90,11 +90,11 @@ RENDER(Request& context)
|
||||
|
||||
<pre><?
|
||||
|
||||
DTree assoc_left;
|
||||
DValue assoc_left;
|
||||
assoc_left["name"] = "left";
|
||||
assoc_left["shared"] = "left-value";
|
||||
|
||||
DTree assoc_right;
|
||||
DValue assoc_right;
|
||||
assoc_right["shared"] = "right-value";
|
||||
assoc_right["extra"] = "new-value";
|
||||
|
||||
@ -108,15 +108,15 @@ RENDER(Request& context)
|
||||
|
||||
<pre><?
|
||||
|
||||
DTree list_left;
|
||||
DValue list_left;
|
||||
list_left.set_array();
|
||||
DTree a; a = "A"; list_left.push(a);
|
||||
DTree b; b = "B"; list_left.push(b);
|
||||
DValue a; a = "A"; list_left.push(a);
|
||||
DValue b; b = "B"; list_left.push(b);
|
||||
|
||||
DTree list_right;
|
||||
DValue list_right;
|
||||
list_right.set_array();
|
||||
DTree c; c = "C"; list_right.push(c);
|
||||
DTree d; d = "D"; list_right.push(d);
|
||||
DValue c; c = "C"; list_right.push(c);
|
||||
DValue d; d = "D"; list_right.push(d);
|
||||
|
||||
print("left:\n", var_dump(list_left), "\n");
|
||||
print("right:\n", var_dump(list_right), "\n");
|
||||
@ -11,7 +11,7 @@ RENDER(Request& context)
|
||||
test_demo_render_restricted_html(context, "File Append", "write to server-side files");
|
||||
return;
|
||||
}
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
|
||||
@ -12,7 +12,7 @@ void render_card(String url, String title, String desc)
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree p;
|
||||
DValue p;
|
||||
p.set(context.params);
|
||||
bool allow_server_demos = test_demo_request_allowed(context);
|
||||
|
||||
@ -35,10 +35,10 @@ RENDER(Request& context)
|
||||
<? if(allow_server_demos) { render_card("error-reporting.uce", "Error Reporting", "Error handling and output"); } ?>
|
||||
|
||||
<div class="grid-heading">Data Types & Parsing</div>
|
||||
<? render_card("dtree.uce", "DTree", "Dynamic hierarchical data tree"); ?>
|
||||
<? render_card("dvalue.uce", "DValue", "Dynamic hierarchical data tree"); ?>
|
||||
<? render_card("json.uce", "JSON", "Parse and encode JSON data"); ?>
|
||||
<? render_card("xml.uce", "XML", "Structural XML encode/decode with DTree"); ?>
|
||||
<? render_card("yaml.uce", "YAML", "Concise config files with DTree"); ?>
|
||||
<? render_card("xml.uce", "XML", "Structural XML encode/decode with DValue"); ?>
|
||||
<? render_card("yaml.uce", "YAML", "Concise config files with DValue"); ?>
|
||||
<? render_card("preprocessor-comments.uce", "Preprocessor Comments", "Regression coverage for comment parsing in templates"); ?>
|
||||
<? render_card("regex.uce", "Regular Expressions", "PCRE2 matching, captures, replacement, and splitting"); ?>
|
||||
<? render_card("string.uce", "String", "String operations"); ?>
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
DTree/JSON
|
||||
DValue/JSON
|
||||
</h1>
|
||||
|
||||
JSON:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree options;
|
||||
DValue options;
|
||||
options["components"][":::warning"] = "components/markdown/warning";
|
||||
options["components"]["node.code_block"] = "components/markdown/code_block";
|
||||
|
||||
@ -9,7 +9,7 @@ RENDER(Request& context)
|
||||
file_get_contents("markdown-example.md")
|
||||
);
|
||||
|
||||
DTree ast = markdown_to_ast(markdown_src, options);
|
||||
DValue ast = markdown_to_ast(markdown_src, options);
|
||||
String html = markdown_to_html(markdown_src, options);
|
||||
|
||||
<>
|
||||
|
||||
@ -9,7 +9,7 @@ RENDER(Request& context)
|
||||
test_demo_render_restricted_html(context, "MemcacheD", "inspect and mutate local memcached state");
|
||||
return;
|
||||
}
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
|
||||
@ -28,13 +28,13 @@ COMPONENT:PROBE(Request& context)
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree first;
|
||||
DValue first;
|
||||
first["label"] = "First component call";
|
||||
|
||||
DTree second;
|
||||
DValue second;
|
||||
second["label"] = "Second component call in the same request";
|
||||
|
||||
DTree third;
|
||||
DValue third;
|
||||
third["label"] = "Third component call through unit_call(\"COMPONENT:PROBE\")";
|
||||
|
||||
<><html>
|
||||
|
||||
@ -19,8 +19,8 @@ void regex_bool_row(String label, bool result, bool expect)
|
||||
RENDER(Request& context)
|
||||
{
|
||||
String text = "Contact ops@example.test or tag #uce, #docs, and café.";
|
||||
DTree first_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", text);
|
||||
DTree tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", text);
|
||||
DValue first_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", text);
|
||||
DValue tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", text);
|
||||
StringList pieces = regex_split("\\s*,\\s*", "uce, components, markdown");
|
||||
|
||||
<>
|
||||
@ -38,7 +38,7 @@ RENDER(Request& context)
|
||||
Regular Expressions
|
||||
</h1>
|
||||
|
||||
<p>UCE regex functions use PCRE2 and return ordinary UCE strings, lists, and DTree values.</p>
|
||||
<p>UCE regex functions use PCRE2 and return ordinary UCE strings, lists, and DValue values.</p>
|
||||
<p>sample = <code><?= text ?></code></p>
|
||||
|
||||
<h2>Validation And Search</h2>
|
||||
|
||||
@ -23,7 +23,7 @@ RENDER(Request& context)
|
||||
return;
|
||||
}
|
||||
|
||||
DTree notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10");
|
||||
DValue notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10");
|
||||
String error = sqlite_error(db);
|
||||
?><html>
|
||||
<head>
|
||||
@ -41,7 +41,7 @@ RENDER(Request& context)
|
||||
<? if(error != "ok" && error != "connected") { ?><pre><?= error ?></pre><? } ?>
|
||||
</div>
|
||||
<div class="test-grid">
|
||||
<? notes.each([&](DTree note, String key) { ?>
|
||||
<? notes.each([&](DValue note, String key) { ?>
|
||||
<div class="test-card">
|
||||
<strong>#<?= note["id"].to_string() ?> <?= note["created_at"].to_string() ?></strong>
|
||||
<span><?= note["body"].to_string() ?></span>
|
||||
|
||||
@ -8,7 +8,7 @@ RENDER(Request& context)
|
||||
test_demo_render_restricted_html(context, "Tasks", "spawn and inspect background tasks");
|
||||
return;
|
||||
}
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
String task_name = first(context.get["task-name"], "example-task");
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ RENDER(Request& context)
|
||||
test_demo_render_restricted_html(context, "Task repeat", "schedule recurring background tasks");
|
||||
return;
|
||||
}
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
String task_name = first(context.get["task-name"], "example-task");
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ String unit_status_class(String compile_status, String error_status)
|
||||
return("status-muted");
|
||||
}
|
||||
|
||||
String unit_flag_label(DTree value)
|
||||
String unit_flag_label(DValue value)
|
||||
{
|
||||
return(value.to_string() == "(true)" ? "✅" : "❌");
|
||||
}
|
||||
@ -61,7 +61,7 @@ RENDER(Request& context)
|
||||
if(selected_path == "" && unit_paths.size() > 0)
|
||||
selected_path = unit_paths.front();
|
||||
|
||||
DTree selected_info = unit_info(selected_path);
|
||||
DValue selected_info = unit_info(selected_path);
|
||||
String selected_compile_status = selected_info["compile_status"].to_string();
|
||||
String selected_error_status = selected_info["error_status"].to_string();
|
||||
String selected_status_class = unit_status_class(selected_compile_status, selected_error_status);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
bool allow_mutation = test_demo_request_allowed(context);
|
||||
DTree payload;
|
||||
DValue payload;
|
||||
|
||||
if(context.get["compile"] != "")
|
||||
{
|
||||
@ -19,11 +19,11 @@ RENDER(Request& context)
|
||||
|
||||
payload["current"] = unit_info();
|
||||
|
||||
DTree units;
|
||||
DValue units;
|
||||
units.set_array();
|
||||
for(auto& unit_path : units_list())
|
||||
{
|
||||
DTree item;
|
||||
DValue item;
|
||||
item = unit_path;
|
||||
units.push(item);
|
||||
}
|
||||
|
||||
@ -8,9 +8,9 @@ String clean_chat_field(String raw, u32 max_length)
|
||||
return(value);
|
||||
}
|
||||
|
||||
DTree chat_event(Request& context, String type, String body)
|
||||
DValue chat_event(Request& context, String type, String body)
|
||||
{
|
||||
DTree event;
|
||||
DValue event;
|
||||
event["type"] = type;
|
||||
event["body"] = body;
|
||||
event["connection_id"] = context.params["WS_CONNECTION_ID"];
|
||||
@ -159,7 +159,7 @@ WS(Request& context)
|
||||
return;
|
||||
}
|
||||
|
||||
DTree payload = json_decode(context.in);
|
||||
DValue payload = json_decode(context.in);
|
||||
String type = clean_chat_field(payload["type"].to_string(), 24);
|
||||
String name = clean_chat_field(payload["name"].to_string(), 32);
|
||||
String body = clean_chat_field(payload["body"].to_string(), 500);
|
||||
@ -183,7 +183,7 @@ WS(Request& context)
|
||||
context.connection["last_type"] = type;
|
||||
context.connection["last_body"] = body;
|
||||
context.connection["message_count"] = (f64)(message_count + 1);
|
||||
DTree event = chat_event(context, "message", body);
|
||||
DValue event = chat_event(context, "message", body);
|
||||
ws_send(json_encode(event));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2,31 +2,31 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree book;
|
||||
DValue book;
|
||||
book["name"] = "book";
|
||||
book["attrs"]["id"] = "b1";
|
||||
book["attrs"]["type"] = "reference";
|
||||
|
||||
DTree title;
|
||||
DValue title;
|
||||
title["name"] = "title";
|
||||
title["text"] = "UCE & XML";
|
||||
book["children"].push(title);
|
||||
|
||||
DTree chapter;
|
||||
DValue chapter;
|
||||
chapter["name"] = "chapter";
|
||||
chapter["attrs"]["number"] = "1";
|
||||
chapter["text"] = "Structural conversion without schema validation.";
|
||||
book["children"].push(chapter);
|
||||
|
||||
String encoded = xml_encode(book);
|
||||
DTree decoded = xml_decode(encoded);
|
||||
DValue decoded = xml_decode(encoded);
|
||||
|
||||
DTree simple;
|
||||
DValue simple;
|
||||
simple["title"] = "Hello";
|
||||
simple["count"] = "3";
|
||||
|
||||
String incoming = "<note priority=\"high\"><to>UCE</to><body><![CDATA[5 < 6]]></body><symbol>AB</symbol></note>";
|
||||
DTree incoming_tree = xml_decode(incoming);
|
||||
DValue incoming_tree = xml_decode(incoming);
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
@ -35,12 +35,12 @@ RENDER(Request& context)
|
||||
XML
|
||||
</h1>
|
||||
|
||||
<p><code>xml_encode()</code> and <code>xml_decode()</code> convert between XML strings and element-shaped <code>DTree</code> values without schema validation.</p>
|
||||
<p><code>xml_encode()</code> and <code>xml_decode()</code> convert between XML strings and element-shaped <code>DValue</code> values without schema validation.</p>
|
||||
|
||||
<h2>Encoded Element Tree</h2>
|
||||
<pre><?= encoded ?></pre>
|
||||
|
||||
<h2>Decoded DTree</h2>
|
||||
<h2>Decoded DValue</h2>
|
||||
<pre><?= json_encode(decoded) ?></pre>
|
||||
|
||||
<h2>Simple Map Encoding</h2>
|
||||
@ -49,7 +49,7 @@ RENDER(Request& context)
|
||||
<h2>Decode Existing XML</h2>
|
||||
<p>Input XML:</p>
|
||||
<pre><?= incoming ?></pre>
|
||||
<p>Decoded DTree:</p>
|
||||
<p>Decoded DValue:</p>
|
||||
<pre><?= json_encode(incoming_tree) ?></pre>
|
||||
|
||||
<p>
|
||||
|
||||
@ -12,19 +12,19 @@ RENDER(Request& context)
|
||||
" - cache\n"
|
||||
"message: |\n"
|
||||
" Keep config files readable.\n"
|
||||
" Load them as DTree values.\n";
|
||||
" Load them as DValue values.\n";
|
||||
|
||||
DTree cfg = yaml_decode(source);
|
||||
DValue cfg = yaml_decode(source);
|
||||
String encoded = yaml_encode(cfg);
|
||||
DTree roundtrip = yaml_decode(encoded);
|
||||
DValue roundtrip = yaml_decode(encoded);
|
||||
|
||||
DTree generated;
|
||||
DValue generated;
|
||||
generated["database"]["host"] = "localhost";
|
||||
generated["database"]["port"] = (f64)3306;
|
||||
generated["features"]["components"].set_bool(true);
|
||||
generated["features"]["markdown"].set_bool(true);
|
||||
|
||||
DTree theme;
|
||||
DValue theme;
|
||||
theme = "clean";
|
||||
generated["themes"].push(theme);
|
||||
theme = "compact";
|
||||
@ -37,12 +37,12 @@ RENDER(Request& context)
|
||||
YAML
|
||||
</h1>
|
||||
|
||||
<p><code>yaml_encode()</code> and <code>yaml_decode()</code> convert concise config-style YAML to and from <code>DTree</code> values.</p>
|
||||
<p><code>yaml_encode()</code> and <code>yaml_decode()</code> convert concise config-style YAML to and from <code>DValue</code> values.</p>
|
||||
|
||||
<h2>Config Source</h2>
|
||||
<pre><?= source ?></pre>
|
||||
|
||||
<h2>Decoded DTree</h2>
|
||||
<h2>Decoded DValue</h2>
|
||||
<pre><?= json_encode(cfg) ?></pre>
|
||||
|
||||
<h2>Encoded Again</h2>
|
||||
|
||||
@ -14,12 +14,12 @@ RENDER(Request& context)
|
||||
mkdir(base);
|
||||
mkdir(extract_dir);
|
||||
|
||||
DTree entries;
|
||||
DValue entries;
|
||||
entries["hello.txt"] = "Hello from a generated ZIP archive.\n";
|
||||
entries["notes/readme.txt"] = "zip_create(), zip_list(), zip_read(), and zip_extract() are available to UCE pages.\n";
|
||||
zip_create(archive, entries);
|
||||
|
||||
DTree listing = zip_list(archive);
|
||||
DValue listing = zip_list(archive);
|
||||
String hello = zip_read(archive, "hello.txt");
|
||||
zip_extract(archive, extract_dir);
|
||||
String extracted = file_get_contents(path_join(extract_dir, "notes/readme.txt"));
|
||||
|
||||
@ -2,14 +2,14 @@ Types
|
||||
|
||||
0_Request
|
||||
array_merge
|
||||
0_DTree
|
||||
dtree_filter
|
||||
dtree_group_by
|
||||
dtree_keys
|
||||
dtree_map
|
||||
dtree_omit
|
||||
dtree_pick
|
||||
dtree_values
|
||||
0_DValue
|
||||
dv_filter
|
||||
dv_group_by
|
||||
dv_keys
|
||||
dv_map
|
||||
dv_omit
|
||||
dv_pick
|
||||
dv_values
|
||||
each
|
||||
get_by_path
|
||||
has
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
:title
|
||||
DTree
|
||||
DValue
|
||||
|
||||
:sig
|
||||
DTree
|
||||
DValue
|
||||
|
||||
:see
|
||||
>types
|
||||
@ -10,23 +10,23 @@ get_by_path
|
||||
json_decode
|
||||
|
||||
:content
|
||||
`DTree` is UCE's general-purpose structured value type. It is the runtime's default container for nested data such as configuration trees, call payloads, decoded JSON, connection state, and metadata returned by runtime helpers.
|
||||
`DValue` is UCE's general-purpose structured value type. It is the runtime's default container for nested data such as configuration trees, call payloads, decoded JSON, connection state, and metadata returned by runtime helpers.
|
||||
|
||||
## Value Kinds
|
||||
|
||||
`DTree` can hold:
|
||||
`DValue` can hold:
|
||||
|
||||
- `String`
|
||||
- `f64`
|
||||
- `bool`
|
||||
- pointer values
|
||||
- nested child `DTree` values in a map-shaped container
|
||||
- nested child `DValue` values in a map-shaped container
|
||||
|
||||
Map-shaped `DTree` values can also represent list-like data when their keys are numeric strings in sequence.
|
||||
Map-shaped `DValue` values can also represent list-like data when their keys are numeric strings in sequence.
|
||||
|
||||
## Where It Appears
|
||||
|
||||
You will encounter `DTree` throughout the runtime, especially in:
|
||||
You will encounter `DValue` throughout the runtime, especially in:
|
||||
|
||||
- `context.cfg`
|
||||
- `context.props`
|
||||
@ -47,7 +47,7 @@ You will encounter `DTree` throughout the runtime, especially in:
|
||||
- `.to_bool(default)` performs best-effort boolean conversion.
|
||||
- `.to_stringmap()` converts a map-shaped tree into `StringMap`.
|
||||
|
||||
All read accessors are `const` and never modify the tree; they work directly on `const DTree&` values such as `each()` callback parameters. Every `to_*` conversion takes an optional default that is returned when the value is missing or cannot be converted — see the individual pages (`to_string`, `to_s64`, `to_u64`, `to_f64`, `to_bool`) for the exact rules:
|
||||
All read accessors are `const` and never modify the tree; they work directly on `const DValue&` values such as `each()` callback parameters. Every `to_*` conversion takes an optional default that is returned when the value is missing or cannot be converted — see the individual pages (`to_string`, `to_s64`, `to_u64`, `to_f64`, `to_bool`) for the exact rules:
|
||||
|
||||
```cpp
|
||||
String title = context.props["title"].to_string("Untitled");
|
||||
@ -56,7 +56,7 @@ s64 page_size = context.cfg.get_by_path("app/page_size").to_s64(25);
|
||||
|
||||
`operator[]` creates missing entries, just like `std::map`. `.has()` and `.key()` are the non-mutating lookup helpers, and `.get_by_path()` is the non-creating traversal helper.
|
||||
|
||||
`json_decode()` currently stores JSON numbers as string-valued `DTree` nodes, so typed numeric conversion is the normal way to consume those values.
|
||||
`json_decode()` currently stores JSON numbers as string-valued `DValue` nodes, so typed numeric conversion is the normal way to consume those values.
|
||||
|
||||
References are dereferenced automatically in most normal reads.
|
||||
|
||||
@ -100,9 +100,9 @@ Useful inspection helpers include:
|
||||
|
||||
## each()
|
||||
|
||||
`each(std::function<void (const DTree& t, String key)> f)` iterates over the current tree value (see the `each` page for details).
|
||||
`each(std::function<void (const DValue& t, String key)> f)` iterates over the current tree value (see the `each` page for details).
|
||||
|
||||
For map-shaped `DTree` values, the callback runs once per child entry and receives:
|
||||
For map-shaped `DValue` values, the callback runs once per child entry and receives:
|
||||
|
||||
- `t` as the child value, by const reference (no copy)
|
||||
- `key` as the child key
|
||||
@ -113,7 +113,7 @@ For non-map values, `each()` still invokes the callback once:
|
||||
- `key` is an empty string
|
||||
|
||||
```cpp
|
||||
context.connection["items"].each([&](const DTree& item, String key) {
|
||||
context.connection["items"].each([&](const DValue& item, String key) {
|
||||
print(key, ": ", item.to_string(), "\n");
|
||||
});
|
||||
```
|
||||
@ -127,7 +127,7 @@ u64 compiled_mtime = unit_info("test/hello.uce")["compiled_mtime"].to_u64();
|
||||
|
||||
bool dark_mode = context.props["dark_mode"].to_bool();
|
||||
|
||||
if(DTree* user = payload.key("user")) {
|
||||
if(DValue* user = payload.key("user")) {
|
||||
print(user->to_json());
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ ws_message
|
||||
ws_connection_id
|
||||
ws_connections
|
||||
ws_send
|
||||
0_DTree
|
||||
0_DValue
|
||||
StringMap
|
||||
UploadedFile
|
||||
|
||||
@ -120,7 +120,7 @@ Raw request body. For WebSocket handlers, this is the current message payload.
|
||||
Use this for JSON APIs:
|
||||
|
||||
```cpp
|
||||
DTree body = json_decode(context.in);
|
||||
DValue body = json_decode(context.in);
|
||||
```
|
||||
|
||||
### `context.uploaded_files`
|
||||
@ -165,7 +165,7 @@ Related helpers:
|
||||
|
||||
### `context.call`
|
||||
|
||||
Type: `DTree`
|
||||
Type: `DValue`
|
||||
|
||||
General request-local scratch/configuration tree. It is shared by the page, components, and unit calls participating in the current request. Use it for app-level request state, fragments, router results, page type, page title, and other values that need to be read by later components.
|
||||
|
||||
@ -181,7 +181,7 @@ Prefer clear top-level names when state is app-wide (`route`, `fragments`) and n
|
||||
|
||||
### `context.cfg`
|
||||
|
||||
Type: `DTree`
|
||||
Type: `DValue`
|
||||
|
||||
Request-local structured configuration. The runtime does not fill this with application config by default; application code may assign it during boot/setup:
|
||||
|
||||
@ -199,19 +199,19 @@ String site_name = context.cfg.get_by_path("site/name").to_string();
|
||||
|
||||
### `context.props`
|
||||
|
||||
Type: `DTree`
|
||||
Type: `DValue`
|
||||
|
||||
Invocation-local props for `component()`, `component_render()`, and macro-style `unit_call()` entrypoints. During a component call, the runtime temporarily replaces `context.props` with the props passed to that component and restores the previous value after the call returns.
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
DValue props;
|
||||
props["title"] = "Dashboard";
|
||||
print(component("components/card", props, context));
|
||||
```
|
||||
|
||||
### `context.connection`
|
||||
|
||||
Type: `DTree`
|
||||
Type: `DValue`
|
||||
|
||||
WebSocket connection-local state. Mutations persist across `WS(Request& context)` calls for the same socket.
|
||||
|
||||
@ -394,7 +394,7 @@ RENDER(Request& context)
|
||||
RENDER(Request& context)
|
||||
{
|
||||
context.header["Content-Type"] = "application/json";
|
||||
DTree response;
|
||||
DValue response;
|
||||
response["ok"].set_bool(true);
|
||||
print(json_encode(response));
|
||||
}
|
||||
@ -413,7 +413,7 @@ RENDER(Request& context)
|
||||
### Component props
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
DValue props;
|
||||
props["title"] = "Welcome";
|
||||
print(component("components/card", props, context));
|
||||
```
|
||||
|
||||
@ -46,12 +46,12 @@ Inside the handler, the usual `Request& context` fields are available:
|
||||
|
||||
CLI responses default to `text/plain; charset=utf-8`, but the handler may set headers and status explicitly.
|
||||
|
||||
Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DTree`.
|
||||
Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DValue`.
|
||||
|
||||
```cpp
|
||||
CLI(Request& context)
|
||||
{
|
||||
DTree input = cli_input(context);
|
||||
DValue input = cli_input(context);
|
||||
String action = first(input["action"].to_string(), "ping");
|
||||
if(action == "ping")
|
||||
{
|
||||
|
||||
@ -20,7 +20,7 @@ UCE reassembles fragmented messages before calling `WS(Request& context)`. Text
|
||||
|
||||
## Connection State
|
||||
|
||||
`context.connection` is a broker-owned `DTree` for the current socket. It starts empty for a new client and persists across later `WS(Request& context)` calls on that same connection.
|
||||
`context.connection` is a broker-owned `DValue` for the current socket. It starts empty for a new client and persists across later `WS(Request& context)` calls on that same connection.
|
||||
|
||||
## Message Data
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
:sig
|
||||
StringMap array_merge(StringMap a, StringMap b)
|
||||
DTree array_merge(DTree a, DTree b)
|
||||
DValue array_merge(DValue a, DValue b)
|
||||
|
||||
:params
|
||||
a : left-hand source map or tree
|
||||
@ -9,7 +9,7 @@ return value : merged result
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
0_DValue
|
||||
StringMap
|
||||
json_decode
|
||||
|
||||
@ -18,7 +18,7 @@ Merges two maps or trees using PHP-like merge behavior.
|
||||
|
||||
For `StringMap`, keys from `b` overwrite keys from `a`.
|
||||
|
||||
For `DTree`, string keys from `b` overwrite keys from `a`. Numeric keys are appended and reindexed when either side behaves like a list.
|
||||
For `DValue`, string keys from `b` overwrite keys from `a`. Numeric keys are appended and reindexed when either side behaves like a list.
|
||||
|
||||
This helper is the closest UCE equivalent to PHP `array_merge()` for common request, config, and JSON-shaped data.
|
||||
|
||||
|
||||
@ -21,4 +21,4 @@ CLI(Request& context)
|
||||
}
|
||||
```
|
||||
|
||||
For commands that need multiple values or typed reads, prefer calling `cli_input(context)` once and reading the returned `DTree` directly.
|
||||
For commands that need multiple values or typed reads, prefer calling `cli_input(context)` once and reading the returned `DValue` directly.
|
||||
|
||||
@ -2,17 +2,17 @@
|
||||
cli_input
|
||||
|
||||
:sig
|
||||
DTree cli_input(Request& context)
|
||||
DValue cli_input(Request& context)
|
||||
|
||||
:see
|
||||
>1_CLI
|
||||
>json_decode
|
||||
>DTree
|
||||
>DValue
|
||||
|
||||
:content
|
||||
Returns a structured parameter tree for a `CLI(Request& context)` invocation.
|
||||
|
||||
`cli_input()` merges simple command inputs into one `DTree`:
|
||||
`cli_input()` merges simple command inputs into one `DValue`:
|
||||
|
||||
1. query parameters from `context.get`
|
||||
2. form parameters from `context.post`
|
||||
@ -25,7 +25,7 @@ If the JSON body is a scalar or array instead of an object, the decoded value is
|
||||
```cpp
|
||||
CLI(Request& context)
|
||||
{
|
||||
DTree input = cli_input(context);
|
||||
DValue input = cli_input(context);
|
||||
String action = first(input["action"].to_string(), "help");
|
||||
|
||||
if(action == "echo")
|
||||
|
||||
@ -12,7 +12,7 @@ unit_render
|
||||
3_C++ Preprocessor
|
||||
map
|
||||
filter
|
||||
dtree_filter
|
||||
dv_filter
|
||||
|
||||
:content
|
||||
UCE is server-first C++ with a small template preprocessor. It does not try to be React, but several concepts map cleanly.
|
||||
@ -47,8 +47,8 @@ The function library includes small collection helpers for common route/menu/car
|
||||
```cpp
|
||||
auto visible = filter(routes, [](String route) { return(route != "admin"); });
|
||||
auto labels = map(visible, [](String route) { return(to_upper(route)); });
|
||||
DTree app_items = dtree_filter(menu, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
|
||||
DTree by_section = dtree_group_by(menu, [](DTree item, String key) { return(item["section"].to_string()); });
|
||||
DValue app_items = dv_filter(menu, [](DValue item, String key) { return(item["section"].to_string() == "app"); });
|
||||
DValue by_section = dv_group_by(menu, [](DValue item, String key) { return(item["section"].to_string()); });
|
||||
```
|
||||
|
||||
Use these when the transformation communicates intent. Prefer explicit loops when side effects or multi-step validation are the main concern.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
String component(String name, [DTree props], [Request& context])
|
||||
String component(String name, [DValue props], [Request& context])
|
||||
|
||||
:see
|
||||
>ob
|
||||
@ -37,7 +37,7 @@ When a component unit defines `ONCE(Request& context)`, the runtime calls that h
|
||||
Default component handler:
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
DValue props;
|
||||
props["title"] = "Status";
|
||||
|
||||
<><?: component("workspace/panel", props, context) ?></>
|
||||
@ -46,7 +46,7 @@ props["title"] = "Status";
|
||||
Named component handler:
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
DValue props;
|
||||
props["title"] = "System";
|
||||
props["body"] = "Healthy";
|
||||
|
||||
@ -76,7 +76,7 @@ COMPONENT:BODY(Request& context)
|
||||
Preparing props in C++ before rendering:
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
DValue props;
|
||||
props["items"][0] = "alpha";
|
||||
props["items"][1] = "beta";
|
||||
props["items"][2] = "gamma";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
void component_render(String name, [DTree props], [Request& context])
|
||||
void component_render(String name, [DValue props], [Request& context])
|
||||
|
||||
:see
|
||||
>ob
|
||||
@ -24,7 +24,7 @@ Use `component_render()` when you want to write component output directly from C
|
||||
## Example
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
DValue props;
|
||||
props["body"] = "Hello";
|
||||
|
||||
component_render("components/card:BODY", props, context);
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
:title
|
||||
dtree_filter
|
||||
dv_filter
|
||||
|
||||
:sig
|
||||
DTree dtree_filter(DTree tree, function<bool (DTree, String)> f)
|
||||
DValue dv_filter(DValue tree, function<bool (DValue, String)> f)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
filter
|
||||
|
||||
:content
|
||||
@ -15,7 +15,7 @@ Keeps children for which f returns true. List-like input stays list-like.
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
DTree visible = dtree_filter(items, [](DTree item, String key) { return(item["hidden"].to_bool() == false); });
|
||||
DValue visible = dv_filter(items, [](DValue item, String key) { return(item["hidden"].to_bool() == false); });
|
||||
```
|
||||
|
||||
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
|
||||
@ -1,12 +1,12 @@
|
||||
:title
|
||||
dtree_group_by
|
||||
dv_group_by
|
||||
|
||||
:sig
|
||||
DTree dtree_group_by(DTree tree, function<String (DTree, String)> f)
|
||||
DValue dv_group_by(DValue tree, function<String (DValue, String)> f)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
filter
|
||||
|
||||
:content
|
||||
@ -15,7 +15,7 @@ Groups children into list-like buckets by the string returned from f.
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
DTree by_section = dtree_group_by(menu, [](DTree item, String key) { return(item["section"].to_string()); });
|
||||
DValue by_section = dv_group_by(menu, [](DValue item, String key) { return(item["section"].to_string()); });
|
||||
```
|
||||
|
||||
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
|
||||
@ -1,21 +1,21 @@
|
||||
:title
|
||||
dtree_pick
|
||||
dv_keys
|
||||
|
||||
:sig
|
||||
DTree dtree_pick(DTree tree, StringList keys)
|
||||
StringList dv_keys(DValue tree)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
filter
|
||||
|
||||
:content
|
||||
Copies only selected keys from a DTree map.
|
||||
Returns map keys from a DValue. Scalar values produce an empty list.
|
||||
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
DTree public_user = dtree_pick(user, {"name", "avatar"});
|
||||
StringList keys = dv_keys(context.cfg["menu"]);
|
||||
```
|
||||
|
||||
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
|
||||
@ -1,12 +1,12 @@
|
||||
:title
|
||||
dtree_map
|
||||
dv_map
|
||||
|
||||
:sig
|
||||
DTree dtree_map(DTree tree, function<DTree (DTree, String)> f)
|
||||
DValue dv_map(DValue tree, function<DValue (DValue, String)> f)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
filter
|
||||
|
||||
:content
|
||||
@ -15,7 +15,7 @@ Transforms each child. List-like input stays list-like; map input keeps keys.
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
DTree titles = dtree_map(items, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); });
|
||||
DValue titles = dv_map(items, [](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); });
|
||||
```
|
||||
|
||||
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
|
||||
@ -1,21 +1,21 @@
|
||||
:title
|
||||
dtree_omit
|
||||
dv_omit
|
||||
|
||||
:sig
|
||||
DTree dtree_omit(DTree tree, StringList keys)
|
||||
DValue dv_omit(DValue tree, StringList keys)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
filter
|
||||
|
||||
:content
|
||||
Copies a DTree map except for selected keys.
|
||||
Copies a DValue map except for selected keys.
|
||||
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
DTree safe_user = dtree_omit(user, {"password_hash"});
|
||||
DValue safe_user = dv_omit(user, {"password_hash"});
|
||||
```
|
||||
|
||||
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
|
||||
@ -1,21 +1,21 @@
|
||||
:title
|
||||
dtree_keys
|
||||
dv_pick
|
||||
|
||||
:sig
|
||||
StringList dtree_keys(DTree tree)
|
||||
DValue dv_pick(DValue tree, StringList keys)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
filter
|
||||
|
||||
:content
|
||||
Returns map keys from a DTree. Scalar values produce an empty list.
|
||||
Copies only selected keys from a DValue map.
|
||||
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
StringList keys = dtree_keys(context.cfg["menu"]);
|
||||
DValue public_user = dv_pick(user, {"name", "avatar"});
|
||||
```
|
||||
|
||||
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
|
||||
@ -1,21 +1,21 @@
|
||||
:title
|
||||
dtree_values
|
||||
dv_values
|
||||
|
||||
:sig
|
||||
DTree dtree_values(DTree tree)
|
||||
DValue dv_values(DValue tree)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
filter
|
||||
|
||||
:content
|
||||
Returns child values as a list-like DTree.
|
||||
Returns child values as a list-like DValue.
|
||||
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
DTree menu_items = dtree_values(context.cfg["menu"]);
|
||||
DValue menu_items = dv_values(context.cfg["menu"]);
|
||||
```
|
||||
|
||||
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
void DTree::each(function<void (const DTree& item, String key)> f) const
|
||||
void DValue::each(function<void (const DValue& item, String key)> f) const
|
||||
|
||||
:params
|
||||
f : callback invoked per child with the child node and its key
|
||||
@ -7,26 +7,26 @@ return value : none
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
dtree_map
|
||||
dtree_filter
|
||||
dtree_keys
|
||||
0_DValue
|
||||
dv_map
|
||||
dv_filter
|
||||
dv_keys
|
||||
is_list
|
||||
|
||||
:content
|
||||
Iterates a `DTree`. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
Iterates a `DValue`. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
For map-shaped nodes the callback runs once per child, in key order, receiving the child and its key. For scalar nodes it runs exactly once with the node itself and an empty key.
|
||||
|
||||
The callback receives the child as `const DTree&` — no copy is made, and all read accessors (`to_string()`, `to_s64()`, `get_by_path()`, ...) work on it directly:
|
||||
The callback receives the child as `const DValue&` — no copy is made, and all read accessors (`to_string()`, `to_s64()`, `get_by_path()`, ...) work on it directly:
|
||||
|
||||
```cpp
|
||||
rows.each([&](const DTree& row, String key) {
|
||||
rows.each([&](const DValue& row, String key) {
|
||||
out("<li>", html_escape(row.get_by_path("title").to_string("Untitled")), "</li>");
|
||||
});
|
||||
```
|
||||
|
||||
Declaring the callback parameter as plain `DTree` also works but deep-copies every child; prefer `const DTree&`. To transform or filter into a new tree, use `dtree_map()` / `dtree_filter()` instead of mutating inside the callback.
|
||||
Declaring the callback parameter as plain `DValue` also works but deep-copies every child; prefer `const DValue&`. To transform or filter into a new tree, use `dv_map()` / `dv_filter()` instead of mutating inside the callback.
|
||||
|
||||
List-shaped trees (see `is_list`) iterate in numeric index order, matching `json_encode()` and the other serializers. Keyed maps iterate in string key order.
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ page_runtime_error : UCE page rendered (status 500) after a recovered fault or u
|
||||
:see
|
||||
>runtime
|
||||
0_Request
|
||||
0_DTree
|
||||
0_DValue
|
||||
set_status
|
||||
|
||||
:content
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
:sig
|
||||
DTree DTree::get_by_path(String path, String delim = "/") const
|
||||
DValue DValue::get_by_path(String path, String delim = "/") const
|
||||
|
||||
:params
|
||||
path : slash-delimited path to traverse
|
||||
delim : optional path separator
|
||||
return value : the resolved child node, or an empty `DTree` when the path cannot be followed
|
||||
return value : the resolved child node, or an empty `DValue` when the path cannot be followed
|
||||
|
||||
:see
|
||||
0_DTree
|
||||
0_DValue
|
||||
0_Request
|
||||
>types
|
||||
json_decode
|
||||
@ -15,9 +15,9 @@ has
|
||||
to_string
|
||||
|
||||
:content
|
||||
Traverses a nested `DTree` without creating missing keys. This is a read accessor: it is `const` and never modifies the tree, unlike `operator[]`, which creates missing entries.
|
||||
Traverses a nested `DValue` without creating missing keys. This is a read accessor: it is `const` and never modifies the tree, unlike `operator[]`, which creates missing entries.
|
||||
|
||||
Empty path segments are ignored, so leading and trailing `/` characters are harmless. If any intermediate node is not a map or a segment is missing, `get_by_path()` returns an empty `DTree`.
|
||||
Empty path segments are ignored, so leading and trailing `/` characters are harmless. If any intermediate node is not a map or a segment is missing, `get_by_path()` returns an empty `DValue`.
|
||||
|
||||
A missing path therefore reads like an empty value — combine it with the `to_*` default arguments to express a fallback in one call:
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
bool DTree::has(String s) const
|
||||
bool DValue::has(String s) const
|
||||
|
||||
:params
|
||||
s : child key to test
|
||||
@ -7,13 +7,13 @@ return value : true when the node is map-shaped and contains the key
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
0_DValue
|
||||
get_by_path
|
||||
each
|
||||
is_array
|
||||
|
||||
:content
|
||||
Tests whether a map-shaped `DTree` contains a child key, without creating it. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
Tests whether a map-shaped `DValue` contains a child key, without creating it. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
Returns `false` for scalar nodes and for missing keys.
|
||||
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
:sig
|
||||
bool DTree::is_array() const
|
||||
bool DValue::is_array() const
|
||||
|
||||
:params
|
||||
return value : true when the node is map-shaped
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
0_DValue
|
||||
is_list
|
||||
has
|
||||
each
|
||||
|
||||
:content
|
||||
Tests whether a `DTree` node is map-shaped, i.e. holds nested child values rather than a scalar. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
Tests whether a `DValue` node is map-shaped, i.e. holds nested child values rather than a scalar. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
`is_array()` is true for both list-like and keyed containers; use `is_list()` to distinguish the two:
|
||||
|
||||
```cpp
|
||||
DTree rows = sqlite_query(db, "select * from notes");
|
||||
DValue rows = sqlite_query(db, "select * from notes");
|
||||
if(rows.is_array())
|
||||
{
|
||||
rows.each([&](const DTree& row, String key) {
|
||||
rows.each([&](const DValue& row, String key) {
|
||||
// ...
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
:sig
|
||||
bool DTree::is_list() const
|
||||
bool DValue::is_list() const
|
||||
|
||||
:params
|
||||
return value : true when the node is a sequential, numerically indexed container
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
0_DValue
|
||||
is_array
|
||||
push
|
||||
each
|
||||
dtree_values
|
||||
dv_values
|
||||
|
||||
:content
|
||||
Tests whether a map-shaped `DTree` represents a list: its keys are the numeric strings `"0"`, `"1"`, `"2"`, ... in unbroken sequence. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
Tests whether a map-shaped `DValue` represents a list: its keys are the numeric strings `"0"`, `"1"`, `"2"`, ... in unbroken sequence. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
Containers built with `push()` are lists. An empty container counts as a list when it was created with `set_array()` or `push()`; a keyed map (or a map with gaps in its numeric keys) is `is_array()` but not `is_list()`.
|
||||
|
||||
```cpp
|
||||
DTree items;
|
||||
DValue items;
|
||||
items.push(first_item);
|
||||
items.push(second_item);
|
||||
// items.is_list() == true
|
||||
@ -27,7 +27,7 @@ items["custom"] = "x";
|
||||
// items.is_list() == false, items.is_array() == true
|
||||
```
|
||||
|
||||
`dtree_map()` and `dtree_filter()` use this distinction to decide whether results re-index from zero or keep their original keys.
|
||||
`dv_map()` and `dv_filter()` use this distinction to decide whether results re-index from zero or keep their original keys.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
:sig
|
||||
DTree json_decode(String s)
|
||||
DValue json_decode(String s)
|
||||
|
||||
:params
|
||||
s : string containing JSON data
|
||||
return value : a DTree object containing the deserialized JSON data
|
||||
return value : a DValue object containing the deserialized JSON data
|
||||
|
||||
:see
|
||||
0_DTree
|
||||
0_DValue
|
||||
json_encode
|
||||
to_bool
|
||||
to_f64
|
||||
to_u64
|
||||
|
||||
:content
|
||||
Deserializes `s` into a `DTree` structure.
|
||||
Deserializes `s` into a `DValue` structure.
|
||||
|
||||
The returned structure is usually consumed through `DTree` accessors such as:
|
||||
The returned structure is usually consumed through `DValue` accessors such as:
|
||||
|
||||
- `tree["key"].to_string()`
|
||||
- `tree["count"].to_u64()`
|
||||
@ -23,10 +23,10 @@ The returned structure is usually consumed through `DTree` accessors such as:
|
||||
|
||||
Current runtime behavior:
|
||||
|
||||
- JSON objects and arrays become map-shaped `DTree` values.
|
||||
- JSON booleans become native `bool` `DTree` values.
|
||||
- JSON strings become native `String` `DTree` values.
|
||||
- JSON numbers currently deserialize as string-valued `DTree` nodes, so typed conversions such as `to_f64()` and `to_u64()` are the normal way to read numeric content.
|
||||
- JSON objects and arrays become map-shaped `DValue` values.
|
||||
- JSON booleans become native `bool` `DValue` values.
|
||||
- JSON strings become native `String` `DValue` values.
|
||||
- JSON numbers currently deserialize as string-valued `DValue` nodes, so typed conversions such as `to_f64()` and `to_u64()` are the normal way to read numeric content.
|
||||
|
||||
Related:
|
||||
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
:sig
|
||||
String json_encode(String s)
|
||||
String json_encode(DTree t)
|
||||
String json_encode(DValue t)
|
||||
|
||||
:params
|
||||
s : string to encode as a JSON string literal
|
||||
t : DTree object to be serialized
|
||||
t : DValue object to be serialized
|
||||
return value : string containing the JSON result
|
||||
|
||||
:see
|
||||
>types
|
||||
json_decode
|
||||
0_DTree
|
||||
0_DValue
|
||||
String
|
||||
html_escape
|
||||
|
||||
:content
|
||||
Serializes either a `String` or a `DTree` into JSON notation.
|
||||
Serializes either a `String` or a `DValue` into JSON notation.
|
||||
|
||||
When passed a `String`, `json_encode()` returns a quoted and escaped JSON string literal.
|
||||
|
||||
When passed a `DTree`, scalar values are serialized directly and nested map values are emitted as JSON objects.
|
||||
When passed a `DValue`, scalar values are serialized directly and nested map values are emitted as JSON objects.
|
||||
|
||||
Related:
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ bool list_every(StringList items, function<bool (String)> f)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
filter
|
||||
|
||||
:content
|
||||
|
||||
@ -6,7 +6,7 @@ String list_find(StringList items, function<bool (String)> f, String fallback =
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
filter
|
||||
|
||||
:content
|
||||
|
||||
@ -6,7 +6,7 @@ bool list_some(StringList items, function<bool (String)> f)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
filter
|
||||
|
||||
:content
|
||||
|
||||
@ -6,7 +6,7 @@ StringList list_sort(StringList items)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
filter
|
||||
|
||||
:content
|
||||
|
||||
@ -6,7 +6,7 @@ StringList list_unique(StringList items)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
filter
|
||||
|
||||
:content
|
||||
|
||||
@ -14,7 +14,7 @@ return value : a new list containing the transformed values
|
||||
>string
|
||||
filter
|
||||
StringList
|
||||
0_DTree
|
||||
0_DValue
|
||||
|
||||
:content
|
||||
Returns a new list by calling `f` for each item in `items`.
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
:sig
|
||||
DTree markdown_to_ast(String src)
|
||||
DTree markdown_to_ast(String src, DTree options)
|
||||
DValue markdown_to_ast(String src)
|
||||
DValue markdown_to_ast(String src, DValue options)
|
||||
|
||||
:params
|
||||
src : markdown source text
|
||||
options : optional markdown options tree
|
||||
return value : a `DTree` document AST
|
||||
return value : a `DValue` document AST
|
||||
|
||||
:see
|
||||
markdown_to_html
|
||||
component
|
||||
component_render
|
||||
json_encode
|
||||
0_DTree
|
||||
0_DValue
|
||||
|
||||
:content
|
||||
Parses Markdown source into a structured `DTree` document tree.
|
||||
Parses Markdown source into a structured `DValue` document tree.
|
||||
|
||||
The parser targets a practical GitHub-flavored subset by default:
|
||||
|
||||
@ -67,8 +67,8 @@ Common inline nodes:
|
||||
Example:
|
||||
|
||||
```uce
|
||||
DTree options = json_decode("{\"components\":{\":::warning\":\"components/markdown/warning\"}}");
|
||||
DTree ast = markdown_to_ast(file_get_contents("README.md"), options);
|
||||
DValue options = json_decode("{\"components\":{\":::warning\":\"components/markdown/warning\"}}");
|
||||
DValue ast = markdown_to_ast(file_get_contents("README.md"), options);
|
||||
print(json_encode(ast));
|
||||
```
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
:sig
|
||||
String markdown_to_html(String src)
|
||||
String markdown_to_html(String src, DTree options)
|
||||
String markdown_to_html(String src, DValue options)
|
||||
|
||||
:params
|
||||
src : markdown source text
|
||||
@ -26,7 +26,7 @@ By default the function aims at a practical GitHub-flavored Markdown target, inc
|
||||
Example:
|
||||
|
||||
```uce
|
||||
DTree options;
|
||||
DValue options;
|
||||
options["components"][":::warning"] = "components/markdown/warning";
|
||||
options["components"]["node.code_block"] = "components/markdown/code_block";
|
||||
String html = markdown_to_html(file_get_contents("guide.md"), options);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
DTree mysql_query(MySQL* m, String q, StringMap params)
|
||||
DValue mysql_query(MySQL* m, String q, StringMap params)
|
||||
|
||||
:params
|
||||
m : pointer to an active MySQL connection struct
|
||||
@ -18,13 +18,13 @@ Executes a MySQL query and returns the resulting data, if any.
|
||||
```cpp
|
||||
StringMap params;
|
||||
params["email"] = "ada@example.test";
|
||||
DTree rows = mysql_query(m,
|
||||
DValue rows = mysql_query(m,
|
||||
"select id, email from users where email = :email",
|
||||
params
|
||||
);
|
||||
```
|
||||
|
||||
The result is returned as a `DTree`, which makes it easy to iterate through rows and read fields with the usual `DTree` accessors.
|
||||
The result is returned as a `DValue`, which makes it easy to iterate through rows and read fields with the usual `DValue` accessors.
|
||||
|
||||
Related:
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
:sig
|
||||
DTree regex_search(String pattern, String subject)
|
||||
DTree regex_search(String pattern, String subject, String flags)
|
||||
DValue regex_search(String pattern, String subject)
|
||||
DValue regex_search(String pattern, String subject, String flags)
|
||||
|
||||
:params
|
||||
pattern : PCRE2 regular expression pattern
|
||||
subject : string to search
|
||||
flags : optional regex flags
|
||||
return value : a DTree describing the first match
|
||||
return value : a DValue describing the first match
|
||||
|
||||
:see
|
||||
>regex
|
||||
@ -14,7 +14,7 @@ regex_match
|
||||
regex_search_all
|
||||
regex_replace
|
||||
regex_split
|
||||
0_DTree
|
||||
0_DValue
|
||||
|
||||
:content
|
||||
Searches `subject` for the first occurrence of `pattern` and returns structured match data.
|
||||
@ -22,7 +22,7 @@ Searches `subject` for the first occurrence of `pattern` and returns structured
|
||||
Example:
|
||||
|
||||
```uce
|
||||
DTree match = regex_search(
|
||||
DValue match = regex_search(
|
||||
"(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)",
|
||||
"Contact ops@example.test"
|
||||
);
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
:sig
|
||||
DTree regex_search_all(String pattern, String subject)
|
||||
DTree regex_search_all(String pattern, String subject, String flags)
|
||||
DValue regex_search_all(String pattern, String subject)
|
||||
DValue regex_search_all(String pattern, String subject, String flags)
|
||||
|
||||
:params
|
||||
pattern : PCRE2 regular expression pattern
|
||||
subject : string to search
|
||||
flags : optional regex flags
|
||||
return value : a DTree containing all non-overlapping matches
|
||||
return value : a DValue containing all non-overlapping matches
|
||||
|
||||
:see
|
||||
>regex
|
||||
@ -14,7 +14,7 @@ regex_match
|
||||
regex_search
|
||||
regex_replace
|
||||
regex_split
|
||||
0_DTree
|
||||
0_DValue
|
||||
|
||||
:content
|
||||
Finds every non-overlapping match of `pattern` in `subject`.
|
||||
@ -22,9 +22,9 @@ Finds every non-overlapping match of `pattern` in `subject`.
|
||||
Example:
|
||||
|
||||
```uce
|
||||
DTree tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", "Ship #uce and #docs");
|
||||
DValue tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", "Ship #uce and #docs");
|
||||
|
||||
tags["matches"].each([](DTree match, String key) {
|
||||
tags["matches"].each([](DValue match, String key) {
|
||||
print(match["named"]["tag"].to_string(), "\n");
|
||||
});
|
||||
```
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
request_query_route
|
||||
|
||||
:sig
|
||||
DTree request_query_route(Request& context, String default_path = "index")
|
||||
DValue request_query_route(Request& context, String default_path = "index")
|
||||
|
||||
:see
|
||||
request_query_path
|
||||
@ -21,7 +21,7 @@ Fields:
|
||||
- `valid`: boolean; true when `l_path` is safe
|
||||
|
||||
```cpp
|
||||
DTree route = request_query_route(context);
|
||||
DValue route = request_query_route(context);
|
||||
if(route["valid"].to_bool())
|
||||
print("route=", route["l_path"].to_string());
|
||||
```
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
:sig
|
||||
DTree sqlite_query(SQLite* db, String q)
|
||||
DTree sqlite_query(SQLite* db, String q, StringMap params)
|
||||
DValue sqlite_query(SQLite* db, String q)
|
||||
DValue sqlite_query(SQLite* db, String q, StringMap params)
|
||||
|
||||
:params
|
||||
db : pointer to an active SQLite connection
|
||||
q : SQL statement
|
||||
params : optional named parameter map
|
||||
return value : list of result rows as a DTree
|
||||
return value : list of result rows as a DValue
|
||||
|
||||
:see
|
||||
>sqlite
|
||||
@ -14,10 +14,10 @@ sqlite_connect
|
||||
sqlite_error
|
||||
sqlite_insert_id
|
||||
sqlite_affected_rows
|
||||
0_DTree
|
||||
0_DValue
|
||||
|
||||
:content
|
||||
Executes one SQLite statement and returns result rows as a `DTree` array. Multi-statement SQL strings are rejected so migrations cannot silently run only their first statement.
|
||||
Executes one SQLite statement and returns result rows as a `DValue` array. Multi-statement SQL strings are rejected so migrations cannot silently run only their first statement.
|
||||
|
||||
Use named parameters with `:name` placeholders only. Positional `?` placeholders and SQLite's other named marker forms (`@name`, `$name`) are rejected so UCE SQLite queries use the same placeholder style as the MySQL helper. UCE binds parameters with SQLite prepared statements; it does not substitute values into the SQL string.
|
||||
|
||||
@ -26,12 +26,12 @@ SQLite* db = sqlite_connect("/tmp/app.sqlite");
|
||||
|
||||
StringMap params;
|
||||
params["email"] = "ada@example.test";
|
||||
DTree rows = sqlite_query(db,
|
||||
DValue rows = sqlite_query(db,
|
||||
"select id, email from users where email = :email",
|
||||
params
|
||||
);
|
||||
```
|
||||
|
||||
Result rows are objects keyed by column name. SQLite integer, float, text, blob, and null values are converted to DTree values. Blob values are returned as byte strings.
|
||||
Result rows are objects keyed by column name. SQLite integer, float, text, blob, and null values are converted to DValue values. Blob values are returned as byte strings.
|
||||
|
||||
For statements that do not return rows, inspect `sqlite_affected_rows()` or `sqlite_insert_id()` after the call.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
bool DTree::to_bool(bool default_value = false) const
|
||||
bool DValue::to_bool(bool default_value = false) const
|
||||
|
||||
:params
|
||||
default_value : returned when the value is missing or empty
|
||||
@ -7,14 +7,14 @@ return value : the value as a boolean, or `default_value`
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
0_DValue
|
||||
json_decode
|
||||
to_f64
|
||||
to_u64
|
||||
to_string
|
||||
|
||||
:content
|
||||
Reads a `DTree` value as a boolean. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
Reads a `DValue` value as a boolean. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
String values such as `true`, `yes`, `on`, and `1` read as true. Values such as `false`, `no`, `off`, `0`, and `null` read as false. Numeric values read as true when non-zero.
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
f64 DTree::to_f64(f64 default_value = 0) const
|
||||
f64 DValue::to_f64(f64 default_value = 0) const
|
||||
|
||||
:params
|
||||
default_value : returned when the value is missing or cannot be parsed as a number
|
||||
@ -7,7 +7,7 @@ return value : the value as a floating-point number, or `default_value`
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
0_DValue
|
||||
json_decode
|
||||
float_val
|
||||
to_bool
|
||||
@ -15,7 +15,7 @@ to_s64
|
||||
to_u64
|
||||
|
||||
:content
|
||||
Reads a `DTree` value as a floating-point number. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
Reads a `DValue` value as a floating-point number. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
String values are trimmed and parsed permissively: numeric forms and the boolean words understood by `to_bool()` both convert. Boolean values become `1.0` or `0.0`.
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
String DTree::to_json(char quote_char = '"') const
|
||||
String DValue::to_json(char quote_char = '"') const
|
||||
|
||||
:params
|
||||
quote_char : quote character used around string output
|
||||
@ -7,13 +7,13 @@ return value : the scalar as a single JSON token
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
0_DValue
|
||||
json_encode
|
||||
json_decode
|
||||
to_string
|
||||
|
||||
:content
|
||||
Renders a single scalar `DTree` value as a JSON token. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
Renders a single scalar `DValue` value as a JSON token. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
Strings are escaped and quoted, numbers render as numeric literals, and booleans render as `true` / `false`.
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
s64 DTree::to_s64(s64 default_value = 0) const
|
||||
s64 DValue::to_s64(s64 default_value = 0) const
|
||||
|
||||
:params
|
||||
default_value : returned when the value is missing or cannot be parsed as a number
|
||||
@ -7,14 +7,14 @@ return value : the value as a signed 64-bit integer, or `default_value`
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
0_DValue
|
||||
to_u64
|
||||
to_f64
|
||||
to_bool
|
||||
to_string
|
||||
|
||||
:content
|
||||
Reads a `DTree` value as a signed integer. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
Reads a `DValue` value as a signed integer. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
String values are trimmed and parsed permissively: plain integers, floating-point forms (truncated toward zero), and the boolean words understood by `to_bool()` (`yes` reads as `1`) all convert. Results outside the `s64` range clamp to the range boundaries. Boolean values become `1` or `0`.
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
String DTree::to_string(String default_value = "") const
|
||||
String DValue::to_string(String default_value = "") const
|
||||
|
||||
:params
|
||||
default_value : returned when the node holds no usable text
|
||||
@ -7,7 +7,7 @@ return value : the scalar content as text, or `default_value`
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
0_DValue
|
||||
to_s64
|
||||
to_f64
|
||||
to_bool
|
||||
@ -15,7 +15,7 @@ to_json
|
||||
get_by_path
|
||||
|
||||
:content
|
||||
Reads a `DTree` value as text. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
Reads a `DValue` value as text. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
`default_value` is returned when the value is missing or not text-convertible:
|
||||
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
:sig
|
||||
StringMap DTree::to_stringmap() const
|
||||
StringMap DValue::to_stringmap() const
|
||||
|
||||
:params
|
||||
return value : a flat `StringMap` projection of the node
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
0_DValue
|
||||
StringMap
|
||||
to_string
|
||||
dtree_keys
|
||||
dtree_values
|
||||
dv_keys
|
||||
dv_values
|
||||
|
||||
:content
|
||||
Converts a `DTree` into a flat `StringMap`. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
Converts a `DValue` into a flat `StringMap`. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
- Map-shaped nodes produce one entry per child, with each child read via `to_string()`. Nested maps flatten to empty strings — this is a one-level projection, not a serializer.
|
||||
- A non-empty scalar produces a single `"value"` entry holding the scalar.
|
||||
@ -25,7 +25,7 @@ Use this when handing request- or config-shaped data to APIs that take `StringMa
|
||||
|
||||
```cpp
|
||||
StringMap params = context.props["filters"].to_stringmap();
|
||||
DTree rows = sqlite_query(db, "select * from notes where author = :author", params);
|
||||
DValue rows = sqlite_query(db, "select * from notes where author = :author", params);
|
||||
```
|
||||
|
||||
For a faithful representation of nested data, use `json_encode()` instead.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
u64 DTree::to_u64(u64 default_value = 0) const
|
||||
u64 DValue::to_u64(u64 default_value = 0) const
|
||||
|
||||
:params
|
||||
default_value : returned when the value is missing or cannot be parsed as a number
|
||||
@ -7,7 +7,7 @@ return value : the value as an unsigned 64-bit integer, or `default_value`
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
0_DValue
|
||||
json_decode
|
||||
int_val
|
||||
to_bool
|
||||
@ -15,7 +15,7 @@ to_s64
|
||||
to_f64
|
||||
|
||||
:content
|
||||
Reads a `DTree` value as an unsigned integer. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
Reads a `DValue` value as an unsigned integer. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
String values are trimmed and parsed permissively, like `to_s64()`. Boolean values become `1` or `0`. Negative values clamp to `0`, and results above the `u64` range clamp to the maximum.
|
||||
|
||||
@ -34,7 +34,7 @@ u64 limit = context.get["limit"].to_u64(50);
|
||||
u64 owner_id = row.get_by_path("owner/id").to_u64();
|
||||
```
|
||||
|
||||
Use this for counts, identifiers, limits, timestamps, and other non-negative numeric values stored inside a `DTree`.
|
||||
Use this for counts, identifiers, limits, timestamps, and other non-negative numeric values stored inside a `DValue`.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
:sig
|
||||
DTree* unit_call(String file_name, String function_name, DTree* call_param = null)
|
||||
DValue* unit_call(String file_name, String function_name, DValue* call_param = null)
|
||||
|
||||
:params
|
||||
file_name : UCE file to load and execute
|
||||
function_name : name of the function to invoke
|
||||
call_param : optional, call parameter
|
||||
return value : DTree* returned from function
|
||||
return value : DValue* returned from function
|
||||
|
||||
:see
|
||||
>ob
|
||||
@ -20,7 +20,7 @@ Calls an exported function inside another UCE file.
|
||||
|
||||
Use `unit_call()` when you need structured data exchange between units rather than rendered HTML output.
|
||||
|
||||
The callee must expose an `EXPORT` function whose name matches `function_name`. Arguments are passed through `call_param`, and the return value is a `DTree*` owned by the callee.
|
||||
The callee must expose an `EXPORT` function whose name matches `function_name`. Arguments are passed through `call_param`, and the return value is a `DValue*` owned by the callee.
|
||||
|
||||
`unit_call()` also understands the request-bound UCE entrypoint names:
|
||||
|
||||
@ -31,7 +31,7 @@ The callee must expose an `EXPORT` function whose name matches `function_name`.
|
||||
- `ONCE`
|
||||
- `INIT`
|
||||
|
||||
When `function_name` matches one of those macro-style entrypoints, `unit_call()` does not look for a plain `EXPORT DTree* ...` function. Instead, it translates the name to the generated C++ symbol, uses the current `Request` context, and passes `call_param` into `context.props`, matching the normal component invocation model.
|
||||
When `function_name` matches one of those macro-style entrypoints, `unit_call()` does not look for a plain `EXPORT DValue* ...` function. Instead, it translates the name to the generated C++ symbol, uses the current `Request` context, and passes `call_param` into `context.props`, matching the normal component invocation model.
|
||||
|
||||
For `RENDER...` and `COMPONENT...`, the unit's `ONCE(Request& context)` hook is still honored automatically before the selected handler runs.
|
||||
|
||||
@ -39,7 +39,7 @@ Example:
|
||||
|
||||
```cpp
|
||||
// export a function
|
||||
EXPORT DTree* test_func(DTree* call_param)
|
||||
EXPORT DValue* test_func(DValue* call_param)
|
||||
{
|
||||
print("HELLO FROM TEST FUNCTION");
|
||||
return(0);
|
||||
@ -52,7 +52,7 @@ unit_call("call_file_funcs.uce", "test_func");
|
||||
Calling a named component handler through `unit_call()`:
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
DValue props;
|
||||
props["title"] = "Diagnostics";
|
||||
props["body"] = "Ready";
|
||||
|
||||
@ -62,7 +62,7 @@ unit_call("components/card.uce", "COMPONENT:BODY", &props);
|
||||
Calling a page render handler through `unit_call()`:
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
DValue props;
|
||||
props["section"] = "summary";
|
||||
|
||||
unit_call("reports/summary.uce", "RENDER", &props);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
DTree unit_info(String path = "")
|
||||
DValue unit_info(String path = "")
|
||||
|
||||
:params
|
||||
path : optional UCE unit path. If empty, uses the current executing unit.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
:sig
|
||||
String var_dump(StringMap t, String prefix = "", String postfix = "\n")
|
||||
String var_dump(StringList t, String prefix = "", String postfix = "\n")
|
||||
String var_dump(DTree t, String prefix = "", String postfix = "\n")
|
||||
String var_dump(DValue t, String prefix = "", String postfix = "\n")
|
||||
|
||||
:params
|
||||
t : object to be dumped into a string
|
||||
@ -9,7 +9,7 @@ return value : string containing a human-friendly representation of 't'
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
0_DValue
|
||||
StringMap
|
||||
json_encode
|
||||
print
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
:sig
|
||||
DTree xml_decode(String s)
|
||||
DValue xml_decode(String s)
|
||||
|
||||
:params
|
||||
s : XML source string
|
||||
return value : element-shaped DTree
|
||||
return value : element-shaped DValue
|
||||
|
||||
:see
|
||||
>markup
|
||||
xml_encode
|
||||
json_decode
|
||||
0_DTree
|
||||
0_DValue
|
||||
String
|
||||
|
||||
:content
|
||||
Parses a simple XML document into a structured `DTree`.
|
||||
Parses a simple XML document into a structured `DValue`.
|
||||
|
||||
`xml_decode()` is intentionally small. It does not validate schemas, DTDs, namespaces, or document types. It parses the first root element and returns the same structural element shape accepted by `xml_encode()`.
|
||||
|
||||
@ -31,7 +31,7 @@ node["children"] = list of child element nodes
|
||||
Example:
|
||||
|
||||
```uce
|
||||
DTree book = xml_decode("<book id=\"b1\"><title>UCE & XML</title></book>");
|
||||
DValue book = xml_decode("<book id=\"b1\"><title>UCE & XML</title></book>");
|
||||
|
||||
book["name"].to_string(); // book
|
||||
book["attrs"]["id"].to_string(); // b1
|
||||
@ -42,7 +42,7 @@ book["children"]["0"]["text"].to_string(); // UCE & XML
|
||||
CDATA and numeric entities are folded into text:
|
||||
|
||||
```uce
|
||||
DTree note = xml_decode("<note><![CDATA[5 < 6]]><symbol>AB</symbol></note>");
|
||||
DValue note = xml_decode("<note><![CDATA[5 < 6]]><symbol>AB</symbol></note>");
|
||||
|
||||
note["text"].to_string(); // 5 < 6
|
||||
note["children"]["0"]["text"].to_string(); // AB
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
:sig
|
||||
String xml_encode(DTree t)
|
||||
String xml_encode(DTree t, String root_name)
|
||||
String xml_encode(DValue t)
|
||||
String xml_encode(DValue t, String root_name)
|
||||
|
||||
:params
|
||||
t : tree to serialize as XML
|
||||
@ -11,11 +11,11 @@ return value : XML string
|
||||
>markup
|
||||
xml_decode
|
||||
json_encode
|
||||
0_DTree
|
||||
0_DValue
|
||||
html_escape
|
||||
|
||||
:content
|
||||
Serializes a `DTree` into a simple XML string.
|
||||
Serializes a `DValue` into a simple XML string.
|
||||
|
||||
`xml_encode()` does not validate against a schema, DTD, or namespace rules. It is a structural converter for application data, similar in spirit to `json_encode()`.
|
||||
|
||||
@ -33,11 +33,11 @@ node["children"] = list of child element nodes
|
||||
Example:
|
||||
|
||||
```uce
|
||||
DTree book;
|
||||
DValue book;
|
||||
book["name"] = "book";
|
||||
book["attrs"]["id"] = "b1";
|
||||
|
||||
DTree title;
|
||||
DValue title;
|
||||
title["name"] = "title";
|
||||
title["text"] = "UCE & XML";
|
||||
book["children"].push(title);
|
||||
@ -54,7 +54,7 @@ The result is:
|
||||
For simple map/list/scalar trees, `xml_encode()` creates ordinary child elements:
|
||||
|
||||
```uce
|
||||
DTree payload;
|
||||
DValue payload;
|
||||
payload["title"] = "Hello";
|
||||
payload["count"] = "3";
|
||||
|
||||
@ -73,4 +73,4 @@ Notes:
|
||||
- Text and attribute values are escaped.
|
||||
- List values use repeated `<item>` children.
|
||||
- Empty elements serialize as self-closing tags.
|
||||
- Map child order follows `DTree` map iteration order.
|
||||
- Map child order follows `DValue` map iteration order.
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
:sig
|
||||
DTree yaml_decode(String s)
|
||||
DValue yaml_decode(String s)
|
||||
|
||||
:params
|
||||
s : YAML source string
|
||||
return value : decoded DTree
|
||||
return value : decoded DValue
|
||||
|
||||
:see
|
||||
>markup
|
||||
yaml_encode
|
||||
json_decode
|
||||
xml_decode
|
||||
0_DTree
|
||||
0_DValue
|
||||
|
||||
:content
|
||||
Parses a practical YAML subset into a `DTree`.
|
||||
Parses a practical YAML subset into a `DValue`.
|
||||
|
||||
`yaml_decode()` is designed for concise UCE config files. It intentionally avoids full YAML schema behavior and does not support anchors, aliases, tags, directives, or complex inline collection syntax.
|
||||
|
||||
@ -30,7 +30,7 @@ String source = "app:\n"
|
||||
" - site\n"
|
||||
" - cache\n";
|
||||
|
||||
DTree cfg = yaml_decode(source);
|
||||
DValue cfg = yaml_decode(source);
|
||||
|
||||
cfg["app"]["name"].to_string(); // UCE Starter
|
||||
cfg["app"]["debug"].to_bool(); // true
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
String yaml_encode(DTree t)
|
||||
String yaml_encode(DValue t)
|
||||
|
||||
:params
|
||||
t : tree to serialize as YAML
|
||||
@ -10,10 +10,10 @@ return value : YAML string
|
||||
yaml_decode
|
||||
json_encode
|
||||
xml_encode
|
||||
0_DTree
|
||||
0_DValue
|
||||
|
||||
:content
|
||||
Serializes a `DTree` into a compact YAML string for configuration files.
|
||||
Serializes a `DValue` into a compact YAML string for configuration files.
|
||||
|
||||
`yaml_encode()` focuses on the ordinary data shapes UCE apps use for config: nested maps, lists, strings, booleans, and numeric `f64` values. It does not emit YAML tags, anchors, aliases, or custom schema features.
|
||||
|
||||
@ -22,12 +22,12 @@ Try the live example in the [YAML demo](../demo/yaml.uce).
|
||||
Example:
|
||||
|
||||
```uce
|
||||
DTree cfg;
|
||||
DValue cfg;
|
||||
cfg["app"]["name"] = "UCE Starter";
|
||||
cfg["app"]["debug"].set_bool(true);
|
||||
cfg["app"]["port"] = (f64)8080;
|
||||
|
||||
DTree path;
|
||||
DValue path;
|
||||
path = "site";
|
||||
cfg["app"]["paths"].push(path);
|
||||
path = "cache";
|
||||
@ -50,8 +50,8 @@ app:
|
||||
|
||||
Notes:
|
||||
|
||||
- Map keys are emitted in `DTree` map iteration order.
|
||||
- Map keys are emitted in `DValue` map iteration order.
|
||||
- Strings are left plain when safe and quoted when needed.
|
||||
- Multiline strings are emitted as literal block scalars with `|`.
|
||||
- Empty strings are emitted as `""` so they are not confused with YAML null.
|
||||
- Decoded numeric-looking config values are strings unless the original `DTree` value was explicitly numeric.
|
||||
- Decoded numeric-looking config values are strings unless the original `DValue` value was explicitly numeric.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
bool zip_create(String zip_file_name, DTree entries)
|
||||
bool zip_create(String zip_file_name, DValue entries)
|
||||
|
||||
:params
|
||||
zip_file_name : path to the archive to create
|
||||
@ -11,7 +11,7 @@ return value : `true` when the archive is written
|
||||
zip_list
|
||||
zip_read
|
||||
zip_extract
|
||||
DTree
|
||||
DValue
|
||||
|
||||
:content
|
||||
Creates a ZIP archive at `zip_file_name`.
|
||||
@ -19,7 +19,7 @@ Creates a ZIP archive at `zip_file_name`.
|
||||
`entries` can be a simple map where each key is the archive member name and each value is the file content:
|
||||
|
||||
```uce
|
||||
DTree entries;
|
||||
DValue entries;
|
||||
entries["hello.txt"] = "Hello ZIP";
|
||||
entries["nested/readme.txt"] = "Nested file";
|
||||
zip_create("/tmp/example.zip", entries);
|
||||
@ -28,7 +28,7 @@ zip_create("/tmp/example.zip", entries);
|
||||
For list-shaped input or when the map key should not be the member name, each child can provide explicit fields:
|
||||
|
||||
```uce
|
||||
DTree item;
|
||||
DValue item;
|
||||
item["name"] = "data/value.txt";
|
||||
item["content"] = "42";
|
||||
entries["ignored-key"] = item;
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
:sig
|
||||
DTree zip_list(String zip_file_name)
|
||||
DValue zip_list(String zip_file_name)
|
||||
|
||||
:params
|
||||
zip_file_name : path to the ZIP archive to inspect
|
||||
return value : DTree containing archive metadata and an `entries` array
|
||||
return value : DValue containing archive metadata and an `entries` array
|
||||
|
||||
:see
|
||||
>sys
|
||||
zip_create
|
||||
zip_read
|
||||
zip_extract
|
||||
DTree
|
||||
DValue
|
||||
|
||||
:content
|
||||
Reads the central directory from `zip_file_name` and returns structured metadata.
|
||||
@ -33,6 +33,6 @@ Each entry contains:
|
||||
Example:
|
||||
|
||||
```uce
|
||||
DTree info = zip_list("/tmp/example.zip");
|
||||
DValue info = zip_list("/tmp/example.zip");
|
||||
print(json_encode(info));
|
||||
```
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree error = context.call["error"];
|
||||
DValue error = context.call["error"];
|
||||
|
||||
<>
|
||||
<!doctype html>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree error = context.call["error"];
|
||||
DValue error = context.call["error"];
|
||||
String signal_name = error["signal_name"].to_string();
|
||||
|
||||
<>
|
||||
|
||||
@ -6,7 +6,7 @@ COMPONENT(Request& context)
|
||||
String subtitle = first(context.props["subtitle"].to_string(), "Choose your preferred authentication method");
|
||||
String callback_url = first(context.props["callback_url"].to_string(), app_link("auth/callback", context));
|
||||
|
||||
DTree services = context.props["services"];
|
||||
DValue services = context.props["services"];
|
||||
if(services.get_type_name() != "array" || services["google"]["name"].to_string() == "")
|
||||
{
|
||||
services["google"]["name"] = "Google";
|
||||
@ -40,7 +40,7 @@ COMPONENT(Request& context)
|
||||
<h2><?= title ?></h2>
|
||||
<label><?= subtitle ?></label>
|
||||
</div>
|
||||
<? services.each([&](DTree service, String service_key) {
|
||||
<? services.each([&](DValue service, String service_key) {
|
||||
String client_id = context.props[service_key + "_client_id"].to_string();
|
||||
String handler_name = ascii_safe_name(service_key);
|
||||
String service_name = service["name"].to_string();
|
||||
|
||||
@ -34,9 +34,9 @@ String data_format_bytes(String raw, bool disk = false)
|
||||
return(String(buf) + " " + units[unit_index]);
|
||||
}
|
||||
|
||||
DTree data_format_value(DTree value, DTree row, DTree column)
|
||||
DValue data_format_value(DValue value, DValue row, DValue column)
|
||||
{
|
||||
DTree result;
|
||||
DValue result;
|
||||
String format = column["format"].to_string();
|
||||
String raw = value.to_string();
|
||||
String sort_value = raw;
|
||||
@ -98,7 +98,7 @@ COMPONENT:SUMMARY_METRICS(Request& context)
|
||||
</div>
|
||||
<? } ?>
|
||||
<div class="dashboard-stat-grid">
|
||||
<? context.props["items"].each([&](DTree item, String key) {
|
||||
<? context.props["items"].each([&](DValue item, String key) {
|
||||
String tone = first(item["tone"].to_string(), "info");
|
||||
String href = item["href"].to_string();
|
||||
String tag = href != "" ? "a" : "div";
|
||||
@ -162,7 +162,7 @@ COMPONENT:SORTABLE_TABLE(Request& context)
|
||||
String subtitle = context.props["subtitle"].to_string();
|
||||
String empty_label = first(context.props["empty_label"].to_string(), "No data available");
|
||||
|
||||
DTree init_options;
|
||||
DValue init_options;
|
||||
init_options["storageKey"] = first(context.props["storage_key"].to_string(), "starter.sort." + table_id);
|
||||
if(context.props["sort"]["column"].to_string() != "")
|
||||
{
|
||||
@ -182,7 +182,7 @@ COMPONENT:SORTABLE_TABLE(Request& context)
|
||||
<table id="<?= table_id ?>" class="u-sortable-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<? context.props["columns"].each([&](DTree column, String key) {
|
||||
<? context.props["columns"].each([&](DValue column, String key) {
|
||||
String align = first(column["align"].to_string(), "left");
|
||||
bool sortable = column["sortable"].to_string() == "" || column["sortable"].to_string() == "1";
|
||||
?><th scope="col" class="align-<?= align ?>"<?= sortable ? "" : " data-sortable=\"false\"" ?>><?= first(column["label"].to_string(), column["key"].to_string(), "Column") ?></th><?
|
||||
@ -193,13 +193,13 @@ COMPONENT:SORTABLE_TABLE(Request& context)
|
||||
<? if(context.props["rows"].get_type_name() != "array" || context.props["rows"]["0"].get_type_name() != "array") { ?>
|
||||
<tr data-empty-row="1"><td colspan="<?= context.props["columns"]._map.size() == 0 ? "1" : std::to_string(context.props["columns"]._map.size()) ?>" class="muted"><?= empty_label ?></td></tr>
|
||||
<? } ?>
|
||||
<? context.props["rows"].each([&](DTree row, String key) {
|
||||
<? context.props["rows"].each([&](DValue row, String key) {
|
||||
if(row.get_type_name() != "array")
|
||||
return;
|
||||
?><tr><?
|
||||
context.props["columns"].each([&](DTree column, String ckey) {
|
||||
context.props["columns"].each([&](DValue column, String ckey) {
|
||||
String col_key = column["key"].to_string();
|
||||
DTree formatted = data_format_value(row[col_key], row, column);
|
||||
DValue formatted = data_format_value(row[col_key], row, column);
|
||||
?><td class="align-<?= first(column["align"].to_string(), "left") ?>" data-sort-value="<?= formatted["sort"].to_string() ?>"><?= formatted["display"].to_string() ?></td><?
|
||||
});
|
||||
?></tr><?
|
||||
@ -221,7 +221,7 @@ COMPONENT:DATA_TABLE(Request& context)
|
||||
{
|
||||
String table_id = first(context.props["id"].to_string(), "data-grid-" + std::to_string((u64)time()));
|
||||
|
||||
DTree columns = context.props["columns"];
|
||||
DValue columns = context.props["columns"];
|
||||
if(columns.get_type_name() != "array")
|
||||
columns.set_array();
|
||||
bool has_explicit_columns = columns.is_list() &&
|
||||
@ -230,8 +230,8 @@ COMPONENT:DATA_TABLE(Request& context)
|
||||
columns["0"]["field"].to_string() != "";
|
||||
if(!has_explicit_columns && context.props["items"]["0"].get_type_name() == "array")
|
||||
{
|
||||
context.props["items"]["0"].each([&](DTree value, String key) {
|
||||
DTree col;
|
||||
context.props["items"]["0"].each([&](DValue value, String key) {
|
||||
DValue col;
|
||||
col["field"] = key;
|
||||
col["headerName"] = key;
|
||||
col["sortable"].set_bool(true);
|
||||
@ -242,30 +242,30 @@ COMPONENT:DATA_TABLE(Request& context)
|
||||
}
|
||||
if(!columns.is_list())
|
||||
{
|
||||
DTree normalized_columns;
|
||||
DValue normalized_columns;
|
||||
normalized_columns.set_array();
|
||||
columns.each([&](DTree column, String key) {
|
||||
columns.each([&](DValue column, String key) {
|
||||
if(column.get_type_name() == "array")
|
||||
normalized_columns.push(column);
|
||||
});
|
||||
columns = normalized_columns;
|
||||
}
|
||||
|
||||
DTree row_data = context.props["items"];
|
||||
DValue row_data = context.props["items"];
|
||||
if(row_data.get_type_name() != "array")
|
||||
row_data.set_array();
|
||||
if(!row_data.is_list())
|
||||
{
|
||||
DTree normalized_rows;
|
||||
DValue normalized_rows;
|
||||
normalized_rows.set_array();
|
||||
row_data.each([&](DTree row, String key) {
|
||||
row_data.each([&](DValue row, String key) {
|
||||
if(row.get_type_name() == "array")
|
||||
normalized_rows.push(row);
|
||||
});
|
||||
row_data = normalized_rows;
|
||||
}
|
||||
|
||||
DTree grid_options;
|
||||
DValue grid_options;
|
||||
grid_options["pagination"].set_bool(true);
|
||||
grid_options["paginationPageSize"] = first(context.props["options"]["paginationPageSize"].to_string(), "20");
|
||||
grid_options["paginationPageSizeSelector"].set_bool(false);
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
#load "../../lib/app.uce"
|
||||
|
||||
void marketing_default_features(DTree& features)
|
||||
void marketing_default_features(DValue& features)
|
||||
{
|
||||
if(features.get_type_name() == "array" && features["0"]["title"].to_string() != "")
|
||||
return;
|
||||
DTree item;
|
||||
DValue item;
|
||||
|
||||
item["icon"] = "⚡";
|
||||
item["title"] = "Lightning Fast";
|
||||
@ -194,7 +194,7 @@ COMPONENT:HERO_SECTION(Request& context)
|
||||
|
||||
COMPONENT:FEATURES_GRID(Request& context)
|
||||
{
|
||||
DTree features = context.props["features"];
|
||||
DValue features = context.props["features"];
|
||||
marketing_default_features(features);
|
||||
|
||||
<>
|
||||
@ -204,7 +204,7 @@ COMPONENT:FEATURES_GRID(Request& context)
|
||||
<p>Discover the powerful features that make development a breeze</p>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
<? features.each([&](DTree feature, String key) {
|
||||
<? features.each([&](DValue feature, String key) {
|
||||
?><div class="feature-card">
|
||||
<div class="feature-icon"><?: feature["icon"].to_string() ?></div>
|
||||
<h3 class="feature-title"><?= feature["title"].to_string() ?></h3>
|
||||
@ -288,10 +288,10 @@ COMPONENT:FEATURES_GRID(Request& context)
|
||||
|
||||
COMPONENT:STATS_SECTION(Request& context)
|
||||
{
|
||||
DTree stats = context.props["stats"];
|
||||
DValue stats = context.props["stats"];
|
||||
if(stats.get_type_name() != "array" || stats["0"]["label"].to_string() == "")
|
||||
{
|
||||
DTree item;
|
||||
DValue item;
|
||||
item["number"] = "99.9%"; item["label"] = "Uptime"; stats.push(item); item.clear();
|
||||
item["number"] = "500ms"; item["label"] = "Average Response"; stats.push(item); item.clear();
|
||||
item["number"] = "50K+"; item["label"] = "Active Users"; stats.push(item); item.clear();
|
||||
@ -306,7 +306,7 @@ COMPONENT:STATS_SECTION(Request& context)
|
||||
<p>Join thousands of developers who have chosen our framework</p>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<? stats.each([&](DTree stat, String key) {
|
||||
<? stats.each([&](DValue stat, String key) {
|
||||
?><div class="stat-item">
|
||||
<div class="stat-number"><?= stat["number"].to_string() ?></div>
|
||||
<div class="stat-label"><?= stat["label"].to_string() ?></div>
|
||||
@ -364,10 +364,10 @@ COMPONENT:STATS_SECTION(Request& context)
|
||||
|
||||
COMPONENT:BRANDS_SHOWCASE(Request& context)
|
||||
{
|
||||
DTree logos = context.props["logos"];
|
||||
DValue logos = context.props["logos"];
|
||||
if(logos.get_type_name() != "array" || logos["0"]["name"].to_string() == "")
|
||||
{
|
||||
DTree item;
|
||||
DValue item;
|
||||
for(s32 i = 1; i <= 6; i++)
|
||||
{
|
||||
item["name"] = "Brand " + std::to_string(i);
|
||||
@ -383,7 +383,7 @@ COMPONENT:BRANDS_SHOWCASE(Request& context)
|
||||
<div class="brands-container">
|
||||
<h3 class="brands-title"><?= title ?></h3>
|
||||
<div class="brands-grid">
|
||||
<? logos.each([&](DTree logo, String key) {
|
||||
<? logos.each([&](DValue logo, String key) {
|
||||
?><div class="brand-item">
|
||||
<img src="<?= logo["url"].to_string() ?>" alt="<?= logo["name"].to_string() ?>" />
|
||||
</div><?
|
||||
@ -410,10 +410,10 @@ COMPONENT:BRANDS_SHOWCASE(Request& context)
|
||||
|
||||
COMPONENT:TESTIMONIALS(Request& context)
|
||||
{
|
||||
DTree testimonials = context.props["testimonials"];
|
||||
DValue testimonials = context.props["testimonials"];
|
||||
if(testimonials.get_type_name() != "array" || testimonials["0"]["name"].to_string() == "")
|
||||
{
|
||||
DTree item;
|
||||
DValue item;
|
||||
item["name"] = "Sarah Johnson"; item["role"] = "Senior Developer at TechCorp"; item["avatar"] = "img/cat01.jpg"; item["content"] = "This framework has revolutionized our development process."; item["rating"] = "5"; testimonials.push(item); item.clear();
|
||||
item["name"] = "Michael Chen"; item["role"] = "CTO at StartupX"; item["avatar"] = "img/cat02.jpg"; item["content"] = "We switched from our legacy system and saw immediate improvements."; item["rating"] = "5"; testimonials.push(item); item.clear();
|
||||
item["name"] = "Emily Rodriguez"; item["role"] = "Full Stack Developer"; item["avatar"] = "img/cat03.jpg"; item["content"] = "The documentation is excellent and the learning curve is gentle."; item["rating"] = "5"; testimonials.push(item);
|
||||
@ -427,7 +427,7 @@ COMPONENT:TESTIMONIALS(Request& context)
|
||||
<p>Don't just take our word for it - hear from the community</p>
|
||||
</div>
|
||||
<div class="testimonials-grid">
|
||||
<? testimonials.each([&](DTree testimonial, String key) {
|
||||
<? testimonials.each([&](DValue testimonial, String key) {
|
||||
?><div class="testimonial-card">
|
||||
<div class="testimonial-content">
|
||||
<div class="quote-icon">"</div>
|
||||
@ -473,11 +473,11 @@ COMPONENT:TESTIMONIALS(Request& context)
|
||||
|
||||
COMPONENT:PRICING_TABLE(Request& context)
|
||||
{
|
||||
DTree plans = context.props["plans"];
|
||||
DValue plans = context.props["plans"];
|
||||
if(plans.get_type_name() != "array" || plans["0"]["name"].to_string() == "")
|
||||
{
|
||||
DTree plan;
|
||||
DTree feature;
|
||||
DValue plan;
|
||||
DValue feature;
|
||||
|
||||
plan["name"] = "Starter"; plan["price"] = "Free"; plan["period"] = ""; plan["description"] = "Perfect for small projects and learning"; plan["cta"] = "Start Free"; feature = "Up to 3 projects"; plan["features"].push(feature); feature = "Community support"; plan["features"].push(feature); feature = "Basic components"; plan["features"].push(feature); plans.push(plan); plan.clear();
|
||||
plan["name"] = "Pro"; plan["price"] = "$24"; plan["period"] = "/mo"; plan["description"] = "For serious products and teams"; plan["cta"] = "Choose Pro"; plan["popular"].set_bool(true); feature = "Unlimited projects"; plan["features"].push(feature); feature = "Priority support"; plan["features"].push(feature); feature = "Advanced components"; plan["features"].push(feature); plans.push(plan); plan.clear();
|
||||
@ -492,7 +492,7 @@ COMPONENT:PRICING_TABLE(Request& context)
|
||||
<p>Choose the plan that fits your product stage.</p>
|
||||
</div>
|
||||
<div class="pricing-grid">
|
||||
<? plans.each([&](DTree plan, String key) {
|
||||
<? plans.each([&](DValue plan, String key) {
|
||||
bool popular = plan["popular"].to_string() != "";
|
||||
?><div class="pricing-card<?= popular ? " popular" : "" ?>">
|
||||
<? if(popular) { ?><div class="popular-badge">Most Popular</div><? } ?>
|
||||
@ -505,7 +505,7 @@ COMPONENT:PRICING_TABLE(Request& context)
|
||||
<p class="plan-description"><?= plan["description"].to_string() ?></p>
|
||||
</div>
|
||||
<ul class="plan-features">
|
||||
<? plan["features"].each([&](DTree feature, String feature_key) { ?><li><?= feature.to_string() ?></li><? }); ?>
|
||||
<? plan["features"].each([&](DValue feature, String feature_key) { ?><li><?= feature.to_string() ?></li><? }); ?>
|
||||
</ul>
|
||||
<div class="plan-footer">
|
||||
<button class="btn <?= popular ? "btn-primary" : "btn-outline" ?> btn-large plan-cta"><?= first(plan["cta"].to_string(), "Choose Plan") ?></button>
|
||||
|
||||
@ -107,7 +107,7 @@ COMPONENT(Request& context)
|
||||
<div id="theme-menu" class="theme-menu" hidden>
|
||||
<div class="theme-menu-title">Available Themes</div>
|
||||
<div class="theme-menu-list">
|
||||
<? context.cfg.get_by_path("theme/options").each([&](DTree theme_info, String theme_key) {
|
||||
<? context.cfg.get_by_path("theme/options").each([&](DValue theme_info, String theme_key) {
|
||||
StringMap params;
|
||||
params["theme"] = theme_key;
|
||||
String href = app_link(route_path == "index" ? "" : route_path, params, context);
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
DTree asset_props;
|
||||
DValue asset_props;
|
||||
asset_props["css"]["0"] = "themes/common/css/gauges.css";
|
||||
asset_props["js"]["0"] = "components/gauges/common.js";
|
||||
print(component("../theme/assets", asset_props, context));
|
||||
@ -11,7 +11,7 @@ COMPONENT(Request& context)
|
||||
String title = context.props["title"].to_string();
|
||||
String subtitle = context.props["subtitle"].to_string();
|
||||
String style = context.props["style"].to_string();
|
||||
DTree scale = context.props["scale"];
|
||||
DValue scale = context.props["scale"];
|
||||
|
||||
<>
|
||||
<div class="arcgauge-set" id="<?= gauge_attr_id(gauge_id) ?>" style="<?= style ?>">
|
||||
@ -22,9 +22,9 @@ COMPONENT(Request& context)
|
||||
</div>
|
||||
<? } ?>
|
||||
<div class="arcgauge-grid">
|
||||
<? context.props["items"].each([&](DTree item, String item_id_raw) {
|
||||
DTree merged = scale;
|
||||
item.each([&](DTree value, String key) { merged[key] = value; });
|
||||
<? context.props["items"].each([&](DValue item, String item_id_raw) {
|
||||
DValue merged = scale;
|
||||
item.each([&](DValue value, String key) { merged[key] = value; });
|
||||
String item_id = gauge_attr_id(item_id_raw);
|
||||
f64 value = float_val(first(merged["value"].to_string(), "0"));
|
||||
f64 max_value = float_val(first(merged["max"].to_string(), "100"));
|
||||
|
||||
@ -21,10 +21,10 @@ String gauge_attr_id(String value)
|
||||
return(ascii_safe_name(value));
|
||||
}
|
||||
|
||||
String gauge_range_color(DTree ranges, f64 value)
|
||||
String gauge_range_color(DValue ranges, f64 value)
|
||||
{
|
||||
String matched = "";
|
||||
ranges.each([&](DTree range, String key) {
|
||||
ranges.each([&](DValue range, String key) {
|
||||
f64 from_value = float_val(first(range["from"].to_string(), "-999999999"));
|
||||
f64 to_value = float_val(first(range["to"].to_string(), "999999999"));
|
||||
if(value >= from_value && value <= to_value && matched == "")
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
DTree asset_props;
|
||||
DValue asset_props;
|
||||
asset_props["css"]["0"] = "themes/common/css/gauges.css";
|
||||
asset_props["js"]["0"] = "components/gauges/common.js";
|
||||
print(component("../theme/assets", asset_props, context));
|
||||
@ -12,7 +12,7 @@ COMPONENT(Request& context)
|
||||
String title = context.props["title"].to_string();
|
||||
String subtitle = context.props["subtitle"].to_string();
|
||||
f64 size = float_val(first(context.props["size"].to_string(), "200"));
|
||||
DTree scale = context.props["scale"];
|
||||
DValue scale = context.props["scale"];
|
||||
f64 scale_angle_start = float_val(first(scale["angle_start"].to_string(), std::to_string(-gauge_pi())));
|
||||
f64 scale_angle_end = float_val(first(scale["angle_end"].to_string(), "0"));
|
||||
f64 img_height = float_val(first(context.props["img_height"].to_string(), std::to_string(size * std::max(cos(scale_angle_start), cos(scale_angle_end)))));
|
||||
@ -26,9 +26,9 @@ COMPONENT(Request& context)
|
||||
</div>
|
||||
<? } ?>
|
||||
<div class="needlegauge-grid">
|
||||
<? context.props["items"].each([&](DTree item, String item_id_raw) {
|
||||
DTree merged = scale;
|
||||
item.each([&](DTree value, String key) { merged[key] = value; });
|
||||
<? context.props["items"].each([&](DValue item, String item_id_raw) {
|
||||
DValue merged = scale;
|
||||
item.each([&](DValue value, String key) { merged[key] = value; });
|
||||
String item_id = gauge_attr_id(item_id_raw);
|
||||
String label = first(merged["label"].to_string(), item_id_raw);
|
||||
String tooltip = merged["tooltip"].to_string();
|
||||
@ -85,7 +85,7 @@ COMPONENT(Request& context)
|
||||
String segments_html = "";
|
||||
if(merged["color"].get_type_name() == "array")
|
||||
{
|
||||
merged["color"].each([&](DTree range, String key) {
|
||||
merged["color"].each([&](DValue range, String key) {
|
||||
f64 range_from = std::max(float_val(first(range["from"].to_string(), std::to_string(min_value))), min_value);
|
||||
f64 range_to = std::min(float_val(first(range["to"].to_string(), std::to_string(max_value))), max_value);
|
||||
f64 angle_from = angle_start + ((range_from - min_value) / vrange) * (angle_end - angle_start);
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
DTree asset_props;
|
||||
DValue asset_props;
|
||||
asset_props["css"]["0"] = "themes/common/css/gauges.css";
|
||||
asset_props["js"]["0"] = "components/gauges/common.js";
|
||||
print(component("../theme/assets", asset_props, context));
|
||||
@ -17,8 +17,8 @@ COMPONENT(Request& context)
|
||||
String subtitle = context.props["subtitle"].to_string();
|
||||
String layout = first(context.props["layout"].to_string(), "horizontal");
|
||||
String bar_color_default = context.props["bar-color"].to_string();
|
||||
DTree scale = context.props["scale"];
|
||||
DTree markers = context.props["markers"];
|
||||
DValue scale = context.props["scale"];
|
||||
DValue markers = context.props["markers"];
|
||||
|
||||
StringList default_palette;
|
||||
default_palette.push_back("var(--success, #10b981)");
|
||||
@ -37,9 +37,9 @@ COMPONENT(Request& context)
|
||||
<? } ?>
|
||||
<? if(layout == "horizontal") { ?>
|
||||
<div class="progressbar-grid progressbar-grid-horizontal">
|
||||
<? context.props["items"].each([&](DTree bar, String bar_id) {
|
||||
DTree merged = scale;
|
||||
bar.each([&](DTree item, String key) { merged[key] = item; });
|
||||
<? context.props["items"].each([&](DValue bar, String bar_id) {
|
||||
DValue merged = scale;
|
||||
bar.each([&](DValue item, String key) { merged[key] = item; });
|
||||
String item_id = gauge_attr_id(bar_id);
|
||||
String merged_item_style = item_style;
|
||||
if(merged["style"].to_string() != "")
|
||||
@ -73,7 +73,7 @@ COMPONENT(Request& context)
|
||||
<div class="progressbar-background progressbar-background-horizontal">
|
||||
<div class="progressbar-bar" id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>-bar"
|
||||
style="background-color: <?= resolved_color ?>; <?= bar_style ?> opacity: <?= opacity ?>; width: <?= std::to_string(pct_value) ?>%;"></div>
|
||||
<? markers.each([&](DTree marker, String marker_id) {
|
||||
<? markers.each([&](DValue marker, String marker_id) {
|
||||
f64 marker_value = float_val(first(marker["value"].to_string(), "0"));
|
||||
f64 marker_pct = gauge_clamp_value(((marker_value - min_value) / vrange) * 100.0, 0.0, 100.0);
|
||||
String marker_color = first(marker["color"].to_string(), "var(--primary-light)");
|
||||
@ -89,9 +89,9 @@ COMPONENT(Request& context)
|
||||
</div>
|
||||
<? } else { ?>
|
||||
<div class="progressbar-grid progressbar-grid-vertical" style="--progressbar-height: <?= first(context.props["height"].to_string(), "240") ?>px;">
|
||||
<? context.props["items"].each([&](DTree bar, String bar_id) {
|
||||
DTree merged = scale;
|
||||
bar.each([&](DTree item, String key) { merged[key] = item; });
|
||||
<? context.props["items"].each([&](DValue bar, String bar_id) {
|
||||
DValue merged = scale;
|
||||
bar.each([&](DValue item, String key) { merged[key] = item; });
|
||||
String item_id = gauge_attr_id(bar_id);
|
||||
String merged_item_style = item_style;
|
||||
if(merged["style"].to_string() != "")
|
||||
@ -122,7 +122,7 @@ COMPONENT(Request& context)
|
||||
<div class="progressbar-background progressbar-background-vertical">
|
||||
<div class="progressbar-bar" id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>-bar"
|
||||
style="background-color: <?= resolved_color ?>; <?= bar_style ?> opacity: <?= opacity ?>; height: <?= std::to_string(pct_value) ?>%;"></div>
|
||||
<? markers.each([&](DTree marker, String marker_id) {
|
||||
<? markers.each([&](DValue marker, String marker_id) {
|
||||
f64 marker_value = float_val(first(marker["value"].to_string(), "0"));
|
||||
f64 marker_pct = gauge_clamp_value(((marker_value - min_value) / vrange) * 100.0, 0.0, 100.0);
|
||||
String marker_color = first(marker["color"].to_string(), "var(--primary-light)");
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
StarterUser users(context);
|
||||
DTree user = users.current();
|
||||
DValue user = users.current();
|
||||
bool signed_in = user["email"].to_string() != "";
|
||||
String theme_key = context.cfg.get_by_path("theme/key").to_string();
|
||||
String wrapper_class = first(context.props["wrapper_class"].to_string(), theme_key == "localfirst" ? "admin-account-card" : "nav-account nav-menu");
|
||||
|
||||
@ -18,7 +18,7 @@ void app_asset_tag_once(Request& context, String kind, String path)
|
||||
}
|
||||
}
|
||||
|
||||
void app_asset_tags(Request& context, String kind, DTree assets)
|
||||
void app_asset_tags(Request& context, String kind, DValue assets)
|
||||
{
|
||||
String scalar = assets.to_string();
|
||||
if(scalar != "")
|
||||
@ -26,7 +26,7 @@ void app_asset_tags(Request& context, String kind, DTree assets)
|
||||
app_asset_tag_once(context, kind, scalar);
|
||||
return;
|
||||
}
|
||||
assets.each([&](const DTree& item, String key) {
|
||||
assets.each([&](const DValue& item, String key) {
|
||||
app_asset_tag_once(context, kind, item.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ COMPONENT(Request& context)
|
||||
if(nav_class == "" && (context.cfg.get_by_path("theme/key").to_string() == "portal-dark" || context.cfg.get_by_path("theme/key").to_string() == "dark" || context.cfg.get_by_path("theme/key").to_string() == "retro-gaming"))
|
||||
nav_class = "nav-shell";
|
||||
|
||||
DTree account_props;
|
||||
DValue account_props;
|
||||
if(context.props["account_wrapper_class"].to_string() != "")
|
||||
account_props["wrapper_class"] = context.props["account_wrapper_class"];
|
||||
if(context.props["links_wrapper_class"].to_string() != "")
|
||||
@ -18,7 +18,7 @@ COMPONENT(Request& context)
|
||||
<nav<?: nav_class != "" ? " class=\"" + html_escape(nav_class) + "\"" : "" ?>>
|
||||
<div class="nav-menu">
|
||||
<a href="<?= app_link("", context) ?>"><?= context.cfg.get_by_path("site/name").to_string() ?></a>
|
||||
<? context.cfg.get_by_path("menu").each([&](DTree menu_item, String menu_key) {
|
||||
<? context.cfg.get_by_path("menu").each([&](DValue menu_item, String menu_key) {
|
||||
if(menu_item["hidden"].to_string() == "1")
|
||||
return;
|
||||
String href = menu_item["external"].to_string() != "" ? "/" + menu_key : app_link(menu_key, context);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
DTree get_config()
|
||||
DValue get_config()
|
||||
{
|
||||
DTree config;
|
||||
DValue config;
|
||||
|
||||
config["site"]["name"] = "UCE Starter";
|
||||
config["site"]["default_page_title"] = "Home";
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
#load "lib/app.uce"
|
||||
|
||||
DTree starter_router_result(String file, String param = "")
|
||||
DValue starter_router_result(String file, String param = "")
|
||||
{
|
||||
DTree result;
|
||||
DValue result;
|
||||
result["file"] = file;
|
||||
if(param != "")
|
||||
result["param"] = param;
|
||||
return(result);
|
||||
}
|
||||
|
||||
DTree starter_router_resolve(Request& context)
|
||||
DValue starter_router_resolve(Request& context)
|
||||
{
|
||||
String lpath = context.call["route"]["l_path"].to_string();
|
||||
if(lpath == "")
|
||||
return(DTree());
|
||||
return(DValue());
|
||||
|
||||
String exact = "views/" + lpath + ".uce";
|
||||
if(file_exists(exact))
|
||||
@ -34,14 +34,14 @@ DTree starter_router_resolve(Request& context)
|
||||
return(starter_router_result(parent_index, param));
|
||||
}
|
||||
|
||||
return(DTree());
|
||||
return(DValue());
|
||||
}
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
app_init(context);
|
||||
|
||||
DTree resolved = starter_router_resolve(context);
|
||||
DValue resolved = starter_router_resolve(context);
|
||||
|
||||
ob_start();
|
||||
if(resolved["file"].to_string() != "")
|
||||
@ -52,7 +52,7 @@ RENDER(Request& context)
|
||||
}
|
||||
else
|
||||
{
|
||||
DTree notfound_props;
|
||||
DValue notfound_props;
|
||||
notfound_props["message"] = "The requested page does not exist.";
|
||||
print(component("components/basic/notfound", notfound_props, context));
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ void app_init(Request& context)
|
||||
context.call["route"]["page"] = context.params["ROUTE_PAGE"];
|
||||
context.call["route"]["valid"].set_bool(context.params["ROUTE_VALID"] != "0");
|
||||
|
||||
DTree config = get_config();
|
||||
DValue config = get_config();
|
||||
session_start();
|
||||
String requested_theme = first(context.get["theme"], context.session["app_theme"], config["theme"]["key"].to_string());
|
||||
if(config["theme"]["options"][requested_theme].get_type_name() != "array")
|
||||
@ -53,8 +53,8 @@ void app_init(Request& context)
|
||||
if(context.get["theme"] != "")
|
||||
context.session["app_theme"] = requested_theme;
|
||||
|
||||
DTree selected_theme = config["theme"]["options"][requested_theme];
|
||||
selected_theme.each([&](DTree item, String key) {
|
||||
DValue selected_theme = config["theme"]["options"][requested_theme];
|
||||
selected_theme.each([&](DValue item, String key) {
|
||||
config["theme"][key] = item;
|
||||
});
|
||||
config["theme"]["key"] = requested_theme;
|
||||
|
||||
@ -50,9 +50,9 @@ struct AppUser
|
||||
return(hash);
|
||||
}
|
||||
|
||||
static DTree read_json_file(String file_name)
|
||||
static DValue read_json_file(String file_name)
|
||||
{
|
||||
DTree result;
|
||||
DValue result;
|
||||
if(!file_exists(file_name))
|
||||
return(result);
|
||||
String raw = trim(file_get_contents(file_name));
|
||||
@ -61,24 +61,24 @@ struct AppUser
|
||||
return(json_decode(raw));
|
||||
}
|
||||
|
||||
static bool write_json_file(String file_name, DTree data)
|
||||
static bool write_json_file(String file_name, DValue data)
|
||||
{
|
||||
mkdir(dirname(file_name));
|
||||
return(file_put_contents(file_name, json_encode(data)));
|
||||
}
|
||||
|
||||
DTree load(String email)
|
||||
DValue load(String email)
|
||||
{
|
||||
DTree user = read_json_file(file_name(email));
|
||||
DValue user = read_json_file(file_name(email));
|
||||
if(user.get_type_name() != "array")
|
||||
return(DTree());
|
||||
return(DValue());
|
||||
user["id"] = normalize_email(email);
|
||||
return(user);
|
||||
}
|
||||
|
||||
DTree create(String email, String password)
|
||||
DValue create(String email, String password)
|
||||
{
|
||||
DTree result;
|
||||
DValue result;
|
||||
email = normalize_email(email);
|
||||
password = trim(password);
|
||||
result["message"] = "";
|
||||
@ -95,7 +95,7 @@ struct AppUser
|
||||
return(result);
|
||||
}
|
||||
|
||||
DTree existing = load(email);
|
||||
DValue existing = load(email);
|
||||
if(existing.get_type_name() == "array" && existing["email"].to_string() != "")
|
||||
{
|
||||
result["message"] = "user_exists";
|
||||
@ -103,12 +103,12 @@ struct AppUser
|
||||
}
|
||||
|
||||
String salt = gen_sha1(std::to_string((u64)time()) + ":" + std::to_string((u64)(time_precise() * 1000000.0)) + ":" + session_id_create()).substr(0, 24);
|
||||
DTree user;
|
||||
DValue user;
|
||||
user["email"] = email;
|
||||
user["salt"] = salt;
|
||||
user["password_hash"] = password_hash(password, salt);
|
||||
user["created"] = std::to_string((u64)time());
|
||||
DTree role;
|
||||
DValue role;
|
||||
role = "user";
|
||||
user["roles"].push(role);
|
||||
|
||||
@ -120,14 +120,14 @@ struct AppUser
|
||||
return(result);
|
||||
}
|
||||
|
||||
DTree sign_in(String email, String password)
|
||||
DValue sign_in(String email, String password)
|
||||
{
|
||||
DTree result;
|
||||
DValue result;
|
||||
result["result"].set_bool(false);
|
||||
email = normalize_email(email);
|
||||
password = trim(password);
|
||||
|
||||
DTree user = load(email);
|
||||
DValue user = load(email);
|
||||
if(user.get_type_name() != "array" || user["email"].to_string() == "")
|
||||
{
|
||||
result["message"] = "no_such_user";
|
||||
@ -161,7 +161,7 @@ struct AppUser
|
||||
String user_id = context.session[session_key()];
|
||||
if(user_id == "")
|
||||
return(false);
|
||||
DTree user = load(user_id);
|
||||
DValue user = load(user_id);
|
||||
if(user["email"].to_string() == "")
|
||||
{
|
||||
context.session.erase(session_key());
|
||||
@ -171,11 +171,11 @@ struct AppUser
|
||||
return(true);
|
||||
}
|
||||
|
||||
DTree current()
|
||||
DValue current()
|
||||
{
|
||||
if(is_signed_in())
|
||||
return(context.call["app"]["current_user"]);
|
||||
return(DTree());
|
||||
return(DValue());
|
||||
}
|
||||
|
||||
void logout()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user