fix: harden UCE runtime and starter
This commit is contained in:
parent
71ddcaf7d4
commit
7f757654b6
10
README.md
10
README.md
@ -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
492
RECOMMENDATIONS.md
Normal 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 `'`.
|
||||
|
||||
**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
496
WASM-PROPOSAL.md
Normal 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.2–2× compute slowdown vs. native (still far ahead of
|
||||
interpreted runtimes); a real ABI/membrane design; ownership of a custom
|
||||
loader; toolchain rough edges (§10).
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
30–60 functions (§5.1). Host errors return as error values; traps are
|
||||
reserved for unit faults. Nothing throws across the membrane.
|
||||
|
||||
**DTrees cross the membrane only as the versioned wire encoding** (§5.3),
|
||||
at exactly three sites: request context in (once), response out (once),
|
||||
bulk I/O results in (per query, optional vs. cursor-style hostcalls).
|
||||
|
||||
### 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, ~1–2k 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 (~1–2k 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.
|
||||
34
docs/patches/sqlite-3.46.1-vendor.md
Normal file
34
docs/patches/sqlite-3.46.1-vendor.md
Normal 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.
|
||||
147
docs/react-developer-affordances-todo.md
Normal file
147
docs/react-developer-affordances-todo.md
Normal 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.
|
||||
@ -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
41
site/demo/collections.uce
Normal 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 →</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></>
|
||||
}
|
||||
@ -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
54
site/demo/sqlite.uce
Normal 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);
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
Output / Invocation Functions
|
||||
|
||||
coming_from_react
|
||||
1_RENDER
|
||||
1_CLI
|
||||
cli_input
|
||||
|
||||
8
site/doc/areas/sqlite.txt
Normal file
8
site/doc/areas/sqlite.txt
Normal file
@ -0,0 +1,8 @@
|
||||
SQLite
|
||||
|
||||
sqlite_connect
|
||||
sqlite_disconnect
|
||||
sqlite_error
|
||||
sqlite_query
|
||||
sqlite_insert_id
|
||||
sqlite_affected_rows
|
||||
@ -7,6 +7,12 @@ filter
|
||||
first
|
||||
join
|
||||
json_encode
|
||||
list_every
|
||||
list_find
|
||||
map
|
||||
list_some
|
||||
list_sort
|
||||
list_unique
|
||||
nibble
|
||||
print
|
||||
strpos
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
69
site/doc/pages/coming_from_react.txt
Normal file
69
site/doc/pages/coming_from_react.txt
Normal 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.
|
||||
26
site/doc/pages/dtree_filter.txt
Normal file
26
site/doc/pages/dtree_filter.txt
Normal 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.
|
||||
26
site/doc/pages/dtree_group_by.txt
Normal file
26
site/doc/pages/dtree_group_by.txt
Normal 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.
|
||||
26
site/doc/pages/dtree_keys.txt
Normal file
26
site/doc/pages/dtree_keys.txt
Normal 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.
|
||||
26
site/doc/pages/dtree_map.txt
Normal file
26
site/doc/pages/dtree_map.txt
Normal 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.
|
||||
26
site/doc/pages/dtree_omit.txt
Normal file
26
site/doc/pages/dtree_omit.txt
Normal 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.
|
||||
26
site/doc/pages/dtree_pick.txt
Normal file
26
site/doc/pages/dtree_pick.txt
Normal 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.
|
||||
26
site/doc/pages/dtree_values.txt
Normal file
26
site/doc/pages/dtree_values.txt
Normal 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.
|
||||
26
site/doc/pages/list_every.txt
Normal file
26
site/doc/pages/list_every.txt
Normal 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.
|
||||
18
site/doc/pages/list_filter.txt
Normal file
18
site/doc/pages/list_filter.txt
Normal 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"); });
|
||||
```
|
||||
26
site/doc/pages/list_find.txt
Normal file
26
site/doc/pages/list_find.txt
Normal 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.
|
||||
19
site/doc/pages/list_map.txt
Normal file
19
site/doc/pages/list_map.txt
Normal 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)); });
|
||||
```
|
||||
26
site/doc/pages/list_some.txt
Normal file
26
site/doc/pages/list_some.txt
Normal 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.
|
||||
26
site/doc/pages/list_sort.txt
Normal file
26
site/doc/pages/list_sort.txt
Normal 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.
|
||||
26
site/doc/pages/list_unique.txt
Normal file
26
site/doc/pages/list_unique.txt
Normal 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
31
site/doc/pages/map.txt
Normal 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()`
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
16
site/doc/pages/request_base_url.txt
Normal file
16
site/doc/pages/request_base_url.txt
Normal 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);
|
||||
```
|
||||
28
site/doc/pages/request_context_params.txt
Normal file
28
site/doc/pages/request_context_params.txt
Normal 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.
|
||||
27
site/doc/pages/request_query_path.txt
Normal file
27
site/doc/pages/request_query_path.txt
Normal 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.
|
||||
29
site/doc/pages/request_query_route.txt
Normal file
29
site/doc/pages/request_query_route.txt
Normal 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.
|
||||
16
site/doc/pages/request_script_url.txt
Normal file
16
site/doc/pages/request_script_url.txt
Normal 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);
|
||||
```
|
||||
24
site/doc/pages/route_path_is_safe.txt
Normal file
24
site/doc/pages/route_path_is_safe.txt
Normal 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.
|
||||
20
site/doc/pages/route_path_normalize.txt
Normal file
20
site/doc/pages/route_path_normalize.txt
Normal 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"]`.
|
||||
34
site/doc/pages/route_path_sanitize.txt
Normal file
34
site/doc/pages/route_path_sanitize.txt
Normal 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.
|
||||
19
site/doc/pages/sqlite_affected_rows.txt
Normal file
19
site/doc/pages/sqlite_affected_rows.txt
Normal 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));
|
||||
```
|
||||
28
site/doc/pages/sqlite_connect.txt
Normal file
28
site/doc/pages/sqlite_connect.txt
Normal 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.
|
||||
14
site/doc/pages/sqlite_disconnect.txt
Normal file
14
site/doc/pages/sqlite_disconnect.txt
Normal 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.
|
||||
15
site/doc/pages/sqlite_error.txt
Normal file
15
site/doc/pages/sqlite_error.txt
Normal 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.
|
||||
19
site/doc/pages/sqlite_insert_id.txt
Normal file
19
site/doc/pages/sqlite_insert_id.txt
Normal 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);
|
||||
```
|
||||
37
site/doc/pages/sqlite_query.txt
Normal file
37
site/doc/pages/sqlite_query.txt
Normal 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.
|
||||
@ -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.
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
|
||||
<>
|
||||
<div id="cookie-consent" style="
|
||||
|
||||
15
site/examples/uce-starter/components/basic/notfound.uce
Normal file
15
site/examples/uce-starter/components/basic/notfound.uce
Normal 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>
|
||||
</>
|
||||
}
|
||||
@ -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")
|
||||
|
||||
@ -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><? } ?>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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><? } ?>
|
||||
<? } ?>
|
||||
|
||||
39
site/examples/uce-starter/components/theme/assets.uce
Normal file
39
site/examples/uce-starter/components/theme/assets.uce
Normal 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"]);
|
||||
}
|
||||
@ -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" : "");
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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() ?>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
@ -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) ?>
|
||||
|
||||
@ -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></>
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,5 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
print(starter_page_main_html(context));
|
||||
print(context.call["fragments"]["main"].to_string());
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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) ?>
|
||||
|
||||
@ -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) ?>
|
||||
|
||||
@ -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>
|
||||
|
||||
22
site/examples/uce-starter/themes/page.uce
Normal file
22
site/examples/uce-starter/themes/page.uce
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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) ?>
|
||||
|
||||
@ -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) ?>
|
||||
|
||||
@ -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) ?>
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 { ?>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 () {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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><?
|
||||
}); ?>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
context.set_status(302, "Found");
|
||||
context.header["Location"] = "/info/";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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") == "<&>"Don'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);
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user