fix: harden UCE runtime and starter

This commit is contained in:
udo 2026-06-11 13:44:24 +00:00
parent 71ddcaf7d4
commit 7f757654b6
128 changed files with 276200 additions and 872 deletions

View File

@ -46,6 +46,8 @@ The current build expects:
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
- standard Linux development headers for `dl`, `pthread`, sockets, and backtrace support
SQLite is vendored under `src/3rdparty/sqlite/` and compiled by `scripts/build_linux.sh`; no system SQLite package is required.
The binary is written to:
```bash
@ -80,9 +82,11 @@ Useful helpers for that data model include:
- `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
- `sqlite_connect()`, `sqlite_query()`, and related helpers for embedded SQLite storage with named prepared parameters
- `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
Named component handlers are also supported:
@ -206,15 +210,19 @@ The current error page includes:
- request URI
- resolved script path
- generated C++ path when available
- high-level error summary
- source/generated excerpts and raw compiler output paths for template/component/unit failure modes
- signal number and name when applicable
- a native backtrace
Compile failures are also formatted with the source path, generated C++ path, compile-output artifact path, a nearby source/generated excerpt when a line can be identified, and the raw compiler output.
This recovery path currently covers normal request handling. It is not yet the universal recovery path for every runtime subsystem.
## Docs And Tests
The most current user-facing reference lives under `site/doc/`, and the demo pages live under `site/test/`.
The most current user-facing reference lives under `site/doc/`, and the demo pages live under `site/test/`. Developers coming from React, Next, or Remix should start with `site/doc/pages/coming_from_react.txt` / `/doc/index.uce?p=coming_from_react` for the concept map and starter-router notes.
Useful entry points:

492
RECOMMENDATIONS.md Normal file
View File

@ -0,0 +1,492 @@
# UCE Code Review — Full Findings (2026-06-11)
Scope: the pending working-tree changes (73 modified tracked files plus new
untracked sources — notably `src/lib/sqlite-connector.cpp/.h` and the
uce-starter theme/router rework).
Method: seven independent review angles (line-by-line diff scan,
removed-behavior audit, cross-file call tracing, reuse, simplification,
efficiency, altitude), followed by a verification pass on the correctness
candidates. Status legend:
- **Confirmed** — verified against the working tree, decisive lines quoted.
- **Plausible** — surfaced by a review angle, not individually re-verified.
- **Refuted** — investigated and found not to be an issue (kept here so
nobody re-flags it).
---
## Part 1 — Correctness (all confirmed)
### 1.1 XSS: island props break out of single-quoted attribute
**File:** `site/examples/uce-starter/components/theme/web_affordances.uce:12`
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
prop value terminates the attribute.
**Failure:** a view passes user-influenced prop text containing an apostrophe,
e.g. `x' autofocus onfocus='alert(1)` — the attribute terminates early and
attacker-controlled attributes/event handlers land on the div. Even benign
values like `Don't` silently truncate the payload.
**Fix:** use a double-quoted attribute (html_escape covers `"`), or extend
`html_escape` to escape `'` as `&#39;`.
**Status:** fixed, but fix not verified yet.
### 1.2 Crash: positional `?` placeholders in sqlite_query
**File:** `src/lib/sqlite-connector.cpp:86`
`bind_params` constructs a `String` directly from
`sqlite3_bind_parameter_name(stmt, i)`, which returns NULL for nameless
positional `?` parameters. `std::string(nullptr)` is UB and crashes before the
`if(name == "") continue;` guard on the next line can run.
**Failure:** any page calling
`sqlite_query(db, "select * from notes where id = ?", params)` with a bare `?`
instead of `:name` segfaults the worker (500 recovery page).
**Fix:** remove support for positional placeholders from sqlite and mysql API in favor of named placeholders, adjust the documentation accordingly.
**Status:** fixed, but fix not verified yet.
### 1.3 HTTP parsing: split_http_headers assumes lines[0] is the request line
**File:** `src/lib/functionlib.cpp:754`
The rewritten parser unconditionally treats the first line as the HTTP request
line. The old parser located the first colon-free line, which tolerated a
leading CRLF (RFC 7230 §3.5) and header-only input. Two failure modes:
- The direct-HTTP caller (`src/fastcgi/src/fcgicc.cc:693`) passes the raw
buffer unstripped — a client sending a stray leading CRLF before
`GET /x.uce HTTP/1.1` gets the request line silently dropped (empty
`REQUEST_METHOD`, request fails).
- Header-only input (`Host: example.test\nX-Token: abc`, e.g. page code
parsing a raw header block) parses `Host:` as `REQUEST_METHOD` and loses
`HTTP_HOST` entirely. The function is directly callable from .uce pages.
**Fix:** skip leading empty lines before consuming the request line, and only
treat a colon-free first line as the request line.
**Status:** fixed, but fix not verified yet.
### 1.4 Silent data loss: multi-statement SQL drops everything after the first `;`
**File:** `src/lib/sqlite-connector.cpp:178`
`SQLite::query` passes `0` for `pzTail` to `sqlite3_prepare_v2` and never
checks for remaining SQL, so multi-statement strings execute only the first
statement and still report `ok`.
**Failure:** a migration page runs
`sqlite_query(db, "create table t(id integer); insert into t values(1);")`
only the CREATE executes, the INSERT is silently dropped, `sqlite_error()`
says `ok`, leaving the database half-migrated with no error signal.
**Fix:** capture `pzTail` and either loop over remaining statements or raise
an error when trailing SQL is present.
**Status:** fixed, but fix not verified yet.
### 1.5 Diagnostics regression: fault backtrace captured after siglongjmp
**File:** `src/linux_fastcgi.cpp:895`
The in-handler `capture_backtrace_string` call (`request_fault_trace`) was
deleted; the trace is now captured after `siglongjmp` back in
`handle_complete`, where the faulting stack has already been unwound. No
capture remains in `on_request_fault_signal`.
**Failure:** a `.uce` page null-derefs → SIGSEGV → the error page's Trace
shows only `handle_complete`/`main` frames. The faulting unit's frames are
gone, making crash reports undiagnosable beyond the signal number (the README
still advertises a native backtrace of the failure).
**Fix:** restore the capture inside `on_request_fault_signal` (on the faulting
stack) and hand the result across the longjmp, as the previous code did.
**Status:** fixed, but fix not verified yet.
### 1.7 Memory leak: SQLite wrapper objects never freed by request cleanup
**File:** `src/lib/sqlite-connector.cpp:248`
`cleanup_sqlite_connections()` closes the raw `sqlite3` handles tracked in
`resources.sqlite_connections` but never deletes the heap-allocated `SQLite`
wrappers from `sqlite_connect` (`new` without `delete`; no arena allocator is
compiled in). `sqlite_disconnect` itself is correct (`delete db` after
unregistering). Note: this mirrors a pre-existing identical leak in the MySQL
connector (`cleanup_mysql_connections`, `mysql-connector.cpp:298-304`).
**Failure:** a page calls `sqlite_connect()` per request and relies on
end-of-request cleanup instead of `sqlite_disconnect()` — exactly what the
cleanup path exists for. One wrapper leaks per request; unbounded RSS growth
in the long-lived FastCGI worker.
**Fix:** track the wrapper objects (not raw handles) in
`resources.sqlite_connections` and delete them in cleanup; fix the MySQL
connector the same way, or factor a shared registry (see 5.1).
**Status:** fixed, but fix not verified yet.
### 1.8 Asset shims emit stylesheets mid-`<body>`
**File:** `site/examples/uce-starter/components/example/marketing_assets.uce:8`
(also `theme_assets.uce`, `gauges/assets.uce`)
The new ONCE-based asset shims fire inside the view's `ob_start()` capture in
`index.uce` (lines 67-85: view is captured into `fragments["main"]`, then
`themes/page` renders), so their `<link rel="stylesheet">` output is baked
into the main fragment and spliced into the content div — inside `<body>`,
not `<head>`.
**Failure:** every marketing/theme/gauges page ships its stylesheet mid-body
(FOUC, invalid-ish markup). The deleted `page_shell` asset registry rendered
these in `<head>`. While this behavior is often okay in practice, we should
improve on this to encourage cleaner output.
**Fix:** Part A: restore a registration mechanism: record component and asset output
that's intended as once per page in context.call["fragments"]["once"] by default
and the page template component can then explicitly slot this in where appropriate.
Part B: Modify the preprocessor so directives support attributes like this:
ONCE(Request& context)
@fragment my-fragment-name
{
...
}
Which will then automatically (in this example) slot the output into
context.call["fragments"]["my-fragment-name"] instead of the default slot name "once".
In the future we'll introduce more attributes with this syntax. Using this flexible mechanism for the fragment slot
name, we can leave it up to the page template where to slot in what. ONCE, COMPONENT,
and RENDER should support the fragment attribute.
**Status:** fixed, but fix not verified yet.
### 1.9 Error page "Generated C++" hint prints a nonexistent path
**File:** `src/linux_fastcgi.cpp:79`
`render_request_failure` computes the hint as
`path_join(BIN_DIRECTORY, SCRIPT_FILENAME) + ".cpp"`, but `path_join` returns
an absolute child unchanged (`src/lib/sys.cpp:179`), while the compiler builds
the real path by plain concatenation `BIN_DIRECTORY + src_path`
(`src/lib/compiler.cpp:631-636`). SCRIPT_FILENAME is always absolute in the
FastCGI path.
**Failure:** every runtime failure page prints e.g.
`Generated C++: /var/www/site/page.uce.cpp` — the BIN_DIRECTORY prefix is
silently dropped and the path never exists (real file:
`/var/cache/uce/work/var/www/site/page.uce.cpp`).
**Fix:** export one helper from `compiler.cpp` that maps a source file to its
generated artifact (`su->pre_path + "/" + su->pre_file_name` — the new
`compiler_format_compile_failure` in this same changeset already computes it
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
**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
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.
**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&`.
**Status:** fixed, but fix not verified yet.
### 1.11 Proactive compiler child can std::terminate on lock failure
**File:** `src/lib/compiler.cpp:288`
`compiler_with_registry_lock` now throws `std::runtime_error` when the
registry lock file can't be opened (previously: warning + proceed unlocked).
`run_proactive_compiler` (`src/linux_fastcgi.cpp:1270-1372`) calls
`compiler_list_known_units` / `compiler_set_known_units` with no try/catch
anywhere in the chain, so a transient failure (fd exhaustion, disk full)
aborts the forked child via `std::terminate`.
**Blast radius is small:** the proactive compiler runs in a forked child that
the main loop respawns each iteration, and request-path callers are protected
by the try/catch in `handle_complete`. Still, a catch-and-log around the
proactive scan is cheap insurance.
**Status:** fixed, but fix not verified yet.
---
## Part 2 — Reuse / duplication (plausible unless noted)
### 2.1 request_query_path() duplicates request_query_route()
**File:** `src/lib/uri.cpp:311`
`request_query_path()` copy-pastes `request_query_route()`'s (line 325)
first-keyless-segment scan verbatim — two identical "find first &-part
without =" loops plus `route_path_sanitize` calls that must be kept in sync.
It also has **zero callers** in `src/` or `site/` (only a doc page references
it).
**Fix:** implement as
`return request_query_route(context, default_path)["l_path"].to_string();`.
**Status:** fixed, but fix not verified yet.
### 2.2 list_filter / list_map re-implement the generic filter<T> / map templates
**File:** `src/lib/functionlib.cpp:71`
`StringList` is `typedef std::vector<String>` (`types.h:52`), so the existing
`filter<T>` template (`functionlib.h:68`, declared ~10 lines above the new
`list_*` declarations) already does exactly what `list_filter` does. Two
filter implementations now live in the same module; behavior fixes must be
applied twice.
**Fix:** drop it and point the doc page at `filter`); same consideration for `list_map`.
**Status:** fixed, but fix not verified yet.
### 2.3 Hand-rolled redirects instead of the redirect() helper
**Files:** `site/examples/uce-starter/views/account/login.uce:13`,
`logout.uce:6`, `profile.uce:8`, `site/demo/sqlite.uce:21-22`
Four call sites inline `context.set_status(302, "Found");
context.header["Location"] = ...` instead of calling the existing runtime
helper `redirect(String url, s32 code = 302)` (`src/lib/uri.cpp:414`). This
replaced the single `app_redirect()` helper the changeset deleted.
**Note — security aspect refuted (see 6.1):** headers are sanitized centrally
on write-out, so this is a pure reuse nit, not a vulnerability.
**Fix:** use `redirect(app_link("account/profile", context))` or restore one
`app_redirect` wrapper.
**Status:** fixed, but fix not verified yet.
### 2.5 Asset tag boilerplate repeated across ~a dozen files
**Files:** `components/example/marketing_assets.uce:5`,
`components/example/theme_assets.uce`, `components/gauges/assets.uce`,
plus inline `<link rel="stylesheet" href="<?= app_asset_url(...) ?>" />`
blocks in `components/theme/head.uce`, `components/data/widgets.uce`,
`components/workspace/primitives.uce`, `views/dashboard.uce`
About a dozen hand-written copies of the stylesheet/script tag pattern around
`app_asset_url()`; a versioning or attribute change (defer/integrity) touches
every file. The marketing and theme shims differ only in one CSS path.
**Fix:** collapse the three shims into one parameterizable assets component.
**Status:** fixed, but fix not verified yet.
---
## Part 3 — Simplification (plausible)
### 3.1 request_query_route() emits a derivable "rejected" flag
**File:** `src/lib/uri.cpp:347`
The route tree contains both `route["valid"]` and `route["rejected"]` where
`rejected` is exactly `!valid`, and nothing in the codebase reads
`"rejected"`. Redundant derivable state doubles the invariant surface — a
future path that sets one flag without the other produces a route tree that
lies.
**Fix:** drop the `"rejected"` key; callers needing it can write
`!route["valid"].to_bool()`.
**Status:** fixed, but fix not verified yet.
### 3.2 Starter router carries write-only diagnostic state
**File:** `site/examples/uce-starter/index.uce:38`
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
`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.
**Fix:** remove unused parts.
**Status:** fixed, but fix not verified yet.
### 3.4 Single-link shim components where a direct ONCE block suffices
**File:** `site/examples/uce-starter/components/example/marketing_assets.uce:8`
(and `theme_assets.uce`)
These are single-`<link>` shim files with empty COMPONENT bodies, invoked via
`print(component(...))` of an empty string. Routed views are mutually
exclusive per request, so the cross-unit dedup the shims provide can never
trigger; they exist only to host one ONCE line at the cost of two extra files
and an indirection on every view. `dashboard.uce:3-6` already demonstrates
the simpler form (ONCE block directly in the view). `gauges/assets.uce` is
the justified case (multiple sibling components per page) and can stay.
**Status:** fixed, but fix not verified yet.
### 3.5 Compile-failure artifact file is self-referential
**File:** `src/lib/compiler.cpp:847`
`compile_shared_unit()` overwrites `su->compiler_messages` with the formatted
failure report, then writes that report into `compile_output_file_name` — the
same artifact the report's own "Compile output:" line
(`compiler_format_compile_failure`, line 800) points readers at. No copy of
the raw, unformatted compiler output survives for tooling to parse.
**Fix:** keep raw messages as the stored/recorded artifact content and format
only at the print/response boundary.
**Status:** fixed, but fix not verified yet.
---
## Part 4 — Efficiency (plausible unless noted)
### 4.1 QUERY_STRING parsed twice per request
**File:** `src/linux_fastcgi.cpp:744`
`prepare_request_body_maps` calls `request_populate_context_params` (which
splits QUERY_STRING on `&` + uri-decodes inside `request_query_route`) and
then `parse_query(QUERY_STRING)` re-splits and re-decodes the identical
string on the next line. Runs on every HTTP request, CLI invocation, and
websocket event. Route params are also computed eagerly for requests that
never read `ROUTE_*`/`BASE_URL`.
**Fix:** parse QUERY_STRING once into `request.get` first and derive the
route token from that single pass, or populate the route params lazily.
**Status:** fixed, but fix not verified yet.
### 4.2 Repeated normalization in request_populate_context_params
**File:** `src/lib/uri.cpp:351`
Per request: `route_path_normalize` runs three times on the same path
(directly, inside `route_path_sanitize`, and inside `route_path_is_safe`),
`request_script_url` is computed twice (for SCRIPT_URL and again inside
`request_base_url`), and `route_path_normalize` (line 255) strips slashes via
`path = path.substr(1)` in a while loop — O(n²) copies per slash run.
**Fix:** normalize once and pass the normalized string down; compute
script_url into a local used by both params; replace the substr loops with
`find_first_not_of("/")` / `find_last_not_of("/")` and a single substr.
**Status:** fixed, but fix not verified yet.
### 4.4 collect_rows rebuilds column-name strings for every row
**File:** `src/lib/sqlite-connector.cpp:118`
For R rows × C columns the row loop does R×C `sqlite3_column_name` calls plus
R×C String heap constructions and map inserts keyed on the fresh string,
though column names are invariant across rows. A 10k-row, 8-column result
builds 80k redundant name strings per query.
**Fix:** build a `std::vector<String> names(column_count)` once before the
step loop and index it inside (`row[names[i]]`).
**Status:** fixed, but fix not verified yet.
### 4.5 bind_params copies the params map twice and binds SQLITE_TRANSIENT
**File:** `src/lib/sqlite-connector.cpp:95`
`SQLite::query` takes `StringMap` by value and passes it by value again to
`bind_params`, then binds with `SQLITE_TRANSIENT`, forcing SQLite to memcpy
each value a third time — even though the copied map outlives the statement
(it lives until after `sqlite3_finalize`).
**Fix:** pass `const StringMap&` through `query()`/`bind_params` and bind
with `SQLITE_STATIC` (or at least drop the two by-value map copies).
**Status:** fixed, but fix not verified yet.
### 4.6 Per-request connection opens re-run pragmas; busy_timeout set twice
**File:** `src/lib/sqlite-connector.cpp:15`
`connect()` calls `sqlite3_busy_timeout(5000)` and then
`apply_default_pragmas` runs `PRAGMA busy_timeout = 5000` again (pure
duplicate). `PRAGMA journal_mode = WAL` is persistent in the database file
but re-issued on every open. Because cleanup closes all handles at request
end, a page like `site/demo/sqlite.uce` pays `sqlite3_open_v2` + 4 pragmas +
`CREATE TABLE IF NOT EXISTS` on every request.
**Fix:** drop the redundant busy_timeout pragma; cache open
connections per worker keyed by path across requests (resetting state at
request end) so pragmas and schema checks run once per worker.
**Status:** fixed, but fix not verified yet.
---
## Part 5 — Altitude / design (plausible unless noted)
### 5.3 Test suites hand-registered in three parallel lists — already drifting
**Files:** `site/tests/index.uce:15`, `tests/plugins/uce_site_suite.py`
A test page must be registered in three places: the `site/tests/*.uce` file
itself, a `site_tests_card()` line in `index.uce`, and a tuple in
`uce_site_suite.py`. The new `sqlite.uce` was added to all three by hand. The
lists have already drifted: `site/tests/call_helpers.uce` exists on disk but
appears in neither `index.uce` nor any plugin list, and
`security_headers.uce` is covered only by the security-smoke plugin, not the
index cards — new suites can silently fall out of the dashboard and/or CI.
**Fix:** enumerate `site/tests/*.uce` with ls()/glob in both `index.uce` and
`uce_site_suite.py`, with title/tags metadata declared once (in the test page
or a single shared manifest).
**Status:** fixed, but fix not verified yet.
### 5.4 compiler_developer_hints() couples the runtime to clang's English message text
**File:** `src/lib/compiler.cpp:778`
The hint table pattern-matches hardcoded English clang diagnostic substrings
("no member named", "expected ';'", ...). A clang version bump, a switch to
gcc (COMPILE_SCRIPT is user-configurable server config), or localized
diagnostics silently degrades every hint to the generic fallback, and each
new error class means hand-extending an if-chain in runtime C++.
**Fix:** the excerpt mechanism added in the same change
(`compiler_format_compile_failure`'s source/generated excerpts + artifact
paths) already carries the diagnostic value; make the hints a data-driven
table or drop them in favor of the excerpts.
**Status:** fixed, but fix not verified yet.
### 5.5 Generated-artifact path computed by hand in two places
**File:** `src/linux_fastcgi.cpp:79` vs `src/lib/compiler.cpp:631-636`
The deeper-fix framing of 1.9: the moment the compiler's artifact layout or
naming changes (hashing, per-config subdirs), any hand-recomputed path goes
stale. One exported source-file → artifact-path helper in `compiler.cpp`,
used by both the compile-failure formatter and the runtime failure page.
**Status:** fixed, but fix not verified yet.

496
WASM-PROPOSAL.md Normal file
View File

@ -0,0 +1,496 @@
# WASM-PROPOSAL: WebAssembly Unit Runtime for UCE
- **Status:** proposal / design draft (2026-06-11)
- **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`).
---
## 1. Motivation
Two long-standing structural problems and one strategic opportunity share a
single root cause: **request code shares an address space and an allocator
with the runtime.**
1. **The arena attempt failed for a structural reason.** The
`GLOBAL_ARENA_ALLOCATOR` design in `_scratchpad.cpp` swapped the global
`operator new`/`delete` against a `current_memory_arena`. A single global
allocator cannot distinguish request-lifetime allocations from
process-lifetime ones: during a request, server-lifetime structures
(compile registry, sessions, config trees, unit statics) also allocate.
Under the arena those dangle on reset; with `delete` as a no-op,
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.
2. **Fault recovery is best-effort, not sound.** The current
SIGSEGV → `sigsetjmp`/`siglongjmp` recovery performs no unwinding, skips
destructors, can resume over corrupted worker state, and (see
RECOMMENDATIONS.md 1.5) cannot reliably produce a useful backtrace.
A faulting unit *can* have scribbled on runtime memory before the signal.
3. **UCE can only ever host fully-trusted code.** A `.uce` page is arbitrary
native code with full process privileges. Multi-tenant or user-supplied
page hosting is structurally impossible in the native model.
A WASM execution model deletes the shared-fate fact itself rather than
patching around it:
- **Arena by construction:** each request runs in its own linear memory,
dropped wholesale at request end. Request-lifetime memory physically cannot
outlive the request; server-lifetime state physically cannot live inside
it. The `_scratchpad.cpp` design becomes correct because the boundary is
structural, not typological.
- **Sound recovery:** a trap (null deref, OOB, stack exhaustion) is a defined
host-side error that unwinds cleanly, with a precise guest stack trace, and
the unit cannot have touched host memory. Render error page, drop
workspace, keep serving — actually correct, not hopeful.
- **Capability security:** page code gets exactly the imported API surface
and nothing else. Multi-tenant hosting becomes possible.
- **Secondary wins:** architecture-independent unit artifacts (no clang
required on prod), safe module unload/replace (vs. never-safe `dlclose`),
first-class limits (linear-memory cap = RAM limit, epoch/fuel = CPU
timeout), per-request memory stats for free.
**Accepted costs:** ~1.22× compute slowdown vs. native (still far ahead of
interpreted runtimes); a real ABI/membrane design; ownership of a custom
loader; toolchain rough edges (§10).
---
## 2. Rejected alternatives (recorded so they stay rejected)
- **In-process PMR arena.** Requires re-typedefing `String`/`DTree` 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
RPCs: callees receive the context **by reference**, mutate `context.call`,
share the `ob_*` capture stack and `ONCE` dedup state. Per-component
instances force serialize/copy/deserialize of the context on every
`component()` call (a dozen+ per page in the starter), require
host-mediation of the ob stack, and silently change reference semantics to
copy semantics. Rejected.
- **One linked module per app ("the blob").** Introduces an "app" concept
UCE does not have, makes every edit a global relink, turns the artifact
cache into a build graph — webpack's worst traits without its benefits.
Rejected. The file stays the unit.
- **Eager pre-loading of known units at worker warm-up.** Rejected; loading
is strictly lazy, on first explicit call, preserving current semantics.
---
## 3. Architecture overview
### 3.1 Execution model
```
host process (linux_fastcgi, per worker)
├─ vendored wasm runtime (§10.1)
├─ unit artifact cache: one PIC .wasm per unit (replaces per-unit .so)
├─ loader (§6): dylink parsing, base allocation, GOT resolution,
│ symbol registry, ABI stamp check
├─ core snapshot: "core module, initialized" memory+table image
└─ per request: WORKSPACE
├─ linear memory (CoW-born from core snapshot)
├─ shared funcref table
├─ core module instance (uce_lib + libc compiled to wasm)
├─ unit module instances (loaded lazily, on first call, incl. mid-request)
└─ host handle table (sqlite/mysql/file/socket handles + closers)
```
- **Workspace = request.** Born from the core snapshot, dropped at request
end. Memory drop is the arena; handle-table drop is resource cleanup
(this generalizes and replaces the per-connector
`cleanup_*_connections()` pattern — RECOMMENDATIONS.md 1.7 / 5.1 become
structurally unrepresentable).
- **One unit = one PIC wasm module.** Compile, cache, and invalidation
granularity stay per-file. The `.uce → C++` translation pipeline is
unchanged; only the compile target changes
(`clang --target=wasm32-wasi -fPIC` + `wasm-ld -shared`).
- **Strictly lazy loading.** A unit's module is instantiated into a
workspace the first time that workspace calls it — including mid-request.
This is the wasm equivalent of today's compile-and-`dlopen`-on-first-hit
and requires no restart, no fallback path. Placement memoization
(deterministic bases so repeat instantiation is cheaper) is a permitted
optimization; it must not change the loading policy.
- **In-flight isolation.** Module versions are immutable; a recompiled unit
becomes a new module. Running workspaces keep what they loaded; new
workspaces get the new version. Old modules are dropped when unreferenced
(safe unload — impossible with `dlclose`).
### 3.2 Memory model
- **One heap, one allocator, one DTree 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,
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
(replaces the tracking `operator new` in `types.h`).
- **Unit statics reset per request** (workspace is born from the core
snapshot, which does not include unit data; unit data segments initialize
at unit load within the workspace). This is *more* shared-nothing than
today, where `.so` statics persist across requests within a worker.
Cross-request state must use explicit host facilities (sessions, caches).
**Breaking change — must be called out in docs and checked against the
site/ tree during Phase 5.**
### 3.3 The host membrane
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),
3. **handles** (opaque `u32` indices into the per-workspace host handle
table; each entry carries a closer callback).
Everything pointer-shaped stays on its own side. Hostcall surface budget:
3060 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).
### 3.4 DTree 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).
- `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.
- 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.
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)
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),
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.
This ABI is versioned: every unit artifact carries a custom section
(`uce.abi`: core ABI version + toolchain fingerprint); the loader refuses
stale units and triggers lazy recompilation (units are lazily compiled
anyway, so this costs nothing structurally).
**Phase 1 of the implementation plan is to introduce this C ABI in the
current native runtime** — it is useful immediately (plugin surface,
testability) and de-risks the rest.
---
## 4. Two component-call planes / multi-language support
The design stratifies languages by one question: *can the toolchain produce a
PIC linear-memory module that adopts a foreign allocator?*
**Plane A — workspace peers** (C++, C, Rust, Zig, …):
- Join the workspace as PIC modules importing core symbols.
- 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>`
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.
**The semantic rule (enforced by the loader, not by convention):**
cross-plane components do **not** receive the mutable context. They get an
explicit interface — props in (copied by definition), rendered output and
declared results back. Plane A keeps the full "here's the world, mutate it"
contract. A `component()` call must never silently change mutation semantics
based on the callee's implementation language.
The cross-instance call mechanism is shared by: Plane B units, and future
cross-trust-boundary components (multi-tenant). Serialization boundaries and
isolation boundaries are the same lines, by design.
---
## 5. ABI sketches (to be finalized in Phase 0/1)
### 5.1 Hostcall surface (grouped; target ≤ 60 functions)
```
request: uce_host_ctx_read(buf) → len // wire-encoded context, once
response: uce_host_respond(status, hdrs_buf, body_buf)
uce_host_stream_write(buf) // chunked/streaming path
log: uce_host_log(level, buf)
sqlite: uce_host_sqlite_connect(path_buf) → handle | err
uce_host_sqlite_query(handle, sql_buf, params_buf) → result_buf | err
uce_host_sqlite_cursor_*(...) // optional row-cursor variant
uce_host_sqlite_insert_id/affected/error/disconnect(handle)
mysql: (same shape; existing connector APIs are already handle-shaped)
files: uce_host_file_read/write/stat/list(path_buf, ...) // policy-gated
session: uce_host_session_get/set(key_buf, val_buf)
http: uce_host_http_request(req_buf) → handle/result_buf // outbound
misc: uce_host_time(), uce_host_random(buf), uce_host_env(key_buf)
loader: uce_host_component_resolve(path_buf) → table_index // may load (§6)
ws: uce_host_ws_send(buf), event delivery via render entry re-invocation
```
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)
```c
typedef struct uce_dtree uce_dtree; // 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*);
/* 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);
/* 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);
/* ob / print / helpers: uce_print, uce_ob_start, uce_ob_get_close,
uce_html_escape, uce_json_encode, ... (mirror uce_lib surface) */
```
No unwinding across this surface; C++ exceptions are caught at the edge and
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
actual fields — value + ordered children):
```
node := value children
value := varint len, bytes (utf-8)
children := varint count, count × ( key: varint len + bytes, node )
flags := one leading byte per node reserved (bit0: is_list hint)
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.
### 5.4 Unit module contract
```
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, ...)
exports: uce_unit_setup, uce_unit_render,
uce_unit_component, uce_unit_websocket
(same roles as today's UCE_SETUP/RENDER/COMPONENT/WEBSOCKET
dlsym symbols in compiler.cpp)
forbidden: defining malloc/free/operator new, own memory, start fn
with side effects beyond data init
```
---
## 6. The loader (host-side, custom, load-bearing)
Owned code, ~12k lines, vendored-runtime-adjacent. Reference logic:
Emscripten's dylink loader (the ABI is the stable, battle-tested part; the
server-side loader is what doesn't exist off the shelf).
Per `load(unit)` into a workspace:
1. Fetch compiled module from artifact cache (compile on miss — today's
lazy-compile path, retargeted).
2. Verify `uce.abi` stamp against the core; on mismatch, recompile unit.
3. Verify import discipline (no allocator/runtime definitions; §3.2).
4. Parse `dylink.0`: data size/alignment, table slots needed.
5. Allocate `__memory_base` (bump within workspace data region) and
`__table_base` (append to shared table).
6. Instantiate with bases; resolve `GOT.*` imports against the workspace
symbol registry (core symbols + previously loaded units); register the
unit's exports.
7. Register entry points in the path → table-index dispatch map.
`uce_host_component_resolve(path)` consults the dispatch map and calls
`load()` on miss — this is how lazy, programmatic, mid-request loading works
with no special cases.
Placement memoization (optional, later): record each unit's first-assigned
bases; reuse across workspaces so instantiation is cheaper and snapshot
growth (below) stays consistent. Does not change the lazy policy.
**Core snapshot:** the only pre-built state is "core module, initialized" —
memory bytes + table state captured once per core build. Workspaces are born
from it via CoW (`mmap(MAP_PRIVATE)` of the snapshot image; the host owns
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
2. workspace = birth_from_core_snapshot() (CoW, ~µs)
3. write wire-encoded context into workspace; core decodes → context DTree
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
6. I/O via hostcalls; resources land in the workspace handle table
7. on return: encode response/headers out; write FastCGI response
on trap: defined error → render error page with guest stack trace;
workspace state is irrelevant because…
8. drop workspace: linear memory gone (arena), handle table closed
(generalized resource cleanup), instances released
```
CPU limit: epoch/fuel interruption → same path as trap.
Memory limit: linear memory max → allocation failure / trap → same path.
---
## 8. What carries over unchanged
- The `.uce → C++` translation, parser, and page semantics.
- The lazy compile-on-first-request model and per-unit artifact caching
(different artifact format).
- The host-side connectors (sqlite/mysql) — already handle-shaped APIs; they
move behind hostcalls with the same `.uce`-visible signatures.
- 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).
---
## 9. Implementation plan
Phases are sequential; each has an exit criterion. No phase except 0 and 2's
scaffolding produces throwaway work.
**Phase 0 — toolchain & runtime spike (timeboxed).**
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,
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
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.
**Phase 2 — core module + membrane.**
Compile `uce_lib` (+ wasi-libc) to wasm as the core module; implement the
hostcall surface (§5.1) in the host; temporary scaffolding allowed: one
statically-linked unit + core to validate codegen and membrane without the
loader. Exit: one real `.uce` page (e.g. `site/tests/core.uce`) renders
correctly through the membrane. Scaffolding is marked throwaway.
**Phase 3 — the loader + workspace.**
Implement §6 in full: dylink parsing, base allocation, GOT resolution,
ABI/import verification, lazy mid-request loading, path dispatch. Per-request
workspace birth/drop (plain memcpy birth is fine here; CoW is Phase 4).
Exit: the uce-starter renders end-to-end with components loading lazily;
`tests/run_network_tests.py --match starter` passes against the wasm worker.
**Phase 4 — production mechanics.**
Core snapshot + CoW birth; bump-allocator flag; epoch/memory limits; trap →
error-page path with guest stack traces (this supersedes the
signal/longjmp machinery and closes RECOMMENDATIONS.md 1.5 structurally);
handle-table cleanup (closes 1.7/5.1); artifact/ABI versioning end-to-end.
Exit: kill-tests (deliberate null-deref page, infinite-loop page, OOM page)
produce clean error pages and an unharmed worker.
**Phase 5 — parity & performance.**
Full network suite green on the wasm worker; differential native-vs-wasm runs
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.
**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.
---
## 10. Risks & mitigations
| Risk | Mitigation |
|---|---|
| wasi-sdk PIC / shared-library maturity (least-trodden toolchain path) | Phase 0 spike before any commitment; pin toolchain versions; statically link libc into the core and export from there (avoid shared wasi-libc entirely) |
| C++ exceptions × PIC × wasm EH | Phase 0 decision point; fallback is error-code discipline at unit entry points (units already have a uniform entry shape) |
| Custom loader correctness (GOT, bases, relocation) | Small, contained (~12k lines); crib logic from Emscripten's reference loader; fuzz with adversarial modules; loader rejects > loader guesses |
| Vendored runtime patches drift from upstream | Same practice as vendored SQLite: provenance + patch files under `docs/patches/`; pin upstream tag; tests gate upgrades |
| Performance regression beyond budget | Phase 5 gates; bump allocator and placement memoization in reserve; native backend retained |
| 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
1. **Exceptions:** wasm EH or error codes at unit boundaries? (Phase 0.)
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.
4. **Cursor vs. bulk** as the default for `sqlite_query` results at the
membrane (offer both; pick the default after Phase 5 benchmarks).
5. **Streaming output:** today's ob model buffers; does the membrane expose
`uce_host_stream_write` from day one or post-MVP?
6. **Core snapshot rebuild cadence** once placement memoization lands
(dead-base reclamation policy).
## 12. Summary
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**
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
boundaries and isolation boundaries are the same lines; component calls stay
function calls; and the file stays the unit.

View File

@ -0,0 +1,34 @@
# SQLite 3.46.1 Vendor Import
Imported the official SQLite amalgamation without local source modifications.
- Version: SQLite 3.46.1
- Upstream archive: https://www.sqlite.org/2024/sqlite-amalgamation-3460100.zip
- Import date: 2026-05-29
- Vendored files:
- `src/3rdparty/sqlite/sqlite3.c`
- `src/3rdparty/sqlite/sqlite3.h`
- `src/3rdparty/sqlite/sqlite3ext.h`
## Checksums
```text
77823cb110929c2bcb0f5d48e4833b5c59a8a6e40cdea3936b99e199dbbe5784 sqlite-amalgamation-3460100.zip
6c35bc5f7f85eac9c49928bacbb02bb694b547aabf69197e058cca245ad80e83 sqlite3.c
89b62c671c5964e137409ce034941b7b05a3af2c9875aba41f47f9483d0c2515 sqlite3.h
b184dd1586d935133d37ad76fa353faf0a1021ff2fdedeedcc3498fff74bbb94 sqlite3ext.h
```
## Runtime compile flags
The amalgamation is included by `src/lib/uce_lib.cpp` with:
```cpp
#define SQLITE_THREADSAFE 1
#define SQLITE_OMIT_LOAD_EXTENSION 1
#define SQLITE_DQS 0
#define SQLITE_DEFAULT_FOREIGN_KEYS 1
#define SQLITE_DEFAULT_WAL_SYNCHRONOUS 1
```
No patch diff is needed because the vendored SQLite files are unmodified.

View File

@ -0,0 +1,147 @@
# React Developer Affordances Todo
## Objective
Add practical value for developers coming from React frameworks while preserving UCE's server-first C++ model. Defer component syntax/children work, avoid global head/assets/islands in the runtime, and focus on function-library data helpers, diagnostics, docs, examples, demos, and a starter-local router with starter-local asset/island components.
## Success Criteria
- [x] Function library has useful collection/data-shaping helpers with docs and tests.
- [x] Compile/runtime diagnostics are more helpful, especially for generated-code and common preprocessor mistakes.
- [x] Docs include a concise React/Next/Remix orientation guide.
- [x] Starter example uses a centralized hierarchical/file-based router in `index.uce` efficiently.
- [x] Starter-local asset/island affordances live as component handlers in the starter, not global runtime APIs.
- [x] Network tests and relevant build checks pass on `k-uce`.
## Current State
- Status: complete
- Last updated: 2026-05-28
- Source of truth: `/root/mount_ssh/k-uce-root-htdocs-uce`
- Runtime/live target: `k-uce:/Code/uce.openfu.com/uce`; rebuilt and restarted `uce.service` on `k-uce`.
## Goal Tree
Legend: `[ ]` not started, `[~]` in progress, `[x]` done, `[!]` blocked, `[-]` superseded
- [x] G1: Add collection/data helpers to function library
- 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.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.
- [x] G2: Improve developer diagnostics
- Why: React frameworks win by making failures easy to act on.
- Done when: compile/runtime error output includes actionable context and docs mention debugging flow.
- Verify: intentional broken page surfaces improved message.
- [x] G2.1: Inspect current compiler/runtime error rendering path.
- [x] G2.2: Add source excerpt / generated path / common-hint text where appropriate.
- [x] G2.3: Document diagnostics.
- [x] G3: Add React/Next/Remix orientation docs
- Why: mapping familiar concepts reduces onboarding cost without adding syntax.
- Done when: docs page exists and is linked from docs/README/demo surfaces.
- Verify: docs page renders live.
- [x] G4: Starter-local router and starter affordances
- Why: User specifically wants hierarchical/file-based routing beautifully in starter `index.uce`.
- Done when: starter routes go through a central router in `index.uce`, and starter-local asset/island component handlers exist and are used where sensible.
- Verify: key starter routes render 200.
- [x] G4.1: Inspect current starter routing.
- [x] G4.2: Refactor to clear route table / hierarchical file resolution in `index.uce`.
- [x] G4.3: Add starter-local `COMPONENT:asset` / `COMPONENT:island` style handlers in one unit.
- [x] G4.4: Use them efficiently in starter pages/layout.
- [x] G5: Demos and examples
- Why: Affordances must be visible to developers, not hidden in APIs.
- Done when: docs/demo/tests expose examples.
- Verify: demo URLs return 200 and tests pass.
- [x] G6: Verification and project docs
- Done when: build/test commands are run on `k-uce`, project notes updated, and adversarial review completed.
## Execution Queue
Complete.
## Decisions
- 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`.
## Assumptions
- Current source-of-truth mount is live-editable; runtime validation requires SSH to `k-uce`.
- Existing site tests are the right place for function-library coverage.
## Blockers and Risks
- No current blockers.
- Future risk: if UCE grows a full parser or component-tag syntax, keep this pass's explicit component/router APIs as a stable lower-level fallback.
## Evidence and Verification Log
- 2026-05-28: Created plan after reviewing README, preprocessor docs, and function library headers.
- 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: 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.
## Change Log
- 2026-05-28: Created initial goal tree.
- 2026-05-28: Implemented helpers, diagnostics, docs, demo, starter router, starter-local web affordance components, tests, and validation.
## Follow-up Cleanup 2026-05-29
- Removed duplicate route cleanup from `starter_router_candidates()` because `app_make_route()` is the single normalization point for `l_path`.
- Weeded out nearby duplicate/obsolete starter code:
- `starter_router_add_candidate(...)` now owns repeated candidate tree construction.
- Removed unused `app_resolve_view()` / `starter_resolve_view()` after moving routing into starter `index.uce`.
- Removed unused legacy registered asset rendering functions from `lib/app.uce`; registered assets now render through `components/theme/web_affordances.uce`.
- `app_init()` now reuses `app_base_url(context)` instead of repeating base URL derivation.
- `web_affordances.uce` now uses one `starter_render_asset_group(...)` loop for CSS and JS.
- Verification: rebuilt on `k-uce`, restarted `uce.service`, checked key starter routes, and ran `tests/run_network_tests.py --match core` successfully.
## Follow-up Routed Views 2026-05-29
- Changed starter route dispatch from `unit_render(...)` to `component(...)`.
- Converted all routed `site/examples/uce-starter/views/*.uce` files to `COMPONENT(Request& context)` rather than `RENDER(Request& context)` because they are central-router-only views.
- Updated the starter README and verified key starter routes. No service restart was required because only `.uce`/docs changed.
## Follow-up Canonical Starter URLs 2026-05-29
- Canonicalized starter self-links from `/examples/uce-starter/index.uce?...` to `/examples/uce-starter/?...` with `app_canonical_script_url(...)`.
- Updated the starter README to show canonical directory URLs.
- Touched the starter front controller to force the `#load`ed helper change into the cached generated unit.
- Verified canonical/direct starter routes and checked generated self-links. No service restart was required.
## Follow-up Not Found Component 2026-05-29
- Moved `starter_router_render_not_found` markup into `components/basic/notfound.uce`.
- Router now delegates 404 body rendering through `component("components/basic/notfound", props, context)`.
- Verified missing routes return `404` and normal dashboard route returns `200`. No service restart required.
## Follow-up Page Shell Component 2026-05-29
- Moved app page rendering into `themes/page.uce` as a component.
- Removed `app_render_page`, `app_theme_page_component`, and `starter_render_page` from `lib/app.uce`.
- Page template resolution now follows `context.call["app"]["page_type"]`: current theme first, common fallback second.
- Verified representative HTML routes and the JSON page-type fallback. No service restart required.
## Follow-up Deep Starter Context Cleanup 2026-05-29
- Removed repeated `starter_boot(context)` calls; root `index.uce` is the boot point.
- Removed `context.call["starter"]` duplicated state and JSON side-channel state.
- JSON routes now use only `context.call["app"]["page_type"]` plus normal captured output.
- Simplified `themes/page.uce` and `themes/common/page.json.uce` accordingly.
- Replaced `starter_*` alias helper usage with direct `app_*` helpers and removed alias wrappers except `StarterUser`.
- Verified key routes and core tests. No service restart required.
## Follow-up Route Context Flattening 2026-05-29
- Flattened `context.call["app"]["route"]` to `context.call["route"]`.
- Moved former `context.call["app"]["router"]` metadata into `context.call["route"]`.
- Verified representative starter HTML, 404, and JSON routes. No service restart required.

View File

@ -17,8 +17,19 @@ FLAGS="-g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math"
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs`"
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\""
echo "Compliling executable..."
time -p $COMPILER src/linux_fastcgi.cpp $SRCFLAGS $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1
echo "Compiling SQLite..."
clang -g -O2 -fPIC \
-DSQLITE_THREADSAFE=1 \
-DSQLITE_OMIT_LOAD_EXTENSION=1 \
-DSQLITE_DQS=0 \
-DSQLITE_DEFAULT_FOREIGN_KEYS=1 \
-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 \
-c src/3rdparty/sqlite/sqlite3.c -o bin/sqlite3.o 2>&1
if [ $? -ne 0 ]; then exit 1; fi
echo "Compiling executable..."
time -p $COMPILER src/linux_fastcgi.cpp bin/sqlite3.o $SRCFLAGS $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1
if [ $? -eq 0 ]
then

41
site/demo/collections.uce Normal file
View File

@ -0,0 +1,41 @@
#include "demo_guard.h"
RENDER(Request& context)
{
DTree p;
p.set(context.params);
StringList routes = {"index", "dashboard", "themes", "dashboard", "workspace/projects"};
StringList unique_routes = list_unique(routes);
StringList sorted_routes = list_sort(unique_routes);
StringList labels = map(sorted_routes, [](String route) { return(to_upper(replace(route, "/", " / "))); });
DTree cards;
DTree 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()); });
<><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
</head>
<body>
<h1><a href="index.uce">UCE Test Suite</a><a class="docs-link" href="../doc/index.uce?p=map">Collection Docs &rarr;</a></h1>
<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>Grouped cards</h3><pre><?= json_encode(grouped) ?></pre></div>
<details>
<summary>Request Parameters</summary>
<pre><?= var_dump(p) ?></pre>
</details>
</body>
</html></>
}

View File

@ -42,6 +42,7 @@ RENDER(Request& context)
<? 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"); ?>
<? render_card("collections.uce", "Collection Helpers", "Map, filter, group, pick, and omit render data"); ?>
<? render_card("str_replace.uce", "String Replace", "Search and replace in strings"); ?>
<? render_card("utf8.uce", "UTF-8", "Unicode string handling"); ?>
<? render_card("random.uce", "RNG / Noise", "Random generation and noise"); ?>
@ -60,6 +61,7 @@ RENDER(Request& context)
<? if(allow_server_demos) { render_card("file_append.uce", "File Append", "Append data to files"); } ?>
<? if(allow_server_demos) { render_card("shell.uce", "Shell", "Execute shell commands"); } ?>
<? if(allow_server_demos) { render_card("memcached.uce", "Memcached", "Memcached key-value store"); } ?>
<? if(allow_server_demos) { render_card("sqlite.uce", "SQLite", "Embedded SQLite database connector"); } ?>
<? if(allow_server_demos) { render_card("mysql.uce", "MySQL", "MySQL database connector"); } ?>
<div class="grid-heading">Advanced</div>

54
site/demo/sqlite.uce Normal file
View File

@ -0,0 +1,54 @@
#include "demo_guard.h"
RENDER(Request& context)
{
if(!test_demo_request_allowed(context))
{
test_demo_render_restricted_html(context, "SQLite demo", "write to a server-side SQLite database");
return;
}
String db_path = "/tmp/uce-demo-sqlite.sqlite";
SQLite* db = sqlite_connect(db_path);
sqlite_query(db, "create table if not exists notes(id integer primary key autoincrement, body text not null, created_at text not null)");
if(context.params["REQUEST_METHOD"] == "POST" && trim(context.post["body"]) != "")
{
StringMap params;
params["body"] = trim(context.post["body"]);
params["created_at"] = time_format_utc("%Y-%m-%d %H:%M:%S");
sqlite_query(db, "insert into notes(body, created_at) values(:body, :created_at)", params);
sqlite_disconnect(db);
redirect("sqlite.uce", 303);
return;
}
DTree notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10");
String error = sqlite_error(db);
?><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
<link rel="stylesheet" href="style.css?v=<?= time() ?>"></link>
</head>
<body>
<h1><a href="index.uce">UCE Demo</a> / SQLite</h1>
<div class="system-info">
<p>This demo stores rows in <code><?= db_path ?></code> with runtime SQLite helpers and named prepared parameters.</p>
<form method="post">
<input name="body" placeholder="note text"></input>
<button type="submit">Add note</button>
</form>
<? if(error != "ok" && error != "connected") { ?><pre><?= error ?></pre><? } ?>
</div>
<div class="test-grid">
<? notes.each([&](DTree note, String key) { ?>
<div class="test-card">
<strong>#<?= note["id"].to_string() ?> <?= note["created_at"].to_string() ?></strong>
<span><?= note["body"].to_string() ?></span>
</div>
<? }); ?>
</div>
</body>
</html><?
sqlite_disconnect(db);
}

View File

@ -1,5 +1,6 @@
Output / Invocation Functions
coming_from_react
1_RENDER
1_CLI
cli_input

View File

@ -0,0 +1,8 @@
SQLite
sqlite_connect
sqlite_disconnect
sqlite_error
sqlite_query
sqlite_insert_id
sqlite_affected_rows

View File

@ -7,6 +7,12 @@ filter
first
join
json_encode
list_every
list_find
map
list_some
list_sort
list_unique
nibble
print
strpos

View File

@ -3,6 +3,13 @@ Types
0_Request
array_merge
0_DTree
dtree_filter
dtree_group_by
dtree_keys
dtree_map
dtree_omit
dtree_pick
dtree_values
get_by_path
set_status
String

View File

@ -2,6 +2,14 @@ URI Functions
encode_query
redirect
request_context_params
request_base_url
request_query_path
request_query_route
request_script_url
route_path_is_safe
route_path_normalize
route_path_sanitize
session_id_create
parse_query
uri_decode

View File

@ -2,61 +2,436 @@
Request
:sig
Request& context;
Request& context
:see
>types
request_context_params
request_script_url
request_base_url
request_query_path
request_query_route
set_status
component
component_render
unit_render
ws_message
unit_call
session_start
set_cookie
parse_query
parse_multipart
ws_message
ws_connection_id
ws_connections
ws_send
0_DTree
StringMap
UploadedFile
:content
`Request& context` is the request-local state object passed into UCE handlers. It carries incoming request data, response state, runtime metadata, and helper trees such as `context.cfg`, `context.props`, and `context.connection`.
`Request& context` is the request-local state object passed into every UCE handler:
## Core Fields
```cpp
RENDER(Request& context) { ... }
COMPONENT(Request& context) { ... }
WS(Request& context) { ... }
CLI(Request& context) { ... }
SERVE_HTTP(Request* req) { ... }
```
- `ServerState* server`: current server state
- `StringMap params`: FastCGI server parameters
- `StringMap get`: current request GET variables
- `StringMap post`: current request POST variables
- `StringMap cookies`: cookies sent by the browser
- `StringMap session`: current session data
- `String session_id`: session cookie ID
- `String session_name`: session cookie name
- `DTree cfg`: request-local configuration tree
- `DTree call`: invocation-local structured data for helpers such as component and unit calls
- `DTree connection`: broker-owned WebSocket connection state that persists across `WS(Request& context)` calls for the same socket
- `std::vector<UploadedFile> uploaded_files`: files uploaded in the current request
- `StringMap header`: headers to send back to the browser
- `StringList set_cookies`: cookies queued for the response
- `u64 random_seed`: current request noise seed
- `u64 random_index`: current request noise index position
It is the main bridge between the runtime and page code. It contains incoming request data, response state, output buffers, per-request scratch trees, session/cookie state, WebSocket metadata, and runtime diagnostics.
## Response Control
## Handler Lifetime
`context.set_status(s32 code[, String reason])` sets the HTTP status line and updates `context.flags.status`. When `reason` is omitted, UCE uses a built-in standard reason phrase for common status codes.
A fresh `Request` is created for each HTTP/CLI/custom-server request. Component and unit calls normally share that same object, so state placed on `context.call`, `context.header`, `context.session`, or `context.cfg` is visible to later components in the same request.
## Flags And Stats
For WebSockets, each incoming message is delivered as its own `Request`, but `context.connection` points at broker-owned per-socket state that persists for the lifetime of that WebSocket connection.
- `bool flags.log_request`: controls whether the request should be logged
- `u32 stats.bytes_written`
- `f64 stats.time_init`
- `f64 stats.time_start`
- `f64 stats.time_end`
`ONCE(Request& context)` hooks run once per request, per resolved unit file, before the first `RENDER`, `COMPONENT`, `CLI`, or matching entrypoint from that unit.
## Common Usage Notes
## Incoming Request Maps
- `context.cfg` is the usual place for structured configuration. Use `context.cfg.get_by_path("path/to/value")` for deep reads.
- `context.props` carries invocation data for component calls and unit calls.
- `context.in` carries the current request body, and for `WS(Request& context)` it is the current WebSocket message payload.
- `context.params["WS_..."]` exposes the current WebSocket message metadata directly on the request parameter map.
- `context.connection` is only meaningful for WebSocket traffic and persists for the lifetime of the connection.
### `context.params` — server/runtime parameters
`unit_render(String file_name, [Request& context])` invokes another UCE file using the current or supplied request context.
Type: `StringMap`
This is the low-level parameter map from FastCGI/direct HTTP plus UCE-populated convenience fields. It is closest to PHP `$_SERVER`.
Common CGI/FastCGI-style keys include:
- `REQUEST_METHOD`: `GET`, `POST`, etc.
- `REQUEST_URI`: raw request URI where available
- `DOCUMENT_URI`: normalized request path where available
- `SCRIPT_NAME`: script path where available
- `SCRIPT_FILENAME`: resolved filesystem path of the active UCE unit
- `QUERY_STRING`: raw query string
- `DOCUMENT_ROOT`: web root used by the frontend/backend
- `CONTENT_TYPE`: request body content type
- `CONTENT_LENGTH`: request body length
- `HTTP_COOKIE`: raw cookie header
- `HTTP_HOST`, `HTTP_USER_AGENT`, `HTTP_ACCEPT`, and other `HTTP_...` headers supplied by the frontend
UCE also populates convenience route/link fields before handlers run:
- `SCRIPT_URL`: canonical script URL; `/index.uce` is collapsed to the containing directory URL
- `BASE_URL`: canonical directory URL for the script
- `ROUTE_PATH`: sanitized first keyless query-string segment, defaulting to `index` when no route was supplied; empty when unsafe input was rejected
- `ROUTE_PAGE`: first segment of `ROUTE_PATH`
- `ROUTE_PATH_RAW`: normalized but untrusted route input, for diagnostics only
- `ROUTE_VALID`: `1` when route input is safe, `0` when the supplied route was rejected
See `request_context_params`, `request_script_url`, `request_base_url`, `request_query_path`, `request_query_route`, and `route_path_sanitize`.
### `context.get`
Type: `StringMap`
Parsed query-string key/value parameters. This is populated from `context.params["QUERY_STRING"]` with `parse_query()`.
```cpp
String theme = first(context.get["theme"], "default");
```
For front-controller route URLs such as `/?dashboard&theme=dark`, the keyless `dashboard` segment is represented by sanitized `ROUTE_PATH`; named parameters such as `theme=dark` are available in `context.get`.
### `context.post`
Type: `StringMap`
Parsed request body parameters for ordinary `POST` requests. URL-encoded bodies are parsed with `parse_query()`. Multipart form data is parsed with `parse_multipart()` and uploaded files are listed in `context.uploaded_files`.
```cpp
if(context.params["REQUEST_METHOD"] == "POST")
String email = context.post["email"];
```
### `context.cookies`
Type: `StringMap`
Cookies sent by the client, parsed from `HTTP_COOKIE`. `set_cookie()` also updates this map after queuing a response cookie, so later code in the same request can observe the new value.
### `context.in`
Type: `String`
Raw request body. For WebSocket handlers, this is the current message payload.
Use this for JSON APIs:
```cpp
DTree body = json_decode(context.in);
```
### `context.uploaded_files`
Type: `std::vector<UploadedFile>`
Each `UploadedFile` contains:
- `file_name`: original submitted filename
- `tmp_name`: temporary server-side upload path
- `size`: uploaded byte count
Use this with multipart form posts.
## Session State
### `context.session`
Type: `StringMap`
Session data loaded by `session_start()`. UCE does not load sessions automatically for every request; call `session_start()` before reading/writing session data.
```cpp
session_start();
context.session["user_id"] = "42";
```
At the end of a successful request, modified session data is saved automatically if a session is active.
### `context.session_id` and `context.session_name`
The active session ID and cookie name after `session_start()`.
Related helpers:
- `session_start()`
- `session_destroy()`
- `session_id_create()`
- `set_cookie()`
## Per-request Structured Trees
### `context.call`
Type: `DTree`
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.
Examples from front-controller style apps:
```cpp
context.call["route"] = request_query_route(context);
context.call["app"]["page_type"] = "html";
context.call["fragments"]["main"] = captured_html;
```
Prefer clear top-level names when state is app-wide (`route`, `fragments`) and nested app names only when the state is truly owned by that app (`app/page_title`, `app/page_type`).
### `context.cfg`
Type: `DTree`
Request-local structured configuration. The runtime does not fill this with application config by default; application code may assign it during boot/setup:
```cpp
context.cfg = get_config();
```
This is separate from `context.server->config`, which is the runtime/server string config from `/etc/uce/settings.cfg`.
Use `get_by_path()` for non-mutating deep reads:
```cpp
String site_name = context.cfg.get_by_path("site/name").to_string();
```
### `context.props`
Type: `DTree`
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;
props["title"] = "Dashboard";
print(component("components/card", props, context));
```
### `context.connection`
Type: `DTree`
WebSocket connection-local state. Mutations persist across `WS(Request& context)` calls for the same socket.
```cpp
context.connection["message_count"] = context.connection["message_count"].to_u64() + 1;
```
Only meaningful for WebSocket handlers.
## Response State
### `context.response_code`
Type: `String`
The raw status line. Usually use `context.set_status(...)` instead of writing this directly.
### `context.header`
Type: `StringMap`
Response headers to emit. Header names are case-sensitive as written.
```cpp
context.header["Content-Type"] = "application/json";
context.header["Location"] = "/info/";
```
### `context.set_cookies`
Type: `StringList`
Queued `Set-Cookie` header lines. Prefer `set_cookie()` instead of editing this directly.
### `context.set_status(code[, reason])`
Sets the HTTP response status and `context.flags.status`.
```cpp
context.set_status(404, "Not Found");
context.set_status(302, "Found");
context.header["Location"] = app_link("dashboard", context);
```
Related helpers:
- `redirect(url[, code])`
- `set_cookie(...)`
## Output Buffers
### `context.ob_stack` and `context.ob`
Internal output-buffer stack. Most code should use helpers instead of touching these directly:
- `print(...)`
- `out(...)`
- `ob_start()`
- `ob_get()`
- `ob_get_close()`
- `ob_close()`
Common capture pattern:
```cpp
ob_start();
print(component("views/dashboard", context));
String html = ob_get_close();
context.call["fragments"]["main"] = html;
```
### `context.out` and `context.err`
Runtime output/error artifacts used by some transports and failure paths. Normal page rendering should use `print()` / output buffers.
## Request Flags
`context.flags` contains runtime booleans and the numeric status:
- `log_request`: whether the request should be logged
- `is_finished`: internal completion marker
- `status`: numeric HTTP status set by `set_status()`
- `output_closed`: internal transport state
- `params_closed`: internal transport state
- `input_closed`: internal transport state
Most page code only reads `flags.status`, if anything.
## Request Stats
`context.stats` contains counters/timing for the request:
- `bytes_written`
- `time_init`
- `time_start`
- `time_end`
- `mem_high`
- `mem_alloc`
- `invoke_count`
These are useful for diagnostics, demos, and runtime instrumentation.
## Random / Noise State
- `random_seed`
- `random_index`
Used by UCE noise/random helpers to provide request-local deterministic progression.
Related helpers include functions in the noise/hash area such as `gen_int`, `gen_float`, `gen_noise64`, and `gen_sha1`.
## WebSocket Fields
In `WS(Request& context)`, the runtime mirrors WebSocket metadata into `context.params` and `context.resources`.
Convenience `context.params` keys include:
- `WS_MESSAGE`
- `WS_CONNECTION_ID`
- `WS_SCOPE`
- `WS_CONNECTION_COUNT`
- `WS_OPCODE`
- `WS_MESSAGE_TYPE`
- `WS_DOCUMENT_URI`
Prefer WebSocket helper functions where possible:
- `ws_message()`
- `ws_connection_id()`
- `ws_scope()`
- `ws_opcode()`
- `ws_is_binary()`
- `ws_connections()`
- `ws_connection_count()`
- `ws_send()`
- `ws_send_to()`
- `ws_close()`
Use `context.connection` for per-socket structured state.
## Runtime / Resource Fields
### `context.server`
Pointer to server state. Useful mainly for low-level/runtime code. Runtime config lives at:
```cpp
context.server->config["KEY"]
```
This is a `StringMap`, separate from app-owned `context.cfg`.
### `context.resources`
Internal runtime resources and transport state. Includes sockets, MySQL handles, WebSocket state, current unit file, and parser buffers. Application code should normally use public helpers instead of editing this directly.
Notable fields:
- `is_websocket`
- `is_cli`
- `websocket_connection_id`
- `websocket_scope`
- `websocket_scope_connection_ids`
- `current_unit_file`
## Common Patterns
### Minimal page
```cpp
RENDER(Request& context)
{
<><h1>Hello <?= context.get["name"] ?></h1></>
}
```
### JSON endpoint
```cpp
RENDER(Request& context)
{
context.header["Content-Type"] = "application/json";
DTree response;
response["ok"].set_bool(true);
print(json_encode(response));
}
```
### Redirect
```cpp
RENDER(Request& context)
{
context.set_status(302, "Found");
context.header["Location"] = "/info/";
}
```
### Component props
```cpp
DTree props;
props["title"] = "Welcome";
print(component("components/card", props, context));
```
### Front-controller route
```cpp
context.call["route"] = request_query_route(context);
String route_path = context.call["route"]["l_path"].to_string();
```
Or use runtime-populated params directly:
```cpp
String route_path = context.params["ROUTE_PATH"];
```
## Related Concepts
- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, and `http_response_code()`
- JavaScript / Node.js: Express `req` and `res`, Fetch `Request`, `Headers`, cookies or session middleware, and per-connection state in WebSocket handlers
- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, output buffering, and `http_response_code()`
- JavaScript / Node.js: Express `req`/`res`, Fetch `Request`/`Response`, route params, middleware-populated locals, and per-socket WebSocket state

View File

@ -26,6 +26,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- Inside a literal block, `<?: expression ?>` emits `print(expression);` without HTML escaping.
- `#load "other.uce"` injects another UCE unit at compile time.
- `RENDER(Request& context)`, `COMPONENT(Request& context)`, `CLI(Request& context)`, `ONCE(Request& context)`, `INIT(Request& context)`, and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`.
- `ONCE`, `RENDER`, and `COMPONENT` may be followed by a preprocessor attribute line such as `@fragment head` before the opening `{`. The handler's output is then captured and appended to `context.call["fragments"]["head"]` instead of being emitted at the call site. `ONCE` defaults to `@fragment once` when no fragment is specified.
- `COMPONENT:NAME(Request& context)` is rewritten by the custom pass into an exported named component handler.
- `EXPORT` is also a normal C++ macro, but the custom pass additionally records exported declarations for metadata.
@ -42,6 +43,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- `<?: ... ?>` becomes `print(...);` and is intended for trusted markup or already-escaped content.
- `#load "file.uce"` is replaced with a generated C++ `#include` that points at the loaded unit's preprocessed `.cpp` file under `BIN_DIRECTORY`.
- Lines beginning with `EXPORT` are scanned so their declarations can be written to a sibling `.exports.txt` file.
- `@fragment slot-name` lines immediately following `ONCE`, `RENDER`, or `COMPONENT` are removed and replaced with an output-capture guard at the start of the handler body.
- Lines beginning with `RENDER:NAME(...)` are rewritten into exported `__uce_render_NAME(...)` functions.
- Lines beginning with `COMPONENT:NAME(...)` are rewritten into exported `__uce_component_NAME(...)` functions for the component helpers.
- The final generated source is written to `BIN_DIRECTORY + src_path + "/" + source_file + ".cpp"`.
@ -130,6 +132,18 @@ ONCE(Request& context)
}
```
One-time page assets captured for a template-controlled slot:
```cpp
ONCE(Request& context)
@fragment head
{
?><link rel="stylesheet" href="/assets/page.css" /><?
}
```
The page template can then render `context.call["fragments"]["head"]` inside `<head>`.
## Rules
- Literal mode can start on either `<>` or `?>`.
@ -155,6 +169,8 @@ ONCE(Request& context)
- Inspect the generated file under `BIN_DIRECTORY` first. That file shows the exact C++ produced by the UCE preprocessor.
- Compiler errors usually point back to the `.uce` source because the preprocessor inserts `#line 1`, but the generated `.cpp` is still the best place to inspect expansion problems.
- Compile failures are reported with the source path, generated C++ path, compile-output artifact path, an excerpt when UCE can identify a line, and the raw compiler output from the configured compile script.
- Runtime request failures include the request/script path, generated C++ path, a hint about inspecting template delimiters and recent component/unit calls, and a native trace when available.
- If a `#load` include looks wrong, check the current file's directory, the configured `BIN_DIRECTORY`, and whether the loaded page already produced its own generated `.cpp`.
## Related Concepts

View File

@ -0,0 +1,69 @@
:title
Coming from React, Next, or Remix
:sig
UCE orientation for React-framework developers
:see
1_RENDER
1_COMPONENT
component
unit_render
3_C++ Preprocessor
map
filter
dtree_filter
:content
UCE is server-first C++ with a small template preprocessor. It does not try to be React, but several concepts map cleanly.
## Concept Map
- `RENDER(Request& context)` is the page/server-render entrypoint.
- `COMPONENT(Request& context)` and `COMPONENT:NAME(Request& context)` are server-rendered components.
- `context.props` is the component invocation payload, similar to props.
- `context.call` is request-local scratch state shared across units during one request.
- `context.cfg` is structured app/config data.
- `ONCE(Request& context)` is per-request setup for a unit before its first render/component entry.
- `INIT(Request& context)` is worker-local setup when a unit is loaded.
- `<?= expression ?>` is escaped interpolation; prefer it for user-visible text.
- `<?: expression ?>` is trusted raw markup output, closer to a deliberate `dangerouslySetInnerHTML` decision.
- `unit_render()` renders another page unit; `component()` returns component HTML as a string.
## Routes and Layouts
UCE does not require a framework-level router. A front controller can keep routing explicit and app-local. The starter example demonstrates this in `site/examples/uce-starter/index.uce`: it resolves a request path by checking:
1. `views/<path>.uce`
2. `views/<path>/index.uce`
3. parent index handlers such as `views/workspace/index.uce` with the last segment as a route parameter
That keeps file-based and hierarchical routing in normal UCE code instead of hiding it in the runtime.
## Data Shaping Near Render Code
The function library includes small collection helpers for common route/menu/card transformations:
```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()); });
```
Use these when the transformation communicates intent. Prefer explicit loops when side effects or multi-step validation are the main concern.
## Assets and Islands
Global runtime APIs for assets and islands are intentionally not part of UCE core. The starter emits CSS and JavaScript from the owning unit's `ONCE(Request& context)` hook, with a few shared sibling asset components when multiple components need the same files. The only starter web-affordance helper left is `COMPONENT:island` in `components/theme/web_affordances.uce` for small progressive-enhancement modules. This keeps app policy in the app without an asset registry layer.
## Debugging
When a unit fails to compile, UCE reports the source path, generated C++ path, compile-output artifact, a source/generated excerpt when it can identify a line, and the raw compiler output. The generated C++ under `BIN_DIRECTORY` is the source of truth for what the configured compiler actually saw.
## What Not To Expect
- No client-side virtual DOM is built into UCE.
- No global file-router is imposed by the runtime.
- No JSX-like component tags are required for this workflow.
- Component children/slot syntax is intentionally deferred; use explicit props and component calls for now.

View File

@ -0,0 +1,26 @@
:title
dtree_filter
:sig
DTree dtree_filter(DTree tree, function<bool (DTree, String)> f)
:see
StringList
0_DTree
filter
:content
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); });
```
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.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

View File

@ -0,0 +1,26 @@
:title
dtree_group_by
:sig
DTree dtree_group_by(DTree tree, function<String (DTree, String)> f)
:see
StringList
0_DTree
filter
:content
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()); });
```
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.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

View File

@ -0,0 +1,26 @@
:title
dtree_keys
:sig
StringList dtree_keys(DTree tree)
:see
StringList
0_DTree
filter
:content
Returns map keys from a DTree. 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
StringList keys = dtree_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.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

View File

@ -0,0 +1,26 @@
:title
dtree_map
:sig
DTree dtree_map(DTree tree, function<DTree (DTree, String)> f)
:see
StringList
0_DTree
filter
:content
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); });
```
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.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

View File

@ -0,0 +1,26 @@
:title
dtree_omit
:sig
DTree dtree_omit(DTree tree, StringList keys)
:see
StringList
0_DTree
filter
:content
Copies a DTree 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"});
```
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.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

View File

@ -0,0 +1,26 @@
:title
dtree_pick
:sig
DTree dtree_pick(DTree tree, StringList keys)
:see
StringList
0_DTree
filter
:content
Copies only selected keys from a DTree 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
DTree public_user = dtree_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.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

View File

@ -0,0 +1,26 @@
:title
dtree_values
:sig
DTree dtree_values(DTree tree)
:see
StringList
0_DTree
filter
:content
Returns child values as a list-like DTree.
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"]);
```
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.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

View File

@ -0,0 +1,26 @@
:title
list_every
:sig
bool list_every(StringList items, function<bool (String)> f)
:see
StringList
0_DTree
filter
:content
Returns true when every item matches.
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
bool all_named = list_every(routes, [](String s) { return(s != ""); });
```
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.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

View File

@ -0,0 +1,18 @@
:title
list_filter
:sig
Use filter(StringList items, function<bool (String)> f)
:see
filter
StringList
:content
`list_filter()` was removed because `StringList` is `std::vector<String>` and the generic `filter()` helper covers the same behavior without a second implementation to maintain.
Use:
```cpp
auto visible = filter(routes, [](String s) { return(s != "admin"); });
```

View File

@ -0,0 +1,26 @@
:title
list_find
:sig
String list_find(StringList items, function<bool (String)> f, String fallback = "")
:see
StringList
0_DTree
filter
:content
Returns the first matching item or fallback.
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
String route = list_find(routes, [](String s) { return(str_starts_with(s, "dashboard")); }, "index");
```
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.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

View File

@ -0,0 +1,19 @@
:title
list_map
:sig
Use map(StringList items, function<String (String)> f)
:see
map
filter
StringList
:content
`list_map()` was removed because `StringList` is `std::vector<String>` and the generic `map()` helper covers the same behavior without a second implementation to maintain.
Use:
```cpp
auto upper = map(names, [](String s) { return(to_upper(s)); });
```

View File

@ -0,0 +1,26 @@
:title
list_some
:sig
bool list_some(StringList items, function<bool (String)> f)
:see
StringList
0_DTree
filter
:content
Returns true when any item matches.
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
bool has_dashboard = list_some(routes, [](String s) { return(s == "dashboard"); });
```
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.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

View File

@ -0,0 +1,26 @@
:title
list_sort
:sig
StringList list_sort(StringList items)
:see
StringList
0_DTree
filter
:content
Returns a sorted copy of the 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
auto sorted = list_sort(tags);
```
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.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

View File

@ -0,0 +1,26 @@
:title
list_unique
:sig
StringList list_unique(StringList items)
:see
StringList
0_DTree
filter
:content
Returns the first occurrence of each string, preserving input order.
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
auto tags = list_unique({"uce", "docs", "uce"});
```
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.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

31
site/doc/pages/map.txt Normal file
View File

@ -0,0 +1,31 @@
:title
map
:sig
StringList map(StringList items, function<String (String)> f)
vector<R> map(vector<T> items, function<R (T)> f)
:params
items : list of items to transform
f : a function that returns the transformed value for each item
return value : a new list containing the transformed values
:see
>string
filter
StringList
0_DTree
:content
Returns a new list by calling `f` for each item in `items`.
```cpp
auto upper = map(names, [](String s) { return(to_upper(s)); });
```
Use `filter()` when you want to keep only matching items, and `map()` when you want to transform each item.
## Related Concepts
- JavaScript / React: `Array.prototype.map()`
- PHP: `array_map()`

View File

@ -13,7 +13,16 @@ return value : a list of rows returned from executing the query
:content
Executes a MySQL query and returns the resulting data, if any.
`params` provides the query parameter values used by the statement.
`params` provides the query parameter values used by the statement. Use named `:name` placeholders only; positional `?` placeholders are rejected.
```cpp
StringMap params;
params["email"] = "ada@example.test";
DTree 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.

View File

@ -7,10 +7,31 @@ return value : a StringMap containing the parameters
:see
>uri
encode_query
request_context_params
:content
Decodes a query-string fragment such as `a=b&c=d` into a `StringMap`.
Parsing rules:
- pairs are separated on `&`
- each pair is split on the first raw `=` only
- both key and value are URL-decoded
- keyless flags such as `preview` are present with an empty string value
- empty pairs, including a trailing `&`, are ignored
- repeated keys use the last value seen
Examples:
```cpp
StringMap q = parse_query("alpha=1&token=a%3Db&preview&empty=");
// q["alpha"] == "1"
// q["token"] == "a=b"
// q["preview"] == ""
// q["empty"] == ""
```
This is useful when you need to work with URL parameter data outside the normal request parsing flow.
Related:

View File

@ -0,0 +1,16 @@
:title
request_base_url
:sig
String request_base_url(Request& context)
:see
request_script_url
request_query_route
:content
Returns the canonical directory base URL for the current script. This is useful for front-controller apps that generate links to sibling assets or query-routed pages.
```cpp
String base_url = request_base_url(context);
```

View File

@ -0,0 +1,28 @@
:title
Request Context Params
:sig
SCRIPT_URL, BASE_URL, ROUTE_PATH, ROUTE_PAGE, ROUTE_PATH_RAW, ROUTE_VALID
:see
request_script_url
request_base_url
request_query_path
request_query_route
route_path_sanitize
route_path_is_safe
0_Request
:content
UCE populates several convenience request parameters before invoking page, component, CLI, custom HTTP, or WebSocket handlers:
- `context.params["SCRIPT_URL"]`: canonical script URL, with `/index.uce` collapsed to the containing directory URL
- `context.params["BASE_URL"]`: canonical directory URL for the script
- `context.params["ROUTE_PATH"]`: sanitized first keyless query-string segment, defaulting to `index` when no route was supplied
- `context.params["ROUTE_PAGE"]`: first segment of `ROUTE_PATH`
- `context.params["ROUTE_PATH_RAW"]`: normalized but not trusted route input, for diagnostics only
- `context.params["ROUTE_VALID"]`: `1` when the supplied/defaulted route is safe, `0` when the supplied route was rejected
`ROUTE_PATH` is safe to compose under an application-controlled route root. Unsafe route input such as `..`, `.`, empty interior segments, backslashes, dots in filenames, or other non route-segment characters is rejected by the runtime and yields an empty `ROUTE_PATH` with `ROUTE_VALID=0`.
These are useful for front-controller apps that use URLs like `/?dashboard` or `/?workspace/projects` while still accepting ordinary named query parameters.

View File

@ -0,0 +1,27 @@
:title
request_query_path
:sig
String request_query_path(Request& context, String default_path = "index")
:see
request_query_route
request_context_params
route_path_sanitize
route_path_is_safe
parse_query
:content
Returns the first keyless query-string segment as a sanitized route path.
For a request such as `/?workspace/projects&theme=dark`, this returns `workspace/projects`.
When no keyless route is supplied, the result is `default_path`. When unsafe route input is supplied, the result is an empty string. Unsafe route input includes `.` or `..` segments, empty interior segments, backslashes, dots in filenames, or any character outside ASCII letters, digits, `_`, and `-`.
```cpp
String route_path = request_query_path(context);
if(route_path == "")
context.set_status(404, "Not Found");
```
The runtime-populated `context.params["ROUTE_PATH"]` uses the same sanitizer.

View File

@ -0,0 +1,29 @@
:title
request_query_route
:sig
DTree request_query_route(Request& context, String default_path = "index")
:see
request_query_path
request_context_params
route_path_sanitize
request_script_url
:content
Builds a small route tree from the first keyless query-string segment.
Fields:
- `raw_path`: normalized but untrusted route input, for diagnostics only
- `l_path`: sanitized full route path, or empty string when input was rejected
- `page`: first path segment of `l_path`, or empty string when input was rejected
- `valid`: boolean; true when `l_path` is safe
```cpp
DTree route = request_query_route(context);
if(route["valid"].to_bool())
print("route=", route["l_path"].to_string());
```
This supports front-controller apps that use URLs such as `/?dashboard` or `/?workspace/projects` while still allowing ordinary named query parameters alongside the route. The returned `l_path` is already sanitized for composing under an application-controlled route root.

View File

@ -0,0 +1,16 @@
:title
request_script_url
:sig
String request_script_url(Request& context)
:see
request_base_url
request_query_route
:content
Returns the request script URL from `DOCUMENT_URI` / `SCRIPT_NAME`, canonicalizing a front-controller URL ending in `/index.uce` to the containing directory URL.
```cpp
String script_url = request_script_url(context);
```

View File

@ -0,0 +1,24 @@
:title
route_path_is_safe
:sig
bool route_path_is_safe(String path)
:see
route_path_sanitize
route_path_normalize
request_query_path
request_query_route
:content
Returns whether a normalized route path is safe to use as a route-derived file path segment.
A safe route path contains only non-empty segments made from ASCII letters, digits, `_`, and `-`. The segments `.` and `..` are rejected.
```cpp
route_path_is_safe("workspace/projects"); // true
route_path_is_safe("workspace/../admin"); // false
route_path_is_safe("view.uce"); // false
```
Most request code should use runtime-populated `context.params["ROUTE_PATH"]` or `request_query_route()` instead of calling this directly.

View File

@ -0,0 +1,20 @@
:title
route_path_normalize
:sig
String route_path_normalize(String path)
:see
route_path_sanitize
route_path_is_safe
request_query_path
request_query_route
:content
Trims whitespace and removes leading/trailing `/` from a route path without deciding whether the path is safe.
```cpp
route_path_normalize(" /workspace/projects/ "); // "workspace/projects"
```
For route data that may be used to compose file paths, prefer `route_path_sanitize()` or runtime-populated `context.params["ROUTE_PATH"]`.

View File

@ -0,0 +1,34 @@
:title
route_path_sanitize
:sig
String route_path_sanitize(String path, String default_path = "index")
:see
route_path_is_safe
route_path_normalize
request_query_path
request_query_route
request_context_params
:content
Normalizes and validates a route path for file-backed routing.
Rules:
- leading and trailing `/` are removed
- an empty path becomes `default_path`
- every path segment must be non-empty
- `.` and `..` segments are rejected
- only ASCII letters, digits, `_`, and `-` are accepted inside a segment
- unsafe input returns an empty string
Use this instead of manually checking app routes before composing file paths.
```cpp
route_path_sanitize("/workspace/projects/"); // "workspace/projects"
route_path_sanitize("../secret"); // ""
route_path_sanitize(""); // "index"
```
`request_query_path()`, `request_query_route()`, and the runtime-populated `ROUTE_PATH` params already use this sanitizer.

View File

@ -0,0 +1,19 @@
:sig
u32 sqlite_affected_rows(SQLite* db)
:params
db : pointer to an active SQLite connection
return value : number of rows changed by the most recent statement
:see
>sqlite
sqlite_query
sqlite_insert_id
:content
Returns the number of rows changed by the most recent insert, update, or delete statement on the connection.
```cpp
sqlite_query(db, "update users set visits = visits + 1 where id = :id", params);
print(sqlite_affected_rows(db));
```

View File

@ -0,0 +1,28 @@
:sig
SQLite* sqlite_connect(String path)
:params
path : filesystem path to the SQLite database file
return value : pointer to a SQLite connection struct
:see
>sqlite
sqlite_query
sqlite_error
sqlite_disconnect
:content
Opens an SQLite database file and returns a connection handle.
UCE opens SQLite with serialized/full-mutex connection mode, sets a busy timeout, enables foreign keys, and applies WAL-oriented defaults:
```sql
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
```
SQLite itself handles file locking and one-writer/many-reader concurrency. UCE does not add a separate app-level database lock.
Connections are registered for request cleanup, but explicit `sqlite_disconnect()` is still preferred when you are done with the handle.

View File

@ -0,0 +1,14 @@
:sig
void sqlite_disconnect(SQLite* db)
:params
db : pointer to an active SQLite connection
:see
>sqlite
sqlite_connect
:content
Closes an SQLite connection and deletes the UCE connection wrapper.
UCE also cleans up SQLite connections that remain open at request end, but explicit disconnects keep resource lifetime local and clear.

View File

@ -0,0 +1,15 @@
:sig
String sqlite_error(SQLite* db)
:params
db : pointer to an SQLite connection
return value : latest connector/SQLite status message
:see
>sqlite
sqlite_query
:content
Returns the latest SQLite connector status or error message.
Successful calls usually set the message to `ok` or `connected`. Failed prepare, bind, step, pragma, or open operations include connector context plus SQLite's own error text.

View File

@ -0,0 +1,19 @@
:sig
u64 sqlite_insert_id(SQLite* db)
:params
db : pointer to an active SQLite connection
return value : last inserted rowid
:see
>sqlite
sqlite_query
sqlite_affected_rows
:content
Returns SQLite's last inserted rowid for the connection after an insert statement.
```cpp
sqlite_query(db, "insert into notes(body) values(:body)", params);
u64 id = sqlite_insert_id(db);
```

View File

@ -0,0 +1,37 @@
:sig
DTree sqlite_query(SQLite* db, String q)
DTree 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
:see
>sqlite
sqlite_connect
sqlite_error
sqlite_insert_id
sqlite_affected_rows
0_DTree
: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.
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.
```cpp
SQLite* db = sqlite_connect("/tmp/app.sqlite");
StringMap params;
params["email"] = "ada@example.test";
DTree 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.
For statements that do not return rows, inspect `sqlite_affected_rows()` or `sqlite_insert_id()` after the call.

View File

@ -17,13 +17,19 @@ Theme rendering is split the same way as the PHP starter:
- `themes/common/page.json.uce`
- `themes/<theme>/page.html.uce`
Those page templates are the authoritative shell layer, and in the UCE port they are proper `COMPONENT(...)` units rather than standalone page entrypoints. `index.uce` resolves the routed view, captures the `main` fragment, then hands off once into `themes/common/page.*.uce` or `themes/<theme>/page.html.uce`, matching the PHP starter's page-layer flow without an extra shell implementation.
Those page templates are the authoritative shell layer, and in the UCE port they are proper `COMPONENT(...)` units rather than standalone page entrypoints. `index.uce` owns the app-local router, captures the `main` fragment, then hands off to `themes/page.uce`. That component resolves `context.call["app"]["page_type"]` by checking `themes/<current-theme>/page.<page_type>.uce` first and then `themes/common/page.<page_type>.uce`, matching the PHP starter's page-layer flow without an extra shell implementation.
The example uses query-string routing in the same style as the PHP starter:
The router is intentionally normal UCE code instead of a runtime feature. It checks `views/<path>.uce`, then `views/<path>/index.uce`, then parent index handlers such as `views/workspace/index.uce` with the last segment stored as `context.call["route"]["param"]`. Matching view files are invoked with `component()`, and view files expose `COMPONENT(Request& context)` rather than `RENDER(Request& context)` because they are intended to render only through the central router. This gives the starter hierarchical/file-based routing while keeping policy in the app.
- `index.uce`
- `index.uce?page1`
- `index.uce?themes&theme=portal-dark`
- `index.uce?workspace/projects`
Starter-local web affordances live in `components/theme/web_affordances.uce`; currently this provides `COMPONENT:island` for progressive enhancement without adding global UCE runtime APIs. Component-specific CSS/JS is emitted by `ONCE(Request& context)` in the component unit that owns it, or by a small shared asset component when multiple sibling components need the same files. The router's 404 body is also a normal component at `components/basic/notfound.uce`, keeping page fragments out of the front controller.
The example uses query-string routing in the same style as the PHP starter, but the canonical public URL is the directory path because `index.uce` is reached through nginx's default index/try-file behavior. Links generated by the starter therefore target:
- `/examples/uce-starter/`
- `/examples/uce-starter/?page1`
- `/examples/uce-starter/?themes&theme=portal-dark`
- `/examples/uce-starter/?workspace/projects`
Direct requests to `/examples/uce-starter/index.uce` still work, but self-links are canonicalized back to `/examples/uce-starter/`.
The demo account pages use a small file-backed user store under `/tmp/uce-starter-data/` with session-based login state.

View File

@ -4,7 +4,7 @@ COMPONENT(Request& context)
{
String title = first(context.props["title"].to_string(), "Sign in with OAuth");
String subtitle = first(context.props["subtitle"].to_string(), "Choose your preferred authentication method");
String callback_url = first(context.props["callback_url"].to_string(), starter_link("auth/callback", context));
String callback_url = first(context.props["callback_url"].to_string(), app_link("auth/callback", context));
DTree services = context.props["services"];
if(services.get_type_name() != "array" || services["google"]["name"].to_string() == "")
@ -46,7 +46,7 @@ COMPONENT(Request& context)
String service_name = service["name"].to_string();
String scope = service["scope"].to_string();
String auth_url = service["auth_url"].to_string();
String store_url = starter_link("auth/store-oauth-session", context);
String store_url = app_link("auth/store-oauth-session", context);
?><div data-service="<?= service_key ?>">
<button type="button" class="btn" onclick='initiateOAuth_<?= handler_name ?>()'>
<i class="<?= service["icon"].to_string() ?>" style="color: <?= service["color"].to_string() ?>"></i>

View File

@ -2,7 +2,6 @@
COMPONENT(Request& context)
{
starter_boot(context);
<>
<div id="cookie-consent" style="

View File

@ -0,0 +1,15 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
String message = first(context.props["message"].to_string(), "The requested page does not exist.");
context.set_status(404, "Not Found");
context.call["app"]["page_title"] = "404 Not Found";
<>
<section class="card">
<h1>404 Not Found</h1>
<p><?= message ?></p>
<p>The starter router looked for the route as an exact view, a directory index, and then parent index handlers.</p>
</section>
</>
}

View File

@ -1,5 +1,15 @@
#load "../../lib/app.uce"
ONCE(Request& context)
{
?><link rel="stylesheet" href="<?= app_asset_url("js/ag-grid/ag-grid.css", context) ?>" />
<link rel="stylesheet" href="<?= app_asset_url("js/ag-grid/ag-theme-alpine.css", context) ?>" />
<script src="<?= app_asset_url("js/u-format.js", context) ?>"></script>
<script src="<?= app_asset_url("js/u-timeseries-chart.js", context) ?>"></script>
<script src="<?= app_asset_url("js/u-sortable-table.js", context) ?>"></script>
<script src="<?= app_asset_url("js/ag-grid/ag-grid-community.min.js", context) ?>"></script><?
}
String data_format_bytes(String raw, bool disk = false)
{
if(trim(raw) == "")
@ -105,8 +115,6 @@ COMPONENT:SUMMARY_METRICS(Request& context)
COMPONENT:TIMESERIES_CHART(Request& context)
{
starter_register_js("js/u-format.js", context);
starter_register_js("js/u-timeseries-chart.js", context);
String chart_id = first(context.props["id"].to_string(), "ts-chart-" + std::to_string((u64)time()));
String canvas_id = chart_id + "-canvas";
@ -148,8 +156,6 @@ COMPONENT:TIMESERIES_CHART(Request& context)
COMPONENT:SORTABLE_TABLE(Request& context)
{
starter_register_js("js/u-format.js", context);
starter_register_js("js/u-sortable-table.js", context);
String table_id = first(context.props["id"].to_string(), "sortable-table-" + std::to_string((u64)time()));
String title = context.props["title"].to_string();
@ -214,9 +220,6 @@ COMPONENT:SORTABLE_TABLE(Request& context)
COMPONENT:DATA_TABLE(Request& context)
{
String table_id = first(context.props["id"].to_string(), "data-grid-" + std::to_string((u64)time()));
starter_register_js("js/ag-grid/ag-grid-community.min.js", context);
starter_register_css("js/ag-grid/ag-grid.css", context);
starter_register_css("js/ag-grid/ag-theme-alpine.css", context);
DTree columns = context.props["columns"];
if(columns.get_type_name() != "array")

View File

@ -2,10 +2,9 @@
COMPONENT(Request& context)
{
starter_boot(context);
String current_theme = context.cfg.get_by_path("theme/key").to_string();
String current_label = first(context.cfg.get_by_path("theme/label").to_string(), current_theme);
String route_path = context.call["starter"]["route"]["l_path"].to_string();
String route_path = context.call["route"]["l_path"].to_string();
<>
<div id="theme-switcher" style="position: fixed; right: 1.5rem; bottom: 1.5rem; z-index: 9999; font-family: inherit;">
@ -111,7 +110,7 @@ COMPONENT(Request& context)
<? context.cfg.get_by_path("theme/options").each([&](DTree theme_info, String theme_key) {
StringMap params;
params["theme"] = theme_key;
String href = starter_link(route_path == "index" ? "" : route_path, params, context);
String href = app_link(route_path == "index" ? "" : route_path, params, context);
?><a class="theme-option<?= theme_key == current_theme ? " is-active" : "" ?>" href="<?= href ?>">
<span><?= theme_info["label"].to_string() ?></span>
<? if(theme_key == current_theme) { ?><small>Active</small><? } ?>

View File

@ -2,8 +2,10 @@
COMPONENT(Request& context)
{
starter_register_js("components/gauges/common.js", context);
starter_register_css("themes/common/css/gauges.css", context);
DTree 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));
String gauge_id = first(context.props["id"].to_string(), "arcgauge-" + std::to_string((u64)time()));
String title = context.props["title"].to_string();

View File

@ -2,8 +2,10 @@
COMPONENT(Request& context)
{
starter_register_js("components/gauges/common.js", context);
starter_register_css("themes/common/css/gauges.css", context);
DTree 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));
String gauge_id = first(context.props["id"].to_string(), "needlegauge-" + std::to_string((u64)time()));
String style = context.props["style"].to_string();

View File

@ -2,8 +2,10 @@
COMPONENT(Request& context)
{
starter_register_js("components/gauges/common.js", context);
starter_register_css("themes/common/css/gauges.css", context);
DTree 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));
String gauge_id = first(context.props["id"].to_string(), "progressbar-" + std::to_string((u64)time()));
String style = context.props["style"].to_string();

View File

@ -2,7 +2,6 @@
COMPONENT(Request& context)
{
starter_boot(context);
StarterUser users(context);
DTree user = users.current();
bool signed_in = user["email"].to_string() != "";
@ -16,14 +15,14 @@ COMPONENT(Request& context)
<? if(signed_in) { ?>
<span class="<?= name_class ?>"><?= first(user["username"].to_string(), user["email"].to_string(), "Account") ?></span>
<? if(links_class != "") { ?><div class="<?= links_class ?>"><? } ?>
<a href="<?= starter_link("account/profile", context) ?>">Profile</a>
<a href="<?= starter_link("account/logout", context) ?>">Logout</a>
<a href="<?= app_link("account/profile", context) ?>">Profile</a>
<a href="<?= app_link("account/logout", context) ?>">Logout</a>
<? if(links_class != "") { ?></div><? } ?>
<? } else { ?>
<? if(links_class != "") { ?><div class="<?= links_class ?>"><? } ?>
<a href="<?= starter_link("account/login", context) ?>">Login</a>
<a href="<?= app_link("account/login", context) ?>">Login</a>
<? if(context.cfg.get_by_path("users/enable_signup").to_string() != "") { ?>
<a href="<?= starter_link("account/register", context) ?>">Register</a>
<a href="<?= app_link("account/register", context) ?>">Register</a>
<? } ?>
<? if(links_class != "") { ?></div><? } ?>
<? } ?>

View File

@ -0,0 +1,39 @@
#load "../../lib/app.uce"
void app_asset_tag_once(Request& context, String kind, String path)
{
if(path == "")
return;
String key = kind + ":" + path;
if(context.call["assets"][key].to_bool())
return;
context.call["assets"][key].set_bool(true);
if(kind == "css")
{
?><link rel="stylesheet" href="<?= app_asset_url(path, context) ?>" /><?
}
else if(kind == "js")
{
?><script src="<?= app_asset_url(path, context) ?>"></script><?
}
}
void app_asset_tags(Request& context, String kind, DTree assets)
{
String scalar = assets.to_string();
if(scalar != "")
{
app_asset_tag_once(context, kind, scalar);
return;
}
assets.each([&](DTree item, String key) {
app_asset_tag_once(context, kind, item.to_string());
});
}
COMPONENT(Request& context)
@fragment head
{
app_asset_tags(context, "css", context.props["css"]);
app_asset_tags(context, "js", context.props["js"]);
}

View File

@ -2,11 +2,6 @@
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
if(embed_mode)
return;
String text = first(context.cfg.get_by_path("theme/footer_text").to_string(), context.cfg.get_by_path("site/name").to_string());
String inner_class = first(context.props["inner_class"].to_string(), context.cfg.get_by_path("theme/key").to_string() == "portal-light" ? "footer-inner" : "");

View File

@ -2,10 +2,6 @@
COMPONENT(Request& context)
{
starter_boot(context);
if(starter_page_embed_mode(context))
return;
String cookie_value = context.props["cookie_consent"].to_string();
bool cookie_consent = !(cookie_value == "0" || cookie_value == "false" || cookie_value == "FALSE" || cookie_value == "no" || cookie_value == "NO");

View File

@ -2,11 +2,10 @@
COMPONENT(Request& context)
{
starter_boot(context);
String title = first(context.call["starter"]["page_title"].to_string(), context.cfg.get_by_path("site/default_page_title").to_string());
String title = first(context.call["app"]["page_title"].to_string(), context.cfg.get_by_path("site/default_page_title").to_string());
String description = first(context.cfg.get_by_path("theme/meta_description").to_string(), "UCE starter example");
String theme_color = first(context.cfg.get_by_path("theme/theme_color").to_string(), "#0f172a");
String icon = starter_asset_url(context.cfg.get_by_path("theme/path").to_string() + "icon.png", context);
String icon = app_asset_url(context.cfg.get_by_path("theme/path").to_string() + "icon.png", context);
<>
<meta charset="utf-8">
@ -16,7 +15,11 @@ COMPONENT(Request& context)
<meta name="theme-color" content="<?= theme_color ?>">
<link rel="apple-touch-icon" href="<?= icon ?>" />
<link rel="icon" type="image/png" sizes="32x32" href="<?= icon ?>" />
<? starter_render_registered_css(context); ?>
<? starter_render_registered_js(context); ?>
<link rel="stylesheet" href="<?= app_asset_url(context.cfg.get_by_path("theme/path").to_string() + "css/style.css", context) ?>" />
<link rel="stylesheet" href="<?= app_asset_url("themes/common/fontawesome/css/all.min.css", context) ?>" />
<script src="<?= app_asset_url("js/u-query.js", context) ?>"></script>
<script src="<?= app_asset_url("js/morphdom.js", context) ?>"></script>
<script src="<?= app_asset_url("js/site.js", context) ?>"></script>
<?: context.call["fragments"]["head"].to_string() ?>
</>
}

View File

@ -1,20 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
if(context.props["main_html"].to_string() != "")
context.call["starter"]["fragments"]["main"] = context.props["main_html"];
if(context.props["json"].get_type_name() == "array")
context.call["starter"]["json"] = context.props["json"];
if(context.props["page_type"].to_string() != "")
context.call["starter"]["page_type"] = context.props["page_type"];
if(context.props["embed_mode"].to_string() != "")
context.call["starter"]["embed_mode"] = context.props["embed_mode"];
starter_render_page(context);
}
COMPONENT:HEAD(Request& context)
{
print(component("head", context.props, context));
}

View File

@ -2,7 +2,6 @@
COMPONENT(Request& context)
{
starter_boot(context);
String nav_class = context.props["nav_class"].to_string();
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";
@ -18,11 +17,12 @@ COMPONENT(Request& context)
<>
<nav<?: nav_class != "" ? " class=\"" + html_escape(nav_class) + "\"" : "" ?>>
<div class="nav-menu">
<a href="<?= starter_link("", context) ?>"><?= context.cfg.get_by_path("site/name").to_string() ?></a>
<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) {
if(menu_item["hidden"].to_string() == "1")
return;
?><a href="<?= starter_menu_href(menu_key, menu_item, context) ?>"><?= menu_item["title"].to_string() ?></a><?
String href = menu_item["external"].to_string() != "" ? "/" + menu_key : app_link(menu_key, context);
?><a href="<?= href ?>"><?= menu_item["title"].to_string() ?></a><?
}); ?>
</div>
<?: component("account_links", account_props, context) ?>

View File

@ -0,0 +1,18 @@
#load "../../lib/app.uce"
COMPONENT:island(Request& context)
{
String name = context.props["name"].to_string();
String module = context.props["module"].to_string();
String id = first(context.props["id"].to_string(), "island-" + ascii_safe_name(name));
String payload = json_encode(context.props["props"]);
if(name == "")
return;
<>
<div id="<?= id ?>" data-uce-island="<?= name ?>" data-props="<?: html_escape(payload) ?>"></div>
</>
if(module != "")
{
<><script type="module" src="<?= app_asset_url(module, context) ?>"></script></>
}
}

View File

@ -1,9 +1,13 @@
#load "../../lib/app.uce"
ONCE(Request& context)
{
?><link rel="stylesheet" href="<?= app_asset_url("themes/common/css/workspace.css", context) ?>" />
<script src="<?= app_asset_url("js/u-workspace-shell.js", context) ?>"></script><?
}
COMPONENT:APP_FRAME(Request& context)
{
starter_register_css("themes/common/css/workspace.css", context);
starter_register_js("js/u-workspace-shell.js", context);
String id = context.props["id"].to_string();
String overlay_id = context.props["overlay_id"].to_string();
String class_name = context.props["class"].to_string();

View File

@ -13,6 +13,7 @@ DTree get_config()
config["theme"]["options"]["light"]["label"] = "Starter Light";
config["theme"]["options"]["light"]["path"] = "themes/light/";
config["theme"]["options"]["light"]["mode"] = "light";
config["theme"]["options"]["light"]["mode-class"] = "";
config["theme"]["options"]["light"]["description"] = "Original starter light theme kept for backward compatibility.";
config["theme"]["options"]["light"]["footer_text"] = "UCE Starter running with the Starter Light theme.";
config["theme"]["options"]["light"]["meta_description"] = "Starter Light theme for the UCE starter";
@ -21,6 +22,7 @@ DTree get_config()
config["theme"]["options"]["dark"]["label"] = "Starter Dark";
config["theme"]["options"]["dark"]["path"] = "themes/dark/";
config["theme"]["options"]["dark"]["mode"] = "dark";
config["theme"]["options"]["dark"]["mode-class"] = "dark-theme";
config["theme"]["options"]["dark"]["description"] = "Original starter dark theme kept for backward compatibility.";
config["theme"]["options"]["dark"]["footer_text"] = "UCE Starter running with the Starter Dark theme.";
config["theme"]["options"]["dark"]["meta_description"] = "Starter Dark theme for the UCE starter";
@ -29,6 +31,7 @@ DTree get_config()
config["theme"]["options"]["portal-light"]["label"] = "AI Portal Light";
config["theme"]["options"]["portal-light"]["path"] = "themes/portal-light/";
config["theme"]["options"]["portal-light"]["mode"] = "light";
config["theme"]["options"]["portal-light"]["mode-class"] = "";
config["theme"]["options"]["portal-light"]["description"] = "Dense, corporate portal layout in a light palette.";
config["theme"]["options"]["portal-light"]["footer_text"] = "UCE Starter running with the AI Portal Light starter theme.";
config["theme"]["options"]["portal-light"]["meta_description"] = "AI Portal Light theme for the UCE starter";
@ -37,6 +40,7 @@ DTree get_config()
config["theme"]["options"]["portal-dark"]["label"] = "AI Portal Dark";
config["theme"]["options"]["portal-dark"]["path"] = "themes/portal-dark/";
config["theme"]["options"]["portal-dark"]["mode"] = "dark";
config["theme"]["options"]["portal-dark"]["mode-class"] = "dark-theme";
config["theme"]["options"]["portal-dark"]["description"] = "Glassy dark portal shell and the current default starter theme.";
config["theme"]["options"]["portal-dark"]["footer_text"] = "UCE Starter running with the AI Portal Dark starter theme.";
config["theme"]["options"]["portal-dark"]["meta_description"] = "AI Portal Dark theme for the UCE starter";
@ -45,6 +49,7 @@ DTree get_config()
config["theme"]["options"]["localfirst"]["label"] = "Local First";
config["theme"]["options"]["localfirst"]["path"] = "themes/localfirst/";
config["theme"]["options"]["localfirst"]["mode"] = "dark";
config["theme"]["options"]["localfirst"]["mode-class"] = "dark-theme";
config["theme"]["options"]["localfirst"]["description"] = "llm2-derived admin shell with sidebar chrome.";
config["theme"]["options"]["localfirst"]["footer_text"] = "UCE Starter running with the Local First starter theme.";
config["theme"]["options"]["localfirst"]["meta_description"] = "Local First admin-shell theme for the UCE starter";
@ -53,6 +58,7 @@ DTree get_config()
config["theme"]["options"]["retro-gaming"]["label"] = "Retro Gaming";
config["theme"]["options"]["retro-gaming"]["path"] = "themes/retro-gaming/";
config["theme"]["options"]["retro-gaming"]["mode"] = "dark";
config["theme"]["options"]["retro-gaming"]["mode-class"] = "dark-theme";
config["theme"]["options"]["retro-gaming"]["description"] = "CRT scanlines, pixel font, neon glow.";
config["theme"]["options"]["retro-gaming"]["footer_text"] = "INSERT COIN // UCE Starter running the Retro Gaming theme.";
config["theme"]["options"]["retro-gaming"]["meta_description"] = "Retro Gaming pixel theme for the UCE starter";

View File

@ -1,29 +1,63 @@
#load "lib/app.uce"
#load "lib/app.uce"
DTree starter_router_result(String file, String param = "")
{
DTree result;
result["file"] = file;
if(param != "")
result["param"] = param;
return(result);
}
DTree starter_router_resolve(Request& context)
{
String lpath = context.call["route"]["l_path"].to_string();
if(lpath == "")
return(DTree());
String exact = "views/" + lpath + ".uce";
if(file_exists(exact))
return(starter_router_result(exact));
String directory_index = "views/" + lpath + "/index.uce";
if(file_exists(directory_index))
return(starter_router_result(directory_index));
auto parts = split(lpath, "/");
while(parts.size() > 1)
{
String param = parts.back();
parts.pop_back();
String parent = join(parts, "/");
String parent_index = "views/" + parent + "/index.uce";
if(file_exists(parent_index))
return(starter_router_result(parent_index, param));
}
return(DTree());
}
RENDER(Request& context)
{
app_init(context);
DTree resolved = app_resolve_view(context);
DTree resolved = starter_router_resolve(context);
ob_start();
if(resolved["file"].to_string() != "")
{
if(resolved["param"].to_string() != "")
context.call["app"]["route"]["param"] = resolved["param"];
unit_render(resolved["file"].to_string(), context);
context.call["route"]["param"] = resolved["param"];
print(component(resolved["file"].to_string(), context));
}
else
{
app_not_found("The requested page does not exist.", context);
<>
<section class="card">
<h1>404 Not Found</h1>
<p><?= context.call["app"]["error"].to_string() ?></p>
</section>
</>
DTree notfound_props;
notfound_props["message"] = "The requested page does not exist.";
print(component("components/basic/notfound", notfound_props, context));
}
String main_html = ob_get_close();
context.call["app"]["fragments"]["main"] = main_html;
app_render_page(context);
context.call["fragments"]["main"] = main_html;
print(component("themes/page", context));
}

View File

@ -1,4 +1,4 @@
(function (root, factory) { // I really hate this convoluted bullshit
(function (root, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {

View File

@ -1,158 +1,18 @@
#load "../config/settings.uce"
#load "user.class.h"
String app_fs_root(Request& context)
{
String root = context.call["app"]["fs_root"].to_string();
if(root == "")
root = cwd_get();
return(root);
}
String app_script_url(Request& context)
{
String url = context.call["app"]["script_url"].to_string();
if(url == "")
url = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
return(url);
}
String app_base_url(Request& context)
{
String base = context.call["app"]["base_url"].to_string();
if(base == "")
{
base = dirname(app_script_url(context));
if(base == "")
base = "/";
if(base[base.length() - 1] != '/')
base.append(1, '/');
}
return(base);
}
String app_fs_path(String relative, Request& context)
{
return(path_join(app_fs_root(context), relative));
}
String app_link(String path, Request& context);
String app_link(String path, StringMap params, Request& context);
void app_init(Request& context);
String app_first_route_segment(Request& context)
{
String query = context.params["QUERY_STRING"];
for(auto part : split(query, "&"))
{
if(part == "")
continue;
if(part.find("=") == String::npos)
return(uri_decode(part));
}
return("");
}
DTree app_make_route(Request& context)
{
DTree route;
String path = app_first_route_segment(context);
path = trim(path);
while(path.length() > 0 && path[0] == '/')
path = path.substr(1);
while(path.length() > 0 && path[path.length() - 1] == '/')
path = path.substr(0, path.length() - 1);
if(path == "")
path = "index";
route["l_path"] = path;
route["page"] = nibble(path, "/");
if(route["page"].to_string() == "")
route["page"] = "index";
return(route);
}
DTree app_resolve_view(Request& context, String base_dir = "views")
{
DTree result;
DTree route = context.call["app"]["route"];
String lpath = first(route["l_path"].to_string(), "index");
String base = trim(base_dir);
if(base != "" && base[base.length() - 1] == '/')
base = base.substr(0, base.length() - 1);
String exact = base + "/" + lpath + ".uce";
if(file_exists(exact))
{
result["file"] = exact;
return(result);
}
String dir_index = base + "/" + lpath + "/index.uce";
if(file_exists(dir_index))
{
result["file"] = dir_index;
return(result);
}
auto parts = split(lpath, "/");
if(parts.size() > 1)
{
String param = parts.back();
parts.pop_back();
String parent = join(parts, "/");
String parent_index = base + "/" + parent + "/index.uce";
if(file_exists(parent_index))
{
result["file"] = parent_index;
result["param"] = param;
return(result);
}
}
return(result);
}
void app_register_css(String path, Request& context)
{
context.call["app"]["assets"]["css"][path] = path;
}
void app_register_js(String path, Request& context)
{
context.call["app"]["assets"]["js"][path] = path;
}
String app_asset_url(String path, Request& context)
{
String url = app_base_url(context) + path;
String fs_path = app_fs_path(path, context);
String url = context.params["BASE_URL"] + path;
String fs_path = path_join(dirname(context.params["SCRIPT_FILENAME"]), path);
if(file_exists(fs_path))
url += "?v=" + std::to_string((u64)file_mtime(fs_path));
return(url);
}
void app_render_registered_css(Request& context)
{
context.call["app"]["assets"]["css"].each([&](DTree item, String key) {
String path = item.to_string();
if(path != "")
{
<><link rel="stylesheet" href="<?= app_asset_url(path, context) ?>" /></>
}
});
}
void app_render_registered_js(Request& context)
{
context.call["app"]["assets"]["js"].each([&](DTree item, String key) {
String path = item.to_string();
if(path != "")
{
<><script src="<?= app_asset_url(path, context) ?>"></script></>
}
});
}
String app_link(String path, Request& context)
{
StringMap params;
@ -170,249 +30,38 @@ String app_link(String path, StringMap params, Request& context)
query += "&" + extra;
else if(query == "")
query = extra;
String url = app_script_url(context);
String url = context.params["SCRIPT_URL"];
if(query != "")
url += "?" + query;
return(url);
}
void app_redirect(String path, Request& context)
{
context.set_status(302, "Found");
context.header["Location"] = app_link(path, context);
}
void app_not_found(String message, Request& context)
{
context.set_status(404, "Not Found");
context.call["app"]["error"] = message;
context.call["app"]["page_title"] = "404 Not Found";
}
String app_menu_href(String menu_key, DTree menu_item, Request& context)
{
if(menu_item["external"].to_string() != "")
return("/" + menu_key);
return(app_link(menu_key, context));
}
String app_html_class(Request& context)
{
String classes = "no-js";
if(context.cfg.get_by_path("theme/mode").to_string() == "dark")
classes += " dark-theme";
return(classes);
}
String app_page_main_html(Request& context)
{
String main_html = context.props["main_html"].to_string();
if(main_html == "")
main_html = context.call["app"]["fragments"]["main"].to_string();
return(main_html);
}
bool app_bool_value(DTree value, bool fallback = false)
{
String raw = trim(value.to_string());
if(raw == "")
return(fallback);
return(
raw != "0" &&
raw != "false" &&
raw != "FALSE" &&
raw != "False" &&
raw != "(false)" &&
raw != "no" &&
raw != "NO" &&
raw != "No"
);
}
bool app_request_embed_mode(Request& context)
{
return(app_bool_value(context.call["app"]["embed_mode"]));
}
bool app_page_embed_mode(Request& context)
{
String embed_value = context.props["embed_mode"].to_string();
if(embed_value != "")
return(app_bool_value(context.props["embed_mode"]));
return(app_request_embed_mode(context));
}
String app_theme_page_component(Request& context)
{
String page_type = first(context.call["app"]["page_type"].to_string(), "html");
if(page_type == "blank")
return("themes/common/page.blank.uce");
if(page_type == "json")
return("themes/common/page.json.uce");
String theme_path = context.cfg.get_by_path("theme/path").to_string();
if(theme_path == "")
return("");
if(theme_path[theme_path.length() - 1] != '/')
theme_path.append(1, '/');
return(theme_path + "page.html.uce");
}
void app_render_page(Request& context)
{
if(context.flags.status >= 300 && context.flags.status < 400)
return;
DTree page_props;
page_props["main_html"] = context.call["app"]["fragments"]["main"];
if(context.call["app"]["json"].get_type_name() == "array")
page_props["json"] = context.call["app"]["json"];
String page_component = app_theme_page_component(context);
if(page_component != "" && file_exists(page_component))
{
print(component(page_component, page_props, context));
return;
}
context.set_status(500, "Internal Server Error");
context.header["Content-Type"] = "text/plain; charset=utf-8";
print("UCE app is missing the page template component: " + page_component);
}
// Backward-compatible aliases for older starter example units.
using StarterUser = AppUser;
void starter_boot(Request& context)
{
app_init(context);
}
String starter_link(String path, Request& context)
{
return(app_link(path, context));
}
String starter_link(String path, StringMap params, Request& context)
{
return(app_link(path, params, context));
}
void starter_register_css(String path, Request& context)
{
app_register_css(path, context);
}
void starter_register_js(String path, Request& context)
{
app_register_js(path, context);
}
String starter_asset_url(String path, Request& context)
{
return(app_asset_url(path, context));
}
void starter_render_registered_css(Request& context)
{
app_render_registered_css(context);
}
void starter_render_registered_js(Request& context)
{
app_render_registered_js(context);
}
bool starter_page_embed_mode(Request& context)
{
return(app_page_embed_mode(context));
}
String starter_page_main_html(Request& context)
{
return(app_page_main_html(context));
}
String starter_html_class(Request& context)
{
return(app_html_class(context));
}
String starter_menu_href(String menu_key, DTree menu_item, Request& context)
{
return(app_menu_href(menu_key, menu_item, context));
}
void starter_render_page(Request& context)
{
app_render_page(context);
}
void starter_redirect(String path, Request& context)
{
app_redirect(path, context);
}
void starter_not_found(String message, Request& context)
{
app_not_found(message, context);
}
DTree starter_make_route(Request& context)
{
return(app_make_route(context));
}
DTree starter_resolve_view(Request& context, String base_dir = "views")
{
return(app_resolve_view(context, base_dir));
}
void app_init(Request& context)
{
if(context.call["app"]["booted"].to_string() == "1")
return;
context.call["app"]["booted"] = "1";
context.call["app"]["fs_root"] = cwd_get();
context.call["app"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
String base_url = dirname(context.call["app"]["script_url"].to_string());
if(base_url == "")
base_url = "/";
if(base_url[base_url.length() - 1] != '/')
base_url.append(1, '/');
context.call["app"]["base_url"] = base_url;
context.call["route"]["raw_path"] = context.params["ROUTE_PATH_RAW"];
context.call["route"]["l_path"] = context.params["ROUTE_PATH"];
context.call["route"]["page"] = context.params["ROUTE_PAGE"];
context.call["route"]["valid"].set_bool(context.params["ROUTE_VALID"] != "0");
DTree config = get_config();
String requested_theme = first(context.get["theme"], context.cookies["app_theme"], config["theme"]["key"].to_string());
if(config["theme"]["options"][requested_theme].to_string() == "" && config["theme"]["options"][requested_theme].get_type_name() != "array")
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")
requested_theme = config["theme"]["key"].to_string();
if(context.get["theme"] != "")
context.session["app_theme"] = requested_theme;
config["theme"]["options"][requested_theme].each([&](DTree item, String key) {
DTree selected_theme = config["theme"]["options"][requested_theme];
selected_theme.each([&](DTree item, String key) {
config["theme"][key] = item;
});
config["theme"]["key"] = requested_theme;
context.cfg = config;
context.call["cfg"].set_reference(&context.cfg);
context.call["app"]["config"].set_reference(&context.cfg);
context.call["app"]["route"] = app_make_route(context);
context.call["app"]["page_type"] = "html";
context.call["app"]["page_title"] = first(config["menu"][context.call["app"]["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
context.call["app"]["embed_mode"].set_bool(context.get["embed"] != "");
if(context.get["theme"] != "" && context.get["theme"] == requested_theme)
{
set_cookie("app_theme", requested_theme, time() + (86400 * 180));
context.cookies["app_theme"] = requested_theme;
}
session_start();
context.call["app"]["page_title"] = first(config["menu"][context.call["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
AppUser(context).is_signed_in();
app_register_css(context.cfg.get_by_path("theme/path").to_string() + "css/style.css", context);
app_register_css("themes/common/fontawesome/css/all.min.css", context);
app_register_js("js/u-query.js", context);
app_register_js("js/morphdom.js", context);
app_register_js("js/site.js", context);
}

View File

@ -2,6 +2,5 @@
COMPONENT(Request& context)
{
starter_boot(context);
print(starter_page_main_html(context));
print(context.call["fragments"]["main"].to_string());
}

View File

@ -2,13 +2,7 @@
COMPONENT(Request& context)
{
starter_boot(context);
context.header["Content-Type"] = "application/json";
context.header["Cache-Control"] = "no-cache, no-store, must-revalidate";
if(context.props["json"].get_type_name() == "array")
print(json_encode(context.props["json"]));
else if(context.call["starter"]["json"].get_type_name() == "array")
print(json_encode(context.call["starter"]["json"]));
else
print(starter_page_main_html(context));
print(context.call["fragments"]["main"].to_string());
}

View File

@ -2,26 +2,23 @@
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
String main_html = context.call["fragments"]["main"].to_string();
DTree nav_props;
nav_props["account_wrapper_class"] = "nav-account nav-menu";
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body<?: embed_mode ? " class=\"embed-mode\"" : "" ?>>
<body>
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", nav_props, context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<div id="content">
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>

View File

@ -2,23 +2,20 @@
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
String main_html = context.call["fragments"]["main"].to_string();
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body<?: embed_mode ? " class=\"embed-mode\"" : "" ?>>
<body>
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<div id="content">
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>

View File

@ -2,10 +2,8 @@
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
String current_path = context.call["starter"]["route"]["l_path"].to_string();
String main_html = context.call["fragments"]["main"].to_string();
String current_path = context.call["route"]["l_path"].to_string();
DTree global_props;
global_props["cookie_consent"].set_bool(false);
@ -17,32 +15,31 @@ COMPONENT(Request& context)
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body class="admin-page localfirst-theme dark-theme<?= embed_mode ? " embed-mode" : "" ?>">
<body class="admin-page localfirst-theme dark-theme">
<?: component("../../components/theme/global_controls", global_props, context) ?>
<div class="admin-shell">
<nav class="admin-nav">
<div class="admin-nav-header">
<a class="admin-nav-title" href="<?= starter_link("", context) ?>">
<img class="admin-nav-logo-img" src="<?= starter_asset_url("themes/localfirst/img/local_first_logo.png", context) ?>" alt="<?= context.cfg.get_by_path("site/name").to_string() ?>" />
<a class="admin-nav-title" href="<?= app_link("", context) ?>">
<img class="admin-nav-logo-img" src="<?= app_asset_url("themes/localfirst/img/local_first_logo.png", context) ?>" alt="<?= context.cfg.get_by_path("site/name").to_string() ?>" />
</a>
</div>
<? context.cfg.get_by_path("menu").each([&](DTree menu_item, String menu_key) {
if(menu_item["hidden"].to_string() == "1")
return;
bool active = menu_key != "" && (current_path == menu_key || current_path.rfind(menu_key + "/", 0) == 0);
?><a class="admin-nav-item<?= active ? " active" : "" ?>" href="<?= starter_menu_href(menu_key, menu_item, context) ?>"><?= menu_item["title"].to_string() ?></a><?
String href = menu_item["external"].to_string() != "" ? "/" + menu_key : app_link(menu_key, context);
?><a class="admin-nav-item<?= active ? " active" : "" ?>" href="<?= href ?>"><?= menu_item["title"].to_string() ?></a><?
}); ?>
</nav>
<div class="admin-main">
<? if(!embed_mode) { ?>
<div class="admin-toolbar">
<?: component("../../components/theme/account_links", account_props, context) ?>
</div>
<? } ?>
<div class="admin-toolbar">
<?: component("../../components/theme/account_links", account_props, context) ?>
</div>
<div id="content" class="admin-content">
<?: main_html ?>
</div>

View File

@ -0,0 +1,22 @@
#load "../lib/app.uce"
COMPONENT(Request& context)
{
if(context.flags.status >= 300 && context.flags.status < 400)
return;
String page_type = first(context.call["app"]["page_type"].to_string(), "html");
String theme_path = "../" + context.cfg.get_by_path("theme/path").to_string();
String page_component = theme_path + "page." + page_type + ".uce";
if(!file_exists(page_component))
page_component = "../themes/common/page." + page_type + ".uce";
if(file_exists(page_component))
print(component(page_component, context));
else
{
context.set_status(500, "Internal Server Error");
context.header["Content-Type"] = "text/plain; charset=utf-8";
print("UCE starter is missing a page template for page type: " + page_type);
}
}

View File

@ -2,26 +2,23 @@
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
String main_html = context.call["fragments"]["main"].to_string();
DTree nav_props;
nav_props["account_wrapper_class"] = "nav-account nav-menu";
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body class="portal-theme portal-dark-theme dark-theme<?= embed_mode ? " embed-mode" : "" ?>">
<body class="portal-theme portal-dark-theme dark-theme">
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", nav_props, context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<div id="content">
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>

View File

@ -2,24 +2,21 @@
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
String main_html = context.call["fragments"]["main"].to_string();
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
footer_props["inner_class"] = "footer-inner";
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body class="portal-theme portal-light-theme<?= embed_mode ? " embed-mode" : "" ?>">
<body class="portal-theme portal-light-theme">
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<div id="content">
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>

View File

@ -2,27 +2,24 @@
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
String main_html = context.call["fragments"]["main"].to_string();
DTree nav_props;
nav_props["account_wrapper_class"] = "nav-account nav-menu";
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body<?: embed_mode ? " class=\"embed-mode\"" : "" ?>>
<body>
<div class="retro-stars"></div>
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", nav_props, context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<div id="content">
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>

View File

@ -1,9 +1,8 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Login";
context.call["app"]["page_title"] = "Login";
StarterUser users(context);
DTree result;
if(context.params["REQUEST_METHOD"] == "POST")
@ -11,7 +10,7 @@ RENDER(Request& context)
result = users.sign_in(context.post["email"], context.post["password"]);
if(result["result"].to_string() != "")
{
starter_redirect("account/profile", context);
redirect(app_link("account/profile", context));
return;
}
}
@ -23,6 +22,6 @@ RENDER(Request& context)
<label>Password: <input type="password" name="password" required></label><br>
<button type="submit">Login</button>
</form>
<p><a href="<?= starter_link("account/register", context) ?>">Register</a></p>
<p><a href="<?= app_link("account/register", context) ?>">Register</a></p>
</>
}

View File

@ -1,8 +1,7 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
StarterUser(context).logout();
starter_redirect("account/login", context);
redirect(app_link("account/login", context));
}

View File

@ -1,15 +1,14 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
StarterUser users(context);
if(!users.is_signed_in())
{
starter_redirect("account/login", context);
redirect(app_link("account/login", context));
return;
}
context.call["starter"]["page_title"] = "Profile";
context.call["app"]["page_title"] = "Profile";
DTree user = users.current();
String roles = "";
user["roles"].each([&](DTree role, String key) {
@ -22,6 +21,6 @@ RENDER(Request& context)
<p>Email: <?= user["email"].to_string() ?></p>
<p>Roles: <?= roles ?></p>
<p>Created: <?= time_format_local("%Y-%m-%dT%H:%M:%S", int_val(user["created"].to_string())) ?></p>
<p><a href="<?= starter_link("account/logout", context) ?>">Logout</a></p>
<p><a href="<?= app_link("account/logout", context) ?>">Logout</a></p>
</>
}

View File

@ -1,9 +1,8 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Register";
context.call["app"]["page_title"] = "Register";
StarterUser users(context);
DTree result;
if(context.params["REQUEST_METHOD"] == "POST")
@ -11,7 +10,7 @@ RENDER(Request& context)
<>
<h1>Register</h1>
<? if(result["result"].to_string() != "") { ?><div class="banner success">Registration successful. <a href="<?= starter_link("account/login", context) ?>">Log in</a></div><? } ?>
<? if(result["result"].to_string() != "") { ?><div class="banner success">Registration successful. <a href="<?= app_link("account/login", context) ?>">Log in</a></div><? } ?>
<? if(result["message"].to_string() != "" && result["result"].to_string() == "") { ?><div class="banner error"><?= result["message"].to_string() ?></div><? } ?>
<form method="post">
<label>Email: <input type="email" name="email" required></label><br>

View File

@ -1,9 +1,8 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "OAuth Callback";
context.call["app"]["page_title"] = "OAuth Callback";
String code = context.get["code"];
String state = context.get["state"];
String error = context.get["error"];
@ -18,8 +17,8 @@ RENDER(Request& context)
<h2>Authentication Failed</h2>
<p><?= first(error_description, error) ?></p>
<div class="callback-actions">
<a href="<?= starter_link("auth/demo", context) ?>" class="btn btn-primary">Try Again</a>
<a href="<?= starter_link("", context) ?>" class="btn btn-outline">Go Home</a>
<a href="<?= app_link("auth/demo", context) ?>" class="btn btn-primary">Try Again</a>
<a href="<?= app_link("", context) ?>" class="btn btn-outline">Go Home</a>
</div>
</div>
<? } else if(code != "" && state != "") { ?>
@ -33,8 +32,8 @@ RENDER(Request& context)
<p><strong>State:</strong> <?= state ?></p>
</div>
<div class="callback-actions">
<a href="<?= starter_link("auth/demo", context) ?>" class="btn btn-primary">Back to Auth Demo</a>
<a href="<?= starter_link("", context) ?>" class="btn btn-outline">Go Home</a>
<a href="<?= app_link("auth/demo", context) ?>" class="btn btn-primary">Back to Auth Demo</a>
<a href="<?= app_link("", context) ?>" class="btn btn-outline">Go Home</a>
</div>
</div>
<? } else { ?>

View File

@ -1,10 +1,11 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Auth";
starter_register_css("views/marketing.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/marketing.css";
print(component("../../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Auth";
DTree props;
props["title"] = "Sign In to Your Account";
@ -13,7 +14,7 @@ RENDER(Request& context)
props["github_client_id"] = "YOUR_GITHUB_CLIENT_ID";
props["discord_client_id"] = "YOUR_DISCORD_CLIENT_ID";
props["twitch_client_id"] = "YOUR_TWITCH_CLIENT_ID";
props["callback_url"] = starter_link("auth/callback", context);
props["callback_url"] = app_link("auth/callback", context);
<>
<h1>Authentication Demo</h1>

View File

@ -1,21 +1,24 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_type"] = "json";
context.call["app"]["page_type"] = "json";
DTree response;
DTree body = json_decode(context.in);
if(context.params["REQUEST_METHOD"] == "POST" && body["oauth_service"].to_string() != "" && body["oauth_state"].to_string() != "")
{
context.session["oauth_service"] = body["oauth_service"].to_string();
context.session["oauth_state"] = body["oauth_state"].to_string();
context.call["starter"]["json"]["status"] = "success";
response["status"] = "success";
}
else
{
context.set_status(400, "Bad Request");
context.call["starter"]["json"]["status"] = "error";
context.call["starter"]["json"]["message"] = "Invalid input";
response["status"] = "error";
response["message"] = "Invalid input";
}
print(json_encode(response));
}

View File

@ -1,10 +1,13 @@
#load "../lib/app.uce"
RENDER(Request& context)
ONCE(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Dashboard";
starter_register_css("views/dashboard.css", context);
?><link rel="stylesheet" href="<?= app_asset_url("views/dashboard.css", context) ?>" /><?
}
COMPONENT(Request& context)
{
context.call["app"]["page_title"] = "Dashboard";
DTree props;
DTree item;

View File

@ -1,10 +1,11 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Features";
starter_register_css("views/marketing.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/marketing.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Features";
<>
<h1>Features</h1>
<p class="card">This page exists in the UCE port so the starter menu resolves cleanly as a full route, while still reusing the same marketing components as the home page.</p>

View File

@ -1,10 +1,8 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Gauges";
starter_register_css("themes/common/css/gauges.css", context);
context.call["app"]["page_title"] = "Gauges";
f64 pi = 3.14159265358979323846;
DTree props;

View File

@ -1,10 +1,11 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Home";
starter_register_css("views/marketing.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/marketing.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Home";
DTree props;
@ -79,13 +80,13 @@ RENDER(Request& context)
?><div class="component-card">
<h3><?= theme["label"].to_string() ?></h3>
<p><?= theme["description"].to_string() ?></p>
<a class="btn" href="<?= starter_link("themes", theme_params, context) ?>">Preview Theme</a>
<a class="btn" href="<?= app_link("themes", theme_params, context) ?>">Preview Theme</a>
</div><?
}); ?>
<div class="component-card">
<h3>Starter Themes</h3>
<p>The original light and dark starter skins remain available in the same gallery for side-by-side checks.</p>
<a class="btn btn-secondary" href="<?= starter_link("themes", context) ?>">Open Gallery</a>
<a class="btn btn-secondary" href="<?= app_link("themes", context) ?>">Open Gallery</a>
</div>
</div>
</div>
@ -98,9 +99,16 @@ RENDER(Request& context)
props["secondary_text"] = "View GitHub";
print(component("../components/example/marketing_blocks:CTA_SECTION", props, context));
DTree island_props;
island_props["name"] = "StarterGuidelines";
island_props["id"] = "starter-guidelines-island";
island_props["props"]["route"] = context.call["route"]["l_path"].to_string();
island_props["props"]["enhancement"] = "progressive";
<>
<div class="card">
<h2>Component Development Guidelines</h2>
<?: component("../components/theme/web_affordances:island", island_props, context) ?>
<div class="guidelines-grid">
<div class="guideline-item" style="border-left-color: var(--primary);"><span class="guideline-icon">🏷️</span><span>Use semantic HTML5 elements</span></div>
<div class="guideline-item" style="border-left-color: var(--secondary);"><span class="guideline-icon">🎨</span><span>Implement CSS custom properties for theming</span></div>

View File

@ -1,10 +1,11 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Components";
starter_register_css("views/marketing.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/marketing.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Components";
<>
<h1>Component System</h1>

View File

@ -1,9 +1,8 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_type"] = "blank";
context.call["app"]["page_type"] = "blank";
<>
<?= std::to_string((u64)time()) ?> - Page 2 Section 1 loaded
<pre>UCE starter AJAX fragment response</pre>

View File

@ -1,16 +1,15 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Ajaxy";
context.call["app"]["page_title"] = "Ajaxy";
<>
<h1>Ajax Demo</h1>
<div id="page2-section1">
<p>This is the content of Page 2. You can add more information here.</p>
<p>Sed at dolor leo. Morbi a tellus sed nisl dictum ultricies sit amet at purus. Nam mattis metus sed nunc egestas convallis.</p>
<br/>
<button id="page2-load-button" data-load-url="<?= starter_link("page2-section1", context) ?>" type="button">Load new text</button>
<button id="page2-load-button" data-load-url="<?= app_link("page2-section1", context) ?>" type="button">Load new text</button>
</div>
<script>
(function () {

View File

@ -1,10 +1,11 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Theme Preview";
starter_register_css("views/themes.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/themes.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Theme Preview";
String current_theme_key = context.cfg.get_by_path("theme/key").to_string();
String theme_label = first(context.cfg.get_by_path("theme/label").to_string(), current_theme_key);
String theme_mode = context.cfg.get_by_path("theme/mode").to_string();
@ -18,8 +19,8 @@ RENDER(Request& context)
<h1><?= theme_label ?></h1>
<p>This route renders the same neutral content inside each theme so downstream projects can compare layout, typography, color tokens, and chrome.</p>
<div class="theme-preview-actions">
<a class="btn" href="<?= starter_link("themes", theme_params, context) ?>">Back to Gallery</a>
<a class="btn btn-secondary" href="<?= starter_link("", theme_params, context) ?>">Open Home in This Theme</a>
<a class="btn" href="<?= app_link("themes", theme_params, context) ?>">Back to Gallery</a>
<a class="btn btn-secondary" href="<?= app_link("", theme_params, context) ?>">Open Home in This Theme</a>
</div>
</section>
<section class="theme-preview-grid">
@ -59,7 +60,6 @@ RENDER(Request& context)
<tbody>
<tr><td>Navigation</td><td>Should remain readable when menus get long.</td><td>Verified</td></tr>
<tr><td>Cards</td><td>Should keep spacing and hierarchy on both desktop and mobile.</td><td>Verified</td></tr>
<tr><td>Embeds</td><td>Should render cleanly inside the gallery iframe.</td><td>Verified</td></tr>
</tbody>
</table>
</div>

View File

@ -82,21 +82,6 @@
margin-bottom: 1rem;
}
.theme-gallery-frame-wrap {
border: 1px solid var(--border);
border-radius: calc(var(--radius-lg, var(--radius)) - 4px);
overflow: hidden;
background: var(--bg-secondary);
}
.theme-gallery-frame-wrap iframe {
display: block;
width: 100%;
height: 430px;
border: 0;
background: #fff;
}
.theme-preview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
@ -150,20 +135,6 @@
gap: 0.85rem;
}
.embed-mode nav,
.embed-mode footer,
.embed-mode #theme-switcher,
.embed-mode .admin-toolbar {
display: none !important;
}
.embed-mode #content,
.embed-mode .admin-content {
margin-top: 0 !important;
min-height: 100vh !important;
padding-top: 1rem !important;
}
@media (max-width: 720px) {
.theme-gallery-grid,
.theme-preview-grid {
@ -173,8 +144,4 @@
.theme-gallery-head {
flex-direction: column;
}
.theme-gallery-frame-wrap iframe {
height: 360px;
}
}

View File

@ -1,26 +1,25 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Themes";
starter_register_css("views/themes.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/themes.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Themes";
String current_theme = context.cfg.get_by_path("theme/key").to_string();
<>
<div class="theme-gallery-shell">
<section class="theme-gallery-hero card">
<span class="theme-gallery-kicker">Starter Theme Gallery</span>
<h1>Compare Themes Side by Side</h1>
<p>The gallery renders the same preview content in each theme family. This makes it easier to judge shell fit, typography, token balance, and embed behavior.</p>
<h1>Compare Themes</h1>
<p>Open each theme preview directly to judge shell fit, typography, and token balance.</p>
</section>
<div class="theme-gallery-grid">
<? context.cfg.get_by_path("theme/options").each([&](DTree theme_info, String theme_key) {
StringMap params;
params["theme"] = theme_key;
String preview_href = starter_link("theme-preview", params, context);
params["embed"] = "1";
String iframe_href = starter_link("theme-preview", params, context);
String preview_href = app_link("theme-preview", params, context);
StringMap home_params;
home_params["theme"] = theme_key;
?><section class="theme-gallery-card<?= theme_key == current_theme ? " is-active" : "" ?>">
@ -33,10 +32,7 @@ RENDER(Request& context)
</div>
<div class="theme-gallery-actions">
<a class="btn" href="<?= preview_href ?>">Open Preview</a>
<a class="btn btn-secondary" href="<?= starter_link("", home_params, context) ?>">Open Home</a>
</div>
<div class="theme-gallery-frame-wrap">
<iframe title="<?= theme_info["label"].to_string() ?> preview" src="<?= iframe_href ?>"></iframe>
<a class="btn btn-secondary" href="<?= app_link("", home_params, context) ?>">Open Home</a>
</div>
</section><?
}); ?>

View File

@ -1,10 +1,9 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Workspace";
String section = first(context.call["starter"]["route"]["param"].to_string(), "overview");
context.call["app"]["page_title"] = "Workspace";
String section = first(context.call["route"]["param"].to_string(), "overview");
if(section != "overview" && section != "projects" && section != "activity")
section = "overview";
@ -19,9 +18,9 @@ RENDER(Request& context)
<>
<div class="ws-nav-group-label">Workspace areas</div>
<div class="ws-nav-list">
<a class="ws-nav-item<?= section == "overview" ? " is-active" : "" ?>" href="<?= starter_link("workspace/overview", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-compass"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Overview</span><span class="ws-nav-meta">Shell</span></span></span></a>
<a class="ws-nav-item<?= section == "projects" ? " is-active" : "" ?>" href="<?= starter_link("workspace/projects", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-folder-tree"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Projects</span><span class="ws-nav-meta">Routes</span></span></span></a>
<a class="ws-nav-item<?= section == "activity" ? " is-active" : "" ?>" href="<?= starter_link("workspace/activity", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-wave-square"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Activity</span><span class="ws-nav-meta">State</span></span></span></a>
<a class="ws-nav-item<?= section == "overview" ? " is-active" : "" ?>" href="<?= app_link("workspace/overview", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-compass"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Overview</span><span class="ws-nav-meta">Shell</span></span></span></a>
<a class="ws-nav-item<?= section == "projects" ? " is-active" : "" ?>" href="<?= app_link("workspace/projects", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-folder-tree"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Projects</span><span class="ws-nav-meta">Routes</span></span></span></a>
<a class="ws-nav-item<?= section == "activity" ? " is-active" : "" ?>" href="<?= app_link("workspace/activity", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-wave-square"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Activity</span><span class="ws-nav-meta">State</span></span></span></a>
</div>
<div class="ws-demo-sidebar-copy"><p>This sidebar and mobile shell pattern was extracted from the AI portal app and normalized against starter theme tokens.</p></div>
</>
@ -32,7 +31,7 @@ RENDER(Request& context)
sidebar_body += component("../../components/workspace/primitives:LIST_STATE", sidebar_note, context);
DTree toolbar_props;
toolbar_props["action_html"] = "<a class=\"ws-sidebar-action-btn\" href=\"" + starter_link("workspace/overview", context) + "\"><i class=\"fas fa-grid-2\"></i><span>Open shell</span></a>";
toolbar_props["action_html"] = "<a class=\"ws-sidebar-action-btn\" href=\"" + app_link("workspace/overview", context) + "\"><i class=\"fas fa-grid-2\"></i><span>Open shell</span></a>";
toolbar_props["search_input_id"] = "workspace-demo-search";
toolbar_props["search_input_name"] = "workspace_demo_search";
toolbar_props["search_placeholder"] = "Search workspace sections";
@ -95,7 +94,7 @@ RENDER(Request& context)
String demo_head = component("../../components/workspace/primitives:SECTION_HEAD", section_head, context);
section_props.clear();
section_props["header_html"] = demo_head;
section_props["body_html"] = detail_html + "<p class=\"ws-inline-note\">Try visiting <strong>" + html_escape(starter_link("workspace/projects", context)) + "</strong> or <strong>" + html_escape(starter_link("workspace/activity", context)) + "</strong> to see the nested route fallback in action.</p>";
section_props["body_html"] = detail_html + "<p class=\"ws-inline-note\">Try visiting <strong>" + html_escape(app_link("workspace/projects", context)) + "</strong> or <strong>" + html_escape(app_link("workspace/activity", context)) + "</strong> to see the nested route fallback in action.</p>";
main_body += component("../../components/workspace/primitives:SECTION", section_props, context);
if(section == "activity")
@ -104,7 +103,7 @@ RENDER(Request& context)
empty_props["icon_class"] = "fas fa-clock-rotate-left";
empty_props["title"] = "No live stream wired yet";
empty_props["text"] = "The shell is generic. Add your own websocket, polling, or event-driven runtime behind it when a real product needs one.";
empty_props["action_html"] = "<a class=\"ws-primary-btn\" href=\"" + starter_link("dashboard", context) + "\">Open dashboard demo</a>";
empty_props["action_html"] = "<a class=\"ws-primary-btn\" href=\"" + app_link("dashboard", context) + "\">Open dashboard demo</a>";
main_body += component("../../components/workspace/primitives:EMPTY_STATE", empty_props, context);
}

View File

@ -1,4 +1,5 @@
RENDER(Request& context)
{
context.set_status(302, "Found");
context.header["Location"] = "/info/";
}
}

View File

@ -18,13 +18,19 @@ RENDER(Request& context)
site_tests_page_start("Core APIs", "Pure helper coverage for strings, UTF-8 splitting, DTree, and JSON encoding/decoding.");
auto comma_parts = split("alpha,beta,gamma", ",");
check("split()", comma_parts.size() == 3 && comma_parts[1] == "beta", join(comma_parts, " | "));
auto empty_delim_parts = split("alpha", "");
check("split()", comma_parts.size() == 3 && comma_parts[1] == "beta" && empty_delim_parts.size() == 1 && empty_delim_parts[0] == "alpha", join(comma_parts, " | "));
auto spaced_parts = split_space(" one two three ");
check("split_space()", spaced_parts.size() == 3 && spaced_parts[2] == "three", join(spaced_parts, " / "));
check("trim()", trim(" padded value ") == "padded value", trim(" padded value "));
StringMap kv = split_kv("\n#comment\nalpha = one\nempty=\n", '=', true, false);
StringMap http_headers = split_http_headers("GET /demo.uce?x=1 HTTP/1.1\r\nHost: example.test\r\nX-Empty:\r\n");
StringMap leading_crlf_headers = split_http_headers("\r\nGET /lead.uce HTTP/1.1\r\nHost: lead.example\r\n");
StringMap header_only = split_http_headers("Host: example.test\nX-Token: abc\n");
check("trim() / split_kv() / split_http_headers()", trim(" padded value ") == "padded value" && kv["alpha"] == "one" && kv["empty"] == "" && http_headers["REQUEST_METHOD"] == "GET" && http_headers["DOCUMENT_URI"] == "/demo.uce" && http_headers["QUERY_STRING"] == "x=1" && http_headers["HTTP_X_EMPTY"] == "" && leading_crlf_headers["REQUEST_METHOD"] == "GET" && leading_crlf_headers["DOCUMENT_URI"] == "/lead.uce" && leading_crlf_headers["HTTP_HOST"] == "lead.example" && header_only["REQUEST_METHOD"] == "" && header_only["HTTP_HOST"] == "example.test" && header_only["HTTP_X_TOKEN"] == "abc", trim(" padded value ") + " / " + var_dump(kv) + " / " + var_dump(http_headers) + " / " + var_dump(leading_crlf_headers) + " / " + var_dump(header_only));
check("replace()", replace("alpha-beta-beta", "beta", "done") == "alpha-done-done", replace("alpha-beta-beta", "beta", "done"));
check("html_escape() attribute-safe quotes", html_escape("<&>\"Don't") == "&lt;&amp;&gt;&quot;Don&#39;t", html_escape("<&>\"Don't"));
check("regex_match()", regex_match("[A-Z][a-z]+", "Alice") && !regex_match("[A-Z][a-z]+", "Alice!"), "full-string validation");
DTree regex_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", "Contact ops@example.test");
@ -42,6 +48,41 @@ RENDER(Request& context)
check("str_starts_with()", str_starts_with("websocket-suite", "websocket"), "websocket-suite starts with websocket");
check("str_ends_with()", str_ends_with("component.uce", ".uce"), "component.uce ends with .uce");
check("to_lower() / to_upper()", to_lower("MiXeD") == "mixed" && to_upper("MiXeD") == "MIXED", to_lower("MiXeD") + " / " + to_upper("MiXeD"));
check("request context params", context.params["SCRIPT_URL"] != "" && context.params["BASE_URL"] != "" && context.params["ROUTE_PATH"] != "" && context.params["ROUTE_PAGE"] != "" && context.params["ROUTE_VALID"] == "1", "script=" + context.params["SCRIPT_URL"] + " base=" + context.params["BASE_URL"] + " route=" + context.params["ROUTE_PATH"] + " page=" + context.params["ROUTE_PAGE"] + " valid=" + context.params["ROUTE_VALID"]);
String saved_query_string = context.params["QUERY_STRING"];
context.params["QUERY_STRING"] = "workspace/projects&theme=dark";
check("request_query_path() delegates to request_query_route()", request_query_path(context) == request_query_route(context)["l_path"].to_string() && request_query_path(context) == "workspace/projects", request_query_path(context) + " / " + json_encode(request_query_route(context)));
context.params["QUERY_STRING"] = saved_query_string;
check("route path sanitizers", route_path_normalize("/workspace/projects/") == "workspace/projects" && route_path_is_safe("workspace/projects") && !route_path_is_safe("../demo") && !route_path_is_safe("workspace/../demo") && !route_path_is_safe("workspace/file.uce") && route_path_sanitize("../demo") == "" && route_path_sanitize("") == "index", route_path_sanitize("/workspace/projects/"));
StringList route_parts = {"dashboard", "index", "dashboard", "themes"};
auto unique_routes = list_unique(route_parts);
auto sorted_routes = list_sort(unique_routes);
auto upper_routes = map(sorted_routes, [](String item) { return(to_upper(item)); });
auto dashboard_routes = filter(route_parts, [](String item) { return(item == "dashboard"); });
check("map/filter/list_unique/sort/find/some/every", unique_routes.size() == 3 && sorted_routes[0] == "dashboard" && upper_routes[2] == "THEMES" && dashboard_routes.size() == 2 && list_find(route_parts, [](String item) { return(str_starts_with(item, "them")); }, "missing") == "themes" && list_some(route_parts, [](String item) { return(item == "index"); }) && list_every(unique_routes, [](String item) { return(item != ""); }), join(upper_routes, ","));
DTree nav;
DTree nav_home;
nav_home["title"] = "Home";
nav_home["section"] = "main";
nav.push(nav_home);
DTree nav_dash;
nav_dash["title"] = "Dashboard";
nav_dash["section"] = "app";
nav.push(nav_dash);
DTree nav_themes;
nav_themes["title"] = "Themes";
nav_themes["section"] = "app";
nav.push(nav_themes);
DTree empty_tree;
DTree empty_pop = empty_tree.pop();
DTree app_nav = dtree_filter(nav, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
DTree nav_titles = dtree_map(app_nav, [](DTree item, String key) { DTree title; title = item["title"].to_string(); return(title); });
DTree grouped_nav = dtree_group_by(nav, [](DTree item, String key) { return(item["section"].to_string()); });
DTree nav_dash_summary = dtree_pick(nav_dash, {"title"});
DTree nav_dash_public = dtree_omit(nav_dash, {"section"});
check("DTree collection helpers", empty_pop.to_string() == "" && app_nav.is_list() && nav_titles["0"].to_string() == "Dashboard" && grouped_nav["app"]["1"]["title"].to_string() == "Themes" && join(dtree_keys(nav_dash_summary), ",") == "title" && dtree_values(nav_dash_public)["0"].to_string() == "Dashboard", json_encode(grouped_nav));
String binary_payload = "core";
binary_payload.push_back((char)0x00);

View File

@ -25,7 +25,7 @@ RENDER(Request& context)
set_cookie("site-tests-cookie", "cookie-value", time() + 300);
context.header["X-Site-Tests"] = "http-suite";
String query_src = "alpha=1&beta=two%20words&gamma=ok";
String query_src = "alpha=1&beta=two%20words&gamma=ok&token=a%3Db&keyless&empty=&trailing=ok&";
StringMap query = parse_query(query_src);
String uri_input = "alpha beta/?x=1&y=2";
String encoded = uri_encode(uri_input);
@ -43,7 +43,7 @@ RENDER(Request& context)
check("uri_encode() / uri_decode()", decoded == uri_input, encoded + " => " + decoded);
check("uri_decode() malformed percent literals", malformed_percent == "% %A %GG ok done", malformed_percent);
check("parse_uri() empty input", empty_uri.parts["raw"] == "", var_dump(empty_uri));
check("parse_query()", query["alpha"] == "1" && query["beta"] == "two words", query_dump);
check("parse_query()", query["alpha"] == "1" && query["beta"] == "two words" && query["token"] == "a=b" && query["keyless"] == "" && query["empty"] == "" && query["trailing"] == "ok" && query.count("") == 0, query_dump);
check("set_cookie()", set_cookie_dump.find("site-tests-cookie") != String::npos && set_cookie_dump.find("cookie-value") != String::npos && set_cookie_dump.find("HttpOnly") != String::npos && set_cookie_dump.find("SameSite=Lax") != String::npos, set_cookie_dump);
check("response header mutation", context.header["X-Site-Tests"] == "http-suite", header_dump);
check("session_start()", session_id != "", "session_id=" + session_id);

Some files were not shown because too many files have changed in this diff Show More