fix: harden UCE runtime and starter

This commit is contained in:
udo
2026-06-11 13:44:24 +00:00
parent 71ddcaf7d4
commit 7f757654b6
128 changed files with 276200 additions and 872 deletions
+41
View File
@@ -0,0 +1,41 @@
#include "demo_guard.h"
RENDER(Request& context)
{
DTree p;
p.set(context.params);
StringList routes = {"index", "dashboard", "themes", "dashboard", "workspace/projects"};
StringList unique_routes = list_unique(routes);
StringList sorted_routes = list_sort(unique_routes);
StringList labels = map(sorted_routes, [](String route) { return(to_upper(replace(route, "/", " / "))); });
DTree cards;
DTree card;
card["title"] = "Dashboard"; card["section"] = "app"; card["href"] = "?dashboard"; cards.push(card); card.clear();
card["title"] = "Themes"; card["section"] = "app"; card["href"] = "?themes"; cards.push(card); card.clear();
card["title"] = "Docs"; card["section"] = "reference"; card["href"] = "../doc/index.uce"; cards.push(card);
DTree app_cards = dtree_filter(cards, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
DTree titles = dtree_map(app_cards, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); });
DTree grouped = dtree_group_by(cards, [](DTree item, String key) { return(item["section"].to_string()); });
<><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
</head>
<body>
<h1><a href="index.uce">UCE Test Suite</a><a class="docs-link" href="../doc/index.uce?p=map">Collection Docs &rarr;</a></h1>
<h2>Collection Helpers</h2>
<p>Small data-shaping helpers are useful for route lists, nav records, cards, and other render-adjacent structures.</p>
<div class="system-info"><h3>StringList route labels</h3><pre><?= join(labels, "\n") ?></pre></div>
<div class="system-info"><h3>DTree app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
<div class="system-info"><h3>Grouped cards</h3><pre><?= json_encode(grouped) ?></pre></div>
<details>
<summary>Request Parameters</summary>
<pre><?= var_dump(p) ?></pre>
</details>
</body>
</html></>
}
+2
View File
@@ -42,6 +42,7 @@ RENDER(Request& context)
<? render_card("preprocessor-comments.uce", "Preprocessor Comments", "Regression coverage for comment parsing in templates"); ?>
<? render_card("regex.uce", "Regular Expressions", "PCRE2 matching, captures, replacement, and splitting"); ?>
<? render_card("string.uce", "String", "String operations"); ?>
<? render_card("collections.uce", "Collection Helpers", "Map, filter, group, pick, and omit render data"); ?>
<? render_card("str_replace.uce", "String Replace", "Search and replace in strings"); ?>
<? render_card("utf8.uce", "UTF-8", "Unicode string handling"); ?>
<? render_card("random.uce", "RNG / Noise", "Random generation and noise"); ?>
@@ -60,6 +61,7 @@ RENDER(Request& context)
<? if(allow_server_demos) { render_card("file_append.uce", "File Append", "Append data to files"); } ?>
<? if(allow_server_demos) { render_card("shell.uce", "Shell", "Execute shell commands"); } ?>
<? if(allow_server_demos) { render_card("memcached.uce", "Memcached", "Memcached key-value store"); } ?>
<? if(allow_server_demos) { render_card("sqlite.uce", "SQLite", "Embedded SQLite database connector"); } ?>
<? if(allow_server_demos) { render_card("mysql.uce", "MySQL", "MySQL database connector"); } ?>
<div class="grid-heading">Advanced</div>
+54
View File
@@ -0,0 +1,54 @@
#include "demo_guard.h"
RENDER(Request& context)
{
if(!test_demo_request_allowed(context))
{
test_demo_render_restricted_html(context, "SQLite demo", "write to a server-side SQLite database");
return;
}
String db_path = "/tmp/uce-demo-sqlite.sqlite";
SQLite* db = sqlite_connect(db_path);
sqlite_query(db, "create table if not exists notes(id integer primary key autoincrement, body text not null, created_at text not null)");
if(context.params["REQUEST_METHOD"] == "POST" && trim(context.post["body"]) != "")
{
StringMap params;
params["body"] = trim(context.post["body"]);
params["created_at"] = time_format_utc("%Y-%m-%d %H:%M:%S");
sqlite_query(db, "insert into notes(body, created_at) values(:body, :created_at)", params);
sqlite_disconnect(db);
redirect("sqlite.uce", 303);
return;
}
DTree notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10");
String error = sqlite_error(db);
?><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
<link rel="stylesheet" href="style.css?v=<?= time() ?>"></link>
</head>
<body>
<h1><a href="index.uce">UCE Demo</a> / SQLite</h1>
<div class="system-info">
<p>This demo stores rows in <code><?= db_path ?></code> with runtime SQLite helpers and named prepared parameters.</p>
<form method="post">
<input name="body" placeholder="note text"></input>
<button type="submit">Add note</button>
</form>
<? if(error != "ok" && error != "connected") { ?><pre><?= error ?></pre><? } ?>
</div>
<div class="test-grid">
<? notes.each([&](DTree note, String key) { ?>
<div class="test-card">
<strong>#<?= note["id"].to_string() ?> <?= note["created_at"].to_string() ?></strong>
<span><?= note["body"].to_string() ?></span>
</div>
<? }); ?>
</div>
</body>
</html><?
sqlite_disconnect(db);
}
+1
View File
@@ -1,5 +1,6 @@
Output / Invocation Functions
coming_from_react
1_RENDER
1_CLI
cli_input
+8
View File
@@ -0,0 +1,8 @@
SQLite
sqlite_connect
sqlite_disconnect
sqlite_error
sqlite_query
sqlite_insert_id
sqlite_affected_rows
+6
View File
@@ -7,6 +7,12 @@ filter
first
join
json_encode
list_every
list_find
map
list_some
list_sort
list_unique
nibble
print
strpos
+7
View File
@@ -3,6 +3,13 @@ Types
0_Request
array_merge
0_DTree
dtree_filter
dtree_group_by
dtree_keys
dtree_map
dtree_omit
dtree_pick
dtree_values
get_by_path
set_status
String
+8
View File
@@ -2,6 +2,14 @@ URI Functions
encode_query
redirect
request_context_params
request_base_url
request_query_path
request_query_route
request_script_url
route_path_is_safe
route_path_normalize
route_path_sanitize
session_id_create
parse_query
uri_decode
+412 -37
View File
@@ -2,61 +2,436 @@
Request
:sig
Request& context;
Request& context
:see
>types
request_context_params
request_script_url
request_base_url
request_query_path
request_query_route
set_status
component
component_render
unit_render
ws_message
unit_call
session_start
set_cookie
parse_query
parse_multipart
ws_message
ws_connection_id
ws_connections
ws_send
0_DTree
StringMap
UploadedFile
:content
`Request& context` is the request-local state object passed into UCE handlers. It carries incoming request data, response state, runtime metadata, and helper trees such as `context.cfg`, `context.props`, and `context.connection`.
`Request& context` is the request-local state object passed into every UCE handler:
## Core Fields
```cpp
RENDER(Request& context) { ... }
COMPONENT(Request& context) { ... }
WS(Request& context) { ... }
CLI(Request& context) { ... }
SERVE_HTTP(Request* req) { ... }
```
- `ServerState* server`: current server state
- `StringMap params`: FastCGI server parameters
- `StringMap get`: current request GET variables
- `StringMap post`: current request POST variables
- `StringMap cookies`: cookies sent by the browser
- `StringMap session`: current session data
- `String session_id`: session cookie ID
- `String session_name`: session cookie name
- `DTree cfg`: request-local configuration tree
- `DTree call`: invocation-local structured data for helpers such as component and unit calls
- `DTree connection`: broker-owned WebSocket connection state that persists across `WS(Request& context)` calls for the same socket
- `std::vector<UploadedFile> uploaded_files`: files uploaded in the current request
- `StringMap header`: headers to send back to the browser
- `StringList set_cookies`: cookies queued for the response
- `u64 random_seed`: current request noise seed
- `u64 random_index`: current request noise index position
It is the main bridge between the runtime and page code. It contains incoming request data, response state, output buffers, per-request scratch trees, session/cookie state, WebSocket metadata, and runtime diagnostics.
## Response Control
## Handler Lifetime
`context.set_status(s32 code[, String reason])` sets the HTTP status line and updates `context.flags.status`. When `reason` is omitted, UCE uses a built-in standard reason phrase for common status codes.
A fresh `Request` is created for each HTTP/CLI/custom-server request. Component and unit calls normally share that same object, so state placed on `context.call`, `context.header`, `context.session`, or `context.cfg` is visible to later components in the same request.
## Flags And Stats
For WebSockets, each incoming message is delivered as its own `Request`, but `context.connection` points at broker-owned per-socket state that persists for the lifetime of that WebSocket connection.
- `bool flags.log_request`: controls whether the request should be logged
- `u32 stats.bytes_written`
- `f64 stats.time_init`
- `f64 stats.time_start`
- `f64 stats.time_end`
`ONCE(Request& context)` hooks run once per request, per resolved unit file, before the first `RENDER`, `COMPONENT`, `CLI`, or matching entrypoint from that unit.
## Common Usage Notes
## Incoming Request Maps
- `context.cfg` is the usual place for structured configuration. Use `context.cfg.get_by_path("path/to/value")` for deep reads.
- `context.props` carries invocation data for component calls and unit calls.
- `context.in` carries the current request body, and for `WS(Request& context)` it is the current WebSocket message payload.
- `context.params["WS_..."]` exposes the current WebSocket message metadata directly on the request parameter map.
- `context.connection` is only meaningful for WebSocket traffic and persists for the lifetime of the connection.
### `context.params` — server/runtime parameters
`unit_render(String file_name, [Request& context])` invokes another UCE file using the current or supplied request context.
Type: `StringMap`
This is the low-level parameter map from FastCGI/direct HTTP plus UCE-populated convenience fields. It is closest to PHP `$_SERVER`.
Common CGI/FastCGI-style keys include:
- `REQUEST_METHOD`: `GET`, `POST`, etc.
- `REQUEST_URI`: raw request URI where available
- `DOCUMENT_URI`: normalized request path where available
- `SCRIPT_NAME`: script path where available
- `SCRIPT_FILENAME`: resolved filesystem path of the active UCE unit
- `QUERY_STRING`: raw query string
- `DOCUMENT_ROOT`: web root used by the frontend/backend
- `CONTENT_TYPE`: request body content type
- `CONTENT_LENGTH`: request body length
- `HTTP_COOKIE`: raw cookie header
- `HTTP_HOST`, `HTTP_USER_AGENT`, `HTTP_ACCEPT`, and other `HTTP_...` headers supplied by the frontend
UCE also populates convenience route/link fields before handlers run:
- `SCRIPT_URL`: canonical script URL; `/index.uce` is collapsed to the containing directory URL
- `BASE_URL`: canonical directory URL for the script
- `ROUTE_PATH`: sanitized first keyless query-string segment, defaulting to `index` when no route was supplied; empty when unsafe input was rejected
- `ROUTE_PAGE`: first segment of `ROUTE_PATH`
- `ROUTE_PATH_RAW`: normalized but untrusted route input, for diagnostics only
- `ROUTE_VALID`: `1` when route input is safe, `0` when the supplied route was rejected
See `request_context_params`, `request_script_url`, `request_base_url`, `request_query_path`, `request_query_route`, and `route_path_sanitize`.
### `context.get`
Type: `StringMap`
Parsed query-string key/value parameters. This is populated from `context.params["QUERY_STRING"]` with `parse_query()`.
```cpp
String theme = first(context.get["theme"], "default");
```
For front-controller route URLs such as `/?dashboard&theme=dark`, the keyless `dashboard` segment is represented by sanitized `ROUTE_PATH`; named parameters such as `theme=dark` are available in `context.get`.
### `context.post`
Type: `StringMap`
Parsed request body parameters for ordinary `POST` requests. URL-encoded bodies are parsed with `parse_query()`. Multipart form data is parsed with `parse_multipart()` and uploaded files are listed in `context.uploaded_files`.
```cpp
if(context.params["REQUEST_METHOD"] == "POST")
String email = context.post["email"];
```
### `context.cookies`
Type: `StringMap`
Cookies sent by the client, parsed from `HTTP_COOKIE`. `set_cookie()` also updates this map after queuing a response cookie, so later code in the same request can observe the new value.
### `context.in`
Type: `String`
Raw request body. For WebSocket handlers, this is the current message payload.
Use this for JSON APIs:
```cpp
DTree body = json_decode(context.in);
```
### `context.uploaded_files`
Type: `std::vector<UploadedFile>`
Each `UploadedFile` contains:
- `file_name`: original submitted filename
- `tmp_name`: temporary server-side upload path
- `size`: uploaded byte count
Use this with multipart form posts.
## Session State
### `context.session`
Type: `StringMap`
Session data loaded by `session_start()`. UCE does not load sessions automatically for every request; call `session_start()` before reading/writing session data.
```cpp
session_start();
context.session["user_id"] = "42";
```
At the end of a successful request, modified session data is saved automatically if a session is active.
### `context.session_id` and `context.session_name`
The active session ID and cookie name after `session_start()`.
Related helpers:
- `session_start()`
- `session_destroy()`
- `session_id_create()`
- `set_cookie()`
## Per-request Structured Trees
### `context.call`
Type: `DTree`
General request-local scratch/configuration tree. It is shared by the page, components, and unit calls participating in the current request. Use it for app-level request state, fragments, router results, page type, page title, and other values that need to be read by later components.
Examples from front-controller style apps:
```cpp
context.call["route"] = request_query_route(context);
context.call["app"]["page_type"] = "html";
context.call["fragments"]["main"] = captured_html;
```
Prefer clear top-level names when state is app-wide (`route`, `fragments`) and nested app names only when the state is truly owned by that app (`app/page_title`, `app/page_type`).
### `context.cfg`
Type: `DTree`
Request-local structured configuration. The runtime does not fill this with application config by default; application code may assign it during boot/setup:
```cpp
context.cfg = get_config();
```
This is separate from `context.server->config`, which is the runtime/server string config from `/etc/uce/settings.cfg`.
Use `get_by_path()` for non-mutating deep reads:
```cpp
String site_name = context.cfg.get_by_path("site/name").to_string();
```
### `context.props`
Type: `DTree`
Invocation-local props for `component()`, `component_render()`, and macro-style `unit_call()` entrypoints. During a component call, the runtime temporarily replaces `context.props` with the props passed to that component and restores the previous value after the call returns.
```cpp
DTree props;
props["title"] = "Dashboard";
print(component("components/card", props, context));
```
### `context.connection`
Type: `DTree`
WebSocket connection-local state. Mutations persist across `WS(Request& context)` calls for the same socket.
```cpp
context.connection["message_count"] = context.connection["message_count"].to_u64() + 1;
```
Only meaningful for WebSocket handlers.
## Response State
### `context.response_code`
Type: `String`
The raw status line. Usually use `context.set_status(...)` instead of writing this directly.
### `context.header`
Type: `StringMap`
Response headers to emit. Header names are case-sensitive as written.
```cpp
context.header["Content-Type"] = "application/json";
context.header["Location"] = "/info/";
```
### `context.set_cookies`
Type: `StringList`
Queued `Set-Cookie` header lines. Prefer `set_cookie()` instead of editing this directly.
### `context.set_status(code[, reason])`
Sets the HTTP response status and `context.flags.status`.
```cpp
context.set_status(404, "Not Found");
context.set_status(302, "Found");
context.header["Location"] = app_link("dashboard", context);
```
Related helpers:
- `redirect(url[, code])`
- `set_cookie(...)`
## Output Buffers
### `context.ob_stack` and `context.ob`
Internal output-buffer stack. Most code should use helpers instead of touching these directly:
- `print(...)`
- `out(...)`
- `ob_start()`
- `ob_get()`
- `ob_get_close()`
- `ob_close()`
Common capture pattern:
```cpp
ob_start();
print(component("views/dashboard", context));
String html = ob_get_close();
context.call["fragments"]["main"] = html;
```
### `context.out` and `context.err`
Runtime output/error artifacts used by some transports and failure paths. Normal page rendering should use `print()` / output buffers.
## Request Flags
`context.flags` contains runtime booleans and the numeric status:
- `log_request`: whether the request should be logged
- `is_finished`: internal completion marker
- `status`: numeric HTTP status set by `set_status()`
- `output_closed`: internal transport state
- `params_closed`: internal transport state
- `input_closed`: internal transport state
Most page code only reads `flags.status`, if anything.
## Request Stats
`context.stats` contains counters/timing for the request:
- `bytes_written`
- `time_init`
- `time_start`
- `time_end`
- `mem_high`
- `mem_alloc`
- `invoke_count`
These are useful for diagnostics, demos, and runtime instrumentation.
## Random / Noise State
- `random_seed`
- `random_index`
Used by UCE noise/random helpers to provide request-local deterministic progression.
Related helpers include functions in the noise/hash area such as `gen_int`, `gen_float`, `gen_noise64`, and `gen_sha1`.
## WebSocket Fields
In `WS(Request& context)`, the runtime mirrors WebSocket metadata into `context.params` and `context.resources`.
Convenience `context.params` keys include:
- `WS_MESSAGE`
- `WS_CONNECTION_ID`
- `WS_SCOPE`
- `WS_CONNECTION_COUNT`
- `WS_OPCODE`
- `WS_MESSAGE_TYPE`
- `WS_DOCUMENT_URI`
Prefer WebSocket helper functions where possible:
- `ws_message()`
- `ws_connection_id()`
- `ws_scope()`
- `ws_opcode()`
- `ws_is_binary()`
- `ws_connections()`
- `ws_connection_count()`
- `ws_send()`
- `ws_send_to()`
- `ws_close()`
Use `context.connection` for per-socket structured state.
## Runtime / Resource Fields
### `context.server`
Pointer to server state. Useful mainly for low-level/runtime code. Runtime config lives at:
```cpp
context.server->config["KEY"]
```
This is a `StringMap`, separate from app-owned `context.cfg`.
### `context.resources`
Internal runtime resources and transport state. Includes sockets, MySQL handles, WebSocket state, current unit file, and parser buffers. Application code should normally use public helpers instead of editing this directly.
Notable fields:
- `is_websocket`
- `is_cli`
- `websocket_connection_id`
- `websocket_scope`
- `websocket_scope_connection_ids`
- `current_unit_file`
## Common Patterns
### Minimal page
```cpp
RENDER(Request& context)
{
<><h1>Hello <?= context.get["name"] ?></h1></>
}
```
### JSON endpoint
```cpp
RENDER(Request& context)
{
context.header["Content-Type"] = "application/json";
DTree response;
response["ok"].set_bool(true);
print(json_encode(response));
}
```
### Redirect
```cpp
RENDER(Request& context)
{
context.set_status(302, "Found");
context.header["Location"] = "/info/";
}
```
### Component props
```cpp
DTree props;
props["title"] = "Welcome";
print(component("components/card", props, context));
```
### Front-controller route
```cpp
context.call["route"] = request_query_route(context);
String route_path = context.call["route"]["l_path"].to_string();
```
Or use runtime-populated params directly:
```cpp
String route_path = context.params["ROUTE_PATH"];
```
## Related Concepts
- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, and `http_response_code()`
- JavaScript / Node.js: Express `req` and `res`, Fetch `Request`, `Headers`, cookies or session middleware, and per-connection state in WebSocket handlers
- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, output buffering, and `http_response_code()`
- JavaScript / Node.js: Express `req`/`res`, Fetch `Request`/`Response`, route params, middleware-populated locals, and per-socket WebSocket state
+16
View File
@@ -26,6 +26,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- Inside a literal block, `<?: expression ?>` emits `print(expression);` without HTML escaping.
- `#load "other.uce"` injects another UCE unit at compile time.
- `RENDER(Request& context)`, `COMPONENT(Request& context)`, `CLI(Request& context)`, `ONCE(Request& context)`, `INIT(Request& context)`, and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`.
- `ONCE`, `RENDER`, and `COMPONENT` may be followed by a preprocessor attribute line such as `@fragment head` before the opening `{`. The handler's output is then captured and appended to `context.call["fragments"]["head"]` instead of being emitted at the call site. `ONCE` defaults to `@fragment once` when no fragment is specified.
- `COMPONENT:NAME(Request& context)` is rewritten by the custom pass into an exported named component handler.
- `EXPORT` is also a normal C++ macro, but the custom pass additionally records exported declarations for metadata.
@@ -42,6 +43,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- `<?: ... ?>` becomes `print(...);` and is intended for trusted markup or already-escaped content.
- `#load "file.uce"` is replaced with a generated C++ `#include` that points at the loaded unit's preprocessed `.cpp` file under `BIN_DIRECTORY`.
- Lines beginning with `EXPORT` are scanned so their declarations can be written to a sibling `.exports.txt` file.
- `@fragment slot-name` lines immediately following `ONCE`, `RENDER`, or `COMPONENT` are removed and replaced with an output-capture guard at the start of the handler body.
- Lines beginning with `RENDER:NAME(...)` are rewritten into exported `__uce_render_NAME(...)` functions.
- Lines beginning with `COMPONENT:NAME(...)` are rewritten into exported `__uce_component_NAME(...)` functions for the component helpers.
- The final generated source is written to `BIN_DIRECTORY + src_path + "/" + source_file + ".cpp"`.
@@ -130,6 +132,18 @@ ONCE(Request& context)
}
```
One-time page assets captured for a template-controlled slot:
```cpp
ONCE(Request& context)
@fragment head
{
?><link rel="stylesheet" href="/assets/page.css" /><?
}
```
The page template can then render `context.call["fragments"]["head"]` inside `<head>`.
## Rules
- Literal mode can start on either `<>` or `?>`.
@@ -155,6 +169,8 @@ ONCE(Request& context)
- Inspect the generated file under `BIN_DIRECTORY` first. That file shows the exact C++ produced by the UCE preprocessor.
- Compiler errors usually point back to the `.uce` source because the preprocessor inserts `#line 1`, but the generated `.cpp` is still the best place to inspect expansion problems.
- Compile failures are reported with the source path, generated C++ path, compile-output artifact path, an excerpt when UCE can identify a line, and the raw compiler output from the configured compile script.
- Runtime request failures include the request/script path, generated C++ path, a hint about inspecting template delimiters and recent component/unit calls, and a native trace when available.
- If a `#load` include looks wrong, check the current file's directory, the configured `BIN_DIRECTORY`, and whether the loaded page already produced its own generated `.cpp`.
## Related Concepts
+69
View File
@@ -0,0 +1,69 @@
:title
Coming from React, Next, or Remix
:sig
UCE orientation for React-framework developers
:see
1_RENDER
1_COMPONENT
component
unit_render
3_C++ Preprocessor
map
filter
dtree_filter
:content
UCE is server-first C++ with a small template preprocessor. It does not try to be React, but several concepts map cleanly.
## Concept Map
- `RENDER(Request& context)` is the page/server-render entrypoint.
- `COMPONENT(Request& context)` and `COMPONENT:NAME(Request& context)` are server-rendered components.
- `context.props` is the component invocation payload, similar to props.
- `context.call` is request-local scratch state shared across units during one request.
- `context.cfg` is structured app/config data.
- `ONCE(Request& context)` is per-request setup for a unit before its first render/component entry.
- `INIT(Request& context)` is worker-local setup when a unit is loaded.
- `<?= expression ?>` is escaped interpolation; prefer it for user-visible text.
- `<?: expression ?>` is trusted raw markup output, closer to a deliberate `dangerouslySetInnerHTML` decision.
- `unit_render()` renders another page unit; `component()` returns component HTML as a string.
## Routes and Layouts
UCE does not require a framework-level router. A front controller can keep routing explicit and app-local. The starter example demonstrates this in `site/examples/uce-starter/index.uce`: it resolves a request path by checking:
1. `views/<path>.uce`
2. `views/<path>/index.uce`
3. parent index handlers such as `views/workspace/index.uce` with the last segment as a route parameter
That keeps file-based and hierarchical routing in normal UCE code instead of hiding it in the runtime.
## Data Shaping Near Render Code
The function library includes small collection helpers for common route/menu/card transformations:
```cpp
auto visible = filter(routes, [](String route) { return(route != "admin"); });
auto labels = map(visible, [](String route) { return(to_upper(route)); });
DTree app_items = dtree_filter(menu, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
DTree by_section = dtree_group_by(menu, [](DTree item, String key) { return(item["section"].to_string()); });
```
Use these when the transformation communicates intent. Prefer explicit loops when side effects or multi-step validation are the main concern.
## Assets and Islands
Global runtime APIs for assets and islands are intentionally not part of UCE core. The starter emits CSS and JavaScript from the owning unit's `ONCE(Request& context)` hook, with a few shared sibling asset components when multiple components need the same files. The only starter web-affordance helper left is `COMPONENT:island` in `components/theme/web_affordances.uce` for small progressive-enhancement modules. This keeps app policy in the app without an asset registry layer.
## Debugging
When a unit fails to compile, UCE reports the source path, generated C++ path, compile-output artifact, a source/generated excerpt when it can identify a line, and the raw compiler output. The generated C++ under `BIN_DIRECTORY` is the source of truth for what the configured compiler actually saw.
## What Not To Expect
- No client-side virtual DOM is built into UCE.
- No global file-router is imposed by the runtime.
- No JSX-like component tags are required for this workflow.
- Component children/slot syntax is intentionally deferred; use explicit props and component calls for now.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_filter
:sig
DTree dtree_filter(DTree tree, function<bool (DTree, String)> f)
:see
StringList
0_DTree
filter
:content
Keeps children for which f returns true. List-like input stays list-like.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
DTree visible = dtree_filter(items, [](DTree item, String key) { return(item["hidden"].to_bool() == false); });
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_group_by
:sig
DTree dtree_group_by(DTree tree, function<String (DTree, String)> f)
:see
StringList
0_DTree
filter
:content
Groups children into list-like buckets by the string returned from f.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
DTree by_section = dtree_group_by(menu, [](DTree item, String key) { return(item["section"].to_string()); });
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_keys
:sig
StringList dtree_keys(DTree tree)
:see
StringList
0_DTree
filter
:content
Returns map keys from a DTree. Scalar values produce an empty list.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
StringList keys = dtree_keys(context.cfg["menu"]);
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_map
:sig
DTree dtree_map(DTree tree, function<DTree (DTree, String)> f)
:see
StringList
0_DTree
filter
:content
Transforms each child. List-like input stays list-like; map input keeps keys.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
DTree titles = dtree_map(items, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); });
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_omit
:sig
DTree dtree_omit(DTree tree, StringList keys)
:see
StringList
0_DTree
filter
:content
Copies a DTree map except for selected keys.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
DTree safe_user = dtree_omit(user, {"password_hash"});
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_pick
:sig
DTree dtree_pick(DTree tree, StringList keys)
:see
StringList
0_DTree
filter
:content
Copies only selected keys from a DTree map.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
DTree public_user = dtree_pick(user, {"name", "avatar"});
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_values
:sig
DTree dtree_values(DTree tree)
:see
StringList
0_DTree
filter
:content
Returns child values as a list-like DTree.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
DTree menu_items = dtree_values(context.cfg["menu"]);
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
list_every
:sig
bool list_every(StringList items, function<bool (String)> f)
:see
StringList
0_DTree
filter
:content
Returns true when every item matches.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
bool all_named = list_every(routes, [](String s) { return(s != ""); });
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+18
View File
@@ -0,0 +1,18 @@
:title
list_filter
:sig
Use filter(StringList items, function<bool (String)> f)
:see
filter
StringList
:content
`list_filter()` was removed because `StringList` is `std::vector<String>` and the generic `filter()` helper covers the same behavior without a second implementation to maintain.
Use:
```cpp
auto visible = filter(routes, [](String s) { return(s != "admin"); });
```
+26
View File
@@ -0,0 +1,26 @@
:title
list_find
:sig
String list_find(StringList items, function<bool (String)> f, String fallback = "")
:see
StringList
0_DTree
filter
:content
Returns the first matching item or fallback.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
String route = list_find(routes, [](String s) { return(str_starts_with(s, "dashboard")); }, "index");
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+19
View File
@@ -0,0 +1,19 @@
:title
list_map
:sig
Use map(StringList items, function<String (String)> f)
:see
map
filter
StringList
:content
`list_map()` was removed because `StringList` is `std::vector<String>` and the generic `map()` helper covers the same behavior without a second implementation to maintain.
Use:
```cpp
auto upper = map(names, [](String s) { return(to_upper(s)); });
```
+26
View File
@@ -0,0 +1,26 @@
:title
list_some
:sig
bool list_some(StringList items, function<bool (String)> f)
:see
StringList
0_DTree
filter
:content
Returns true when any item matches.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
bool has_dashboard = list_some(routes, [](String s) { return(s == "dashboard"); });
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
list_sort
:sig
StringList list_sort(StringList items)
:see
StringList
0_DTree
filter
:content
Returns a sorted copy of the list.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
auto sorted = list_sort(tags);
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
list_unique
:sig
StringList list_unique(StringList items)
:see
StringList
0_DTree
filter
:content
Returns the first occurrence of each string, preserving input order.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
auto tags = list_unique({"uce", "docs", "uce"});
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+31
View File
@@ -0,0 +1,31 @@
:title
map
:sig
StringList map(StringList items, function<String (String)> f)
vector<R> map(vector<T> items, function<R (T)> f)
:params
items : list of items to transform
f : a function that returns the transformed value for each item
return value : a new list containing the transformed values
:see
>string
filter
StringList
0_DTree
:content
Returns a new list by calling `f` for each item in `items`.
```cpp
auto upper = map(names, [](String s) { return(to_upper(s)); });
```
Use `filter()` when you want to keep only matching items, and `map()` when you want to transform each item.
## Related Concepts
- JavaScript / React: `Array.prototype.map()`
- PHP: `array_map()`
+10 -1
View File
@@ -13,7 +13,16 @@ return value : a list of rows returned from executing the query
:content
Executes a MySQL query and returns the resulting data, if any.
`params` provides the query parameter values used by the statement.
`params` provides the query parameter values used by the statement. Use named `:name` placeholders only; positional `?` placeholders are rejected.
```cpp
StringMap params;
params["email"] = "ada@example.test";
DTree rows = mysql_query(m,
"select id, email from users where email = :email",
params
);
```
The result is returned as a `DTree`, which makes it easy to iterate through rows and read fields with the usual `DTree` accessors.
+21
View File
@@ -7,10 +7,31 @@ return value : a StringMap containing the parameters
:see
>uri
encode_query
request_context_params
:content
Decodes a query-string fragment such as `a=b&c=d` into a `StringMap`.
Parsing rules:
- pairs are separated on `&`
- each pair is split on the first raw `=` only
- both key and value are URL-decoded
- keyless flags such as `preview` are present with an empty string value
- empty pairs, including a trailing `&`, are ignored
- repeated keys use the last value seen
Examples:
```cpp
StringMap q = parse_query("alpha=1&token=a%3Db&preview&empty=");
// q["alpha"] == "1"
// q["token"] == "a=b"
// q["preview"] == ""
// q["empty"] == ""
```
This is useful when you need to work with URL parameter data outside the normal request parsing flow.
Related:
+16
View File
@@ -0,0 +1,16 @@
:title
request_base_url
:sig
String request_base_url(Request& context)
:see
request_script_url
request_query_route
:content
Returns the canonical directory base URL for the current script. This is useful for front-controller apps that generate links to sibling assets or query-routed pages.
```cpp
String base_url = request_base_url(context);
```
+28
View File
@@ -0,0 +1,28 @@
:title
Request Context Params
:sig
SCRIPT_URL, BASE_URL, ROUTE_PATH, ROUTE_PAGE, ROUTE_PATH_RAW, ROUTE_VALID
:see
request_script_url
request_base_url
request_query_path
request_query_route
route_path_sanitize
route_path_is_safe
0_Request
:content
UCE populates several convenience request parameters before invoking page, component, CLI, custom HTTP, or WebSocket handlers:
- `context.params["SCRIPT_URL"]`: canonical script URL, with `/index.uce` collapsed to the containing directory URL
- `context.params["BASE_URL"]`: canonical directory URL for the script
- `context.params["ROUTE_PATH"]`: sanitized first keyless query-string segment, defaulting to `index` when no route was supplied
- `context.params["ROUTE_PAGE"]`: first segment of `ROUTE_PATH`
- `context.params["ROUTE_PATH_RAW"]`: normalized but not trusted route input, for diagnostics only
- `context.params["ROUTE_VALID"]`: `1` when the supplied/defaulted route is safe, `0` when the supplied route was rejected
`ROUTE_PATH` is safe to compose under an application-controlled route root. Unsafe route input such as `..`, `.`, empty interior segments, backslashes, dots in filenames, or other non route-segment characters is rejected by the runtime and yields an empty `ROUTE_PATH` with `ROUTE_VALID=0`.
These are useful for front-controller apps that use URLs like `/?dashboard` or `/?workspace/projects` while still accepting ordinary named query parameters.
+27
View File
@@ -0,0 +1,27 @@
:title
request_query_path
:sig
String request_query_path(Request& context, String default_path = "index")
:see
request_query_route
request_context_params
route_path_sanitize
route_path_is_safe
parse_query
:content
Returns the first keyless query-string segment as a sanitized route path.
For a request such as `/?workspace/projects&theme=dark`, this returns `workspace/projects`.
When no keyless route is supplied, the result is `default_path`. When unsafe route input is supplied, the result is an empty string. Unsafe route input includes `.` or `..` segments, empty interior segments, backslashes, dots in filenames, or any character outside ASCII letters, digits, `_`, and `-`.
```cpp
String route_path = request_query_path(context);
if(route_path == "")
context.set_status(404, "Not Found");
```
The runtime-populated `context.params["ROUTE_PATH"]` uses the same sanitizer.
+29
View File
@@ -0,0 +1,29 @@
:title
request_query_route
:sig
DTree request_query_route(Request& context, String default_path = "index")
:see
request_query_path
request_context_params
route_path_sanitize
request_script_url
:content
Builds a small route tree from the first keyless query-string segment.
Fields:
- `raw_path`: normalized but untrusted route input, for diagnostics only
- `l_path`: sanitized full route path, or empty string when input was rejected
- `page`: first path segment of `l_path`, or empty string when input was rejected
- `valid`: boolean; true when `l_path` is safe
```cpp
DTree route = request_query_route(context);
if(route["valid"].to_bool())
print("route=", route["l_path"].to_string());
```
This supports front-controller apps that use URLs such as `/?dashboard` or `/?workspace/projects` while still allowing ordinary named query parameters alongside the route. The returned `l_path` is already sanitized for composing under an application-controlled route root.
+16
View File
@@ -0,0 +1,16 @@
:title
request_script_url
:sig
String request_script_url(Request& context)
:see
request_base_url
request_query_route
:content
Returns the request script URL from `DOCUMENT_URI` / `SCRIPT_NAME`, canonicalizing a front-controller URL ending in `/index.uce` to the containing directory URL.
```cpp
String script_url = request_script_url(context);
```
+24
View File
@@ -0,0 +1,24 @@
:title
route_path_is_safe
:sig
bool route_path_is_safe(String path)
:see
route_path_sanitize
route_path_normalize
request_query_path
request_query_route
:content
Returns whether a normalized route path is safe to use as a route-derived file path segment.
A safe route path contains only non-empty segments made from ASCII letters, digits, `_`, and `-`. The segments `.` and `..` are rejected.
```cpp
route_path_is_safe("workspace/projects"); // true
route_path_is_safe("workspace/../admin"); // false
route_path_is_safe("view.uce"); // false
```
Most request code should use runtime-populated `context.params["ROUTE_PATH"]` or `request_query_route()` instead of calling this directly.
+20
View File
@@ -0,0 +1,20 @@
:title
route_path_normalize
:sig
String route_path_normalize(String path)
:see
route_path_sanitize
route_path_is_safe
request_query_path
request_query_route
:content
Trims whitespace and removes leading/trailing `/` from a route path without deciding whether the path is safe.
```cpp
route_path_normalize(" /workspace/projects/ "); // "workspace/projects"
```
For route data that may be used to compose file paths, prefer `route_path_sanitize()` or runtime-populated `context.params["ROUTE_PATH"]`.
+34
View File
@@ -0,0 +1,34 @@
:title
route_path_sanitize
:sig
String route_path_sanitize(String path, String default_path = "index")
:see
route_path_is_safe
route_path_normalize
request_query_path
request_query_route
request_context_params
:content
Normalizes and validates a route path for file-backed routing.
Rules:
- leading and trailing `/` are removed
- an empty path becomes `default_path`
- every path segment must be non-empty
- `.` and `..` segments are rejected
- only ASCII letters, digits, `_`, and `-` are accepted inside a segment
- unsafe input returns an empty string
Use this instead of manually checking app routes before composing file paths.
```cpp
route_path_sanitize("/workspace/projects/"); // "workspace/projects"
route_path_sanitize("../secret"); // ""
route_path_sanitize(""); // "index"
```
`request_query_path()`, `request_query_route()`, and the runtime-populated `ROUTE_PATH` params already use this sanitizer.
+19
View File
@@ -0,0 +1,19 @@
:sig
u32 sqlite_affected_rows(SQLite* db)
:params
db : pointer to an active SQLite connection
return value : number of rows changed by the most recent statement
:see
>sqlite
sqlite_query
sqlite_insert_id
:content
Returns the number of rows changed by the most recent insert, update, or delete statement on the connection.
```cpp
sqlite_query(db, "update users set visits = visits + 1 where id = :id", params);
print(sqlite_affected_rows(db));
```
+28
View File
@@ -0,0 +1,28 @@
:sig
SQLite* sqlite_connect(String path)
:params
path : filesystem path to the SQLite database file
return value : pointer to a SQLite connection struct
:see
>sqlite
sqlite_query
sqlite_error
sqlite_disconnect
:content
Opens an SQLite database file and returns a connection handle.
UCE opens SQLite with serialized/full-mutex connection mode, sets a busy timeout, enables foreign keys, and applies WAL-oriented defaults:
```sql
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
```
SQLite itself handles file locking and one-writer/many-reader concurrency. UCE does not add a separate app-level database lock.
Connections are registered for request cleanup, but explicit `sqlite_disconnect()` is still preferred when you are done with the handle.
+14
View File
@@ -0,0 +1,14 @@
:sig
void sqlite_disconnect(SQLite* db)
:params
db : pointer to an active SQLite connection
:see
>sqlite
sqlite_connect
:content
Closes an SQLite connection and deletes the UCE connection wrapper.
UCE also cleans up SQLite connections that remain open at request end, but explicit disconnects keep resource lifetime local and clear.
+15
View File
@@ -0,0 +1,15 @@
:sig
String sqlite_error(SQLite* db)
:params
db : pointer to an SQLite connection
return value : latest connector/SQLite status message
:see
>sqlite
sqlite_query
:content
Returns the latest SQLite connector status or error message.
Successful calls usually set the message to `ok` or `connected`. Failed prepare, bind, step, pragma, or open operations include connector context plus SQLite's own error text.
+19
View File
@@ -0,0 +1,19 @@
:sig
u64 sqlite_insert_id(SQLite* db)
:params
db : pointer to an active SQLite connection
return value : last inserted rowid
:see
>sqlite
sqlite_query
sqlite_affected_rows
:content
Returns SQLite's last inserted rowid for the connection after an insert statement.
```cpp
sqlite_query(db, "insert into notes(body) values(:body)", params);
u64 id = sqlite_insert_id(db);
```
+37
View File
@@ -0,0 +1,37 @@
:sig
DTree sqlite_query(SQLite* db, String q)
DTree sqlite_query(SQLite* db, String q, StringMap params)
:params
db : pointer to an active SQLite connection
q : SQL statement
params : optional named parameter map
return value : list of result rows as a DTree
:see
>sqlite
sqlite_connect
sqlite_error
sqlite_insert_id
sqlite_affected_rows
0_DTree
:content
Executes one SQLite statement and returns result rows as a `DTree` array. Multi-statement SQL strings are rejected so migrations cannot silently run only their first statement.
Use named parameters with `:name` placeholders only. Positional `?` placeholders and SQLite's other named marker forms (`@name`, `$name`) are rejected so UCE SQLite queries use the same placeholder style as the MySQL helper. UCE binds parameters with SQLite prepared statements; it does not substitute values into the SQL string.
```cpp
SQLite* db = sqlite_connect("/tmp/app.sqlite");
StringMap params;
params["email"] = "ada@example.test";
DTree rows = sqlite_query(db,
"select id, email from users where email = :email",
params
);
```
Result rows are objects keyed by column name. SQLite integer, float, text, blob, and null values are converted to DTree values. Blob values are returned as byte strings.
For statements that do not return rows, inspect `sqlite_affected_rows()` or `sqlite_insert_id()` after the call.
+12 -6
View File
@@ -17,13 +17,19 @@ Theme rendering is split the same way as the PHP starter:
- `themes/common/page.json.uce`
- `themes/<theme>/page.html.uce`
Those page templates are the authoritative shell layer, and in the UCE port they are proper `COMPONENT(...)` units rather than standalone page entrypoints. `index.uce` resolves the routed view, captures the `main` fragment, then hands off once into `themes/common/page.*.uce` or `themes/<theme>/page.html.uce`, matching the PHP starter's page-layer flow without an extra shell implementation.
Those page templates are the authoritative shell layer, and in the UCE port they are proper `COMPONENT(...)` units rather than standalone page entrypoints. `index.uce` owns the app-local router, captures the `main` fragment, then hands off to `themes/page.uce`. That component resolves `context.call["app"]["page_type"]` by checking `themes/<current-theme>/page.<page_type>.uce` first and then `themes/common/page.<page_type>.uce`, matching the PHP starter's page-layer flow without an extra shell implementation.
The example uses query-string routing in the same style as the PHP starter:
The router is intentionally normal UCE code instead of a runtime feature. It checks `views/<path>.uce`, then `views/<path>/index.uce`, then parent index handlers such as `views/workspace/index.uce` with the last segment stored as `context.call["route"]["param"]`. Matching view files are invoked with `component()`, and view files expose `COMPONENT(Request& context)` rather than `RENDER(Request& context)` because they are intended to render only through the central router. This gives the starter hierarchical/file-based routing while keeping policy in the app.
- `index.uce`
- `index.uce?page1`
- `index.uce?themes&theme=portal-dark`
- `index.uce?workspace/projects`
Starter-local web affordances live in `components/theme/web_affordances.uce`; currently this provides `COMPONENT:island` for progressive enhancement without adding global UCE runtime APIs. Component-specific CSS/JS is emitted by `ONCE(Request& context)` in the component unit that owns it, or by a small shared asset component when multiple sibling components need the same files. The router's 404 body is also a normal component at `components/basic/notfound.uce`, keeping page fragments out of the front controller.
The example uses query-string routing in the same style as the PHP starter, but the canonical public URL is the directory path because `index.uce` is reached through nginx's default index/try-file behavior. Links generated by the starter therefore target:
- `/examples/uce-starter/`
- `/examples/uce-starter/?page1`
- `/examples/uce-starter/?themes&theme=portal-dark`
- `/examples/uce-starter/?workspace/projects`
Direct requests to `/examples/uce-starter/index.uce` still work, but self-links are canonicalized back to `/examples/uce-starter/`.
The demo account pages use a small file-backed user store under `/tmp/uce-starter-data/` with session-based login state.
@@ -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="
@@ -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><? } ?>
<? } ?>
@@ -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";
+48 -14
View File
@@ -1,29 +1,63 @@
#load "lib/app.uce"
#load "lib/app.uce"
DTree starter_router_result(String file, String param = "")
{
DTree result;
result["file"] = file;
if(param != "")
result["param"] = param;
return(result);
}
DTree starter_router_resolve(Request& context)
{
String lpath = context.call["route"]["l_path"].to_string();
if(lpath == "")
return(DTree());
String exact = "views/" + lpath + ".uce";
if(file_exists(exact))
return(starter_router_result(exact));
String directory_index = "views/" + lpath + "/index.uce";
if(file_exists(directory_index))
return(starter_router_result(directory_index));
auto parts = split(lpath, "/");
while(parts.size() > 1)
{
String param = parts.back();
parts.pop_back();
String parent = join(parts, "/");
String parent_index = "views/" + parent + "/index.uce";
if(file_exists(parent_index))
return(starter_router_result(parent_index, param));
}
return(DTree());
}
RENDER(Request& context)
{
app_init(context);
DTree resolved = app_resolve_view(context);
DTree resolved = starter_router_resolve(context);
ob_start();
if(resolved["file"].to_string() != "")
{
if(resolved["param"].to_string() != "")
context.call["app"]["route"]["param"] = resolved["param"];
unit_render(resolved["file"].to_string(), context);
context.call["route"]["param"] = resolved["param"];
print(component(resolved["file"].to_string(), context));
}
else
{
app_not_found("The requested page does not exist.", context);
<>
<section class="card">
<h1>404 Not Found</h1>
<p><?= context.call["app"]["error"].to_string() ?></p>
</section>
</>
DTree notfound_props;
notfound_props["message"] = "The requested page does not exist.";
print(component("components/basic/notfound", notfound_props, context));
}
String main_html = ob_get_close();
context.call["app"]["fragments"]["main"] = main_html;
app_render_page(context);
context.call["fragments"]["main"] = main_html;
print(component("themes/page", context));
}
+1 -1
View File
@@ -1,4 +1,4 @@
(function (root, factory) { // I really hate this convoluted bullshit
(function (root, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
+17 -368
View File
@@ -1,158 +1,18 @@
#load "../config/settings.uce"
#load "user.class.h"
String app_fs_root(Request& context)
{
String root = context.call["app"]["fs_root"].to_string();
if(root == "")
root = cwd_get();
return(root);
}
String app_script_url(Request& context)
{
String url = context.call["app"]["script_url"].to_string();
if(url == "")
url = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
return(url);
}
String app_base_url(Request& context)
{
String base = context.call["app"]["base_url"].to_string();
if(base == "")
{
base = dirname(app_script_url(context));
if(base == "")
base = "/";
if(base[base.length() - 1] != '/')
base.append(1, '/');
}
return(base);
}
String app_fs_path(String relative, Request& context)
{
return(path_join(app_fs_root(context), relative));
}
String app_link(String path, Request& context);
String app_link(String path, StringMap params, Request& context);
void app_init(Request& context);
String app_first_route_segment(Request& context)
{
String query = context.params["QUERY_STRING"];
for(auto part : split(query, "&"))
{
if(part == "")
continue;
if(part.find("=") == String::npos)
return(uri_decode(part));
}
return("");
}
DTree app_make_route(Request& context)
{
DTree route;
String path = app_first_route_segment(context);
path = trim(path);
while(path.length() > 0 && path[0] == '/')
path = path.substr(1);
while(path.length() > 0 && path[path.length() - 1] == '/')
path = path.substr(0, path.length() - 1);
if(path == "")
path = "index";
route["l_path"] = path;
route["page"] = nibble(path, "/");
if(route["page"].to_string() == "")
route["page"] = "index";
return(route);
}
DTree app_resolve_view(Request& context, String base_dir = "views")
{
DTree result;
DTree route = context.call["app"]["route"];
String lpath = first(route["l_path"].to_string(), "index");
String base = trim(base_dir);
if(base != "" && base[base.length() - 1] == '/')
base = base.substr(0, base.length() - 1);
String exact = base + "/" + lpath + ".uce";
if(file_exists(exact))
{
result["file"] = exact;
return(result);
}
String dir_index = base + "/" + lpath + "/index.uce";
if(file_exists(dir_index))
{
result["file"] = dir_index;
return(result);
}
auto parts = split(lpath, "/");
if(parts.size() > 1)
{
String param = parts.back();
parts.pop_back();
String parent = join(parts, "/");
String parent_index = base + "/" + parent + "/index.uce";
if(file_exists(parent_index))
{
result["file"] = parent_index;
result["param"] = param;
return(result);
}
}
return(result);
}
void app_register_css(String path, Request& context)
{
context.call["app"]["assets"]["css"][path] = path;
}
void app_register_js(String path, Request& context)
{
context.call["app"]["assets"]["js"][path] = path;
}
String app_asset_url(String path, Request& context)
{
String url = app_base_url(context) + path;
String fs_path = app_fs_path(path, context);
String url = context.params["BASE_URL"] + path;
String fs_path = path_join(dirname(context.params["SCRIPT_FILENAME"]), path);
if(file_exists(fs_path))
url += "?v=" + std::to_string((u64)file_mtime(fs_path));
return(url);
}
void app_render_registered_css(Request& context)
{
context.call["app"]["assets"]["css"].each([&](DTree item, String key) {
String path = item.to_string();
if(path != "")
{
<><link rel="stylesheet" href="<?= app_asset_url(path, context) ?>" /></>
}
});
}
void app_render_registered_js(Request& context)
{
context.call["app"]["assets"]["js"].each([&](DTree item, String key) {
String path = item.to_string();
if(path != "")
{
<><script src="<?= app_asset_url(path, context) ?>"></script></>
}
});
}
String app_link(String path, Request& context)
{
StringMap params;
@@ -170,249 +30,38 @@ String app_link(String path, StringMap params, Request& context)
query += "&" + extra;
else if(query == "")
query = extra;
String url = app_script_url(context);
String url = context.params["SCRIPT_URL"];
if(query != "")
url += "?" + query;
return(url);
}
void app_redirect(String path, Request& context)
{
context.set_status(302, "Found");
context.header["Location"] = app_link(path, context);
}
void app_not_found(String message, Request& context)
{
context.set_status(404, "Not Found");
context.call["app"]["error"] = message;
context.call["app"]["page_title"] = "404 Not Found";
}
String app_menu_href(String menu_key, DTree menu_item, Request& context)
{
if(menu_item["external"].to_string() != "")
return("/" + menu_key);
return(app_link(menu_key, context));
}
String app_html_class(Request& context)
{
String classes = "no-js";
if(context.cfg.get_by_path("theme/mode").to_string() == "dark")
classes += " dark-theme";
return(classes);
}
String app_page_main_html(Request& context)
{
String main_html = context.props["main_html"].to_string();
if(main_html == "")
main_html = context.call["app"]["fragments"]["main"].to_string();
return(main_html);
}
bool app_bool_value(DTree value, bool fallback = false)
{
String raw = trim(value.to_string());
if(raw == "")
return(fallback);
return(
raw != "0" &&
raw != "false" &&
raw != "FALSE" &&
raw != "False" &&
raw != "(false)" &&
raw != "no" &&
raw != "NO" &&
raw != "No"
);
}
bool app_request_embed_mode(Request& context)
{
return(app_bool_value(context.call["app"]["embed_mode"]));
}
bool app_page_embed_mode(Request& context)
{
String embed_value = context.props["embed_mode"].to_string();
if(embed_value != "")
return(app_bool_value(context.props["embed_mode"]));
return(app_request_embed_mode(context));
}
String app_theme_page_component(Request& context)
{
String page_type = first(context.call["app"]["page_type"].to_string(), "html");
if(page_type == "blank")
return("themes/common/page.blank.uce");
if(page_type == "json")
return("themes/common/page.json.uce");
String theme_path = context.cfg.get_by_path("theme/path").to_string();
if(theme_path == "")
return("");
if(theme_path[theme_path.length() - 1] != '/')
theme_path.append(1, '/');
return(theme_path + "page.html.uce");
}
void app_render_page(Request& context)
{
if(context.flags.status >= 300 && context.flags.status < 400)
return;
DTree page_props;
page_props["main_html"] = context.call["app"]["fragments"]["main"];
if(context.call["app"]["json"].get_type_name() == "array")
page_props["json"] = context.call["app"]["json"];
String page_component = app_theme_page_component(context);
if(page_component != "" && file_exists(page_component))
{
print(component(page_component, page_props, context));
return;
}
context.set_status(500, "Internal Server Error");
context.header["Content-Type"] = "text/plain; charset=utf-8";
print("UCE app is missing the page template component: " + page_component);
}
// Backward-compatible aliases for older starter example units.
using StarterUser = AppUser;
void starter_boot(Request& context)
{
app_init(context);
}
String starter_link(String path, Request& context)
{
return(app_link(path, context));
}
String starter_link(String path, StringMap params, Request& context)
{
return(app_link(path, params, context));
}
void starter_register_css(String path, Request& context)
{
app_register_css(path, context);
}
void starter_register_js(String path, Request& context)
{
app_register_js(path, context);
}
String starter_asset_url(String path, Request& context)
{
return(app_asset_url(path, context));
}
void starter_render_registered_css(Request& context)
{
app_render_registered_css(context);
}
void starter_render_registered_js(Request& context)
{
app_render_registered_js(context);
}
bool starter_page_embed_mode(Request& context)
{
return(app_page_embed_mode(context));
}
String starter_page_main_html(Request& context)
{
return(app_page_main_html(context));
}
String starter_html_class(Request& context)
{
return(app_html_class(context));
}
String starter_menu_href(String menu_key, DTree menu_item, Request& context)
{
return(app_menu_href(menu_key, menu_item, context));
}
void starter_render_page(Request& context)
{
app_render_page(context);
}
void starter_redirect(String path, Request& context)
{
app_redirect(path, context);
}
void starter_not_found(String message, Request& context)
{
app_not_found(message, context);
}
DTree starter_make_route(Request& context)
{
return(app_make_route(context));
}
DTree starter_resolve_view(Request& context, String base_dir = "views")
{
return(app_resolve_view(context, base_dir));
}
void app_init(Request& context)
{
if(context.call["app"]["booted"].to_string() == "1")
return;
context.call["app"]["booted"] = "1";
context.call["app"]["fs_root"] = cwd_get();
context.call["app"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
String base_url = dirname(context.call["app"]["script_url"].to_string());
if(base_url == "")
base_url = "/";
if(base_url[base_url.length() - 1] != '/')
base_url.append(1, '/');
context.call["app"]["base_url"] = base_url;
context.call["route"]["raw_path"] = context.params["ROUTE_PATH_RAW"];
context.call["route"]["l_path"] = context.params["ROUTE_PATH"];
context.call["route"]["page"] = context.params["ROUTE_PAGE"];
context.call["route"]["valid"].set_bool(context.params["ROUTE_VALID"] != "0");
DTree config = get_config();
String requested_theme = first(context.get["theme"], context.cookies["app_theme"], config["theme"]["key"].to_string());
if(config["theme"]["options"][requested_theme].to_string() == "" && config["theme"]["options"][requested_theme].get_type_name() != "array")
session_start();
String requested_theme = first(context.get["theme"], context.session["app_theme"], config["theme"]["key"].to_string());
if(config["theme"]["options"][requested_theme].get_type_name() != "array")
requested_theme = config["theme"]["key"].to_string();
if(context.get["theme"] != "")
context.session["app_theme"] = requested_theme;
config["theme"]["options"][requested_theme].each([&](DTree item, String key) {
DTree selected_theme = config["theme"]["options"][requested_theme];
selected_theme.each([&](DTree item, String key) {
config["theme"][key] = item;
});
config["theme"]["key"] = requested_theme;
context.cfg = config;
context.call["cfg"].set_reference(&context.cfg);
context.call["app"]["config"].set_reference(&context.cfg);
context.call["app"]["route"] = app_make_route(context);
context.call["app"]["page_type"] = "html";
context.call["app"]["page_title"] = first(config["menu"][context.call["app"]["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
context.call["app"]["embed_mode"].set_bool(context.get["embed"] != "");
if(context.get["theme"] != "" && context.get["theme"] == requested_theme)
{
set_cookie("app_theme", requested_theme, time() + (86400 * 180));
context.cookies["app_theme"] = requested_theme;
}
session_start();
context.call["app"]["page_title"] = first(config["menu"][context.call["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
AppUser(context).is_signed_in();
app_register_css(context.cfg.get_by_path("theme/path").to_string() + "css/style.css", context);
app_register_css("themes/common/fontawesome/css/all.min.css", context);
app_register_js("js/u-query.js", context);
app_register_js("js/morphdom.js", context);
app_register_js("js/site.js", context);
}
@@ -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
View File
@@ -0,0 +1,22 @@
#load "../lib/app.uce"
COMPONENT(Request& context)
{
if(context.flags.status >= 300 && context.flags.status < 400)
return;
String page_type = first(context.call["app"]["page_type"].to_string(), "html");
String theme_path = "../" + context.cfg.get_by_path("theme/path").to_string();
String page_component = theme_path + "page." + page_type + ".uce";
if(!file_exists(page_component))
page_component = "../themes/common/page." + page_type + ".uce";
if(file_exists(page_component))
print(component(page_component, context));
else
{
context.set_status(500, "Internal Server Error");
context.header["Content-Type"] = "text/plain; charset=utf-8";
print("UCE starter is missing a page template for page type: " + page_type);
}
}
@@ -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;
+5 -4
View File
@@ -1,10 +1,11 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Features";
starter_register_css("views/marketing.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/marketing.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Features";
<>
<h1>Features</h1>
<p class="card">This page exists in the UCE port so the starter menu resolves cleanly as a full route, while still reusing the same marketing components as the home page.</p>
+2 -4
View File
@@ -1,10 +1,8 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Gauges";
starter_register_css("themes/common/css/gauges.css", context);
context.call["app"]["page_title"] = "Gauges";
f64 pi = 3.14159265358979323846;
DTree props;
+14 -6
View File
@@ -1,10 +1,11 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Home";
starter_register_css("views/marketing.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/marketing.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Home";
DTree props;
@@ -79,13 +80,13 @@ RENDER(Request& context)
?><div class="component-card">
<h3><?= theme["label"].to_string() ?></h3>
<p><?= theme["description"].to_string() ?></p>
<a class="btn" href="<?= starter_link("themes", theme_params, context) ?>">Preview Theme</a>
<a class="btn" href="<?= app_link("themes", theme_params, context) ?>">Preview Theme</a>
</div><?
}); ?>
<div class="component-card">
<h3>Starter Themes</h3>
<p>The original light and dark starter skins remain available in the same gallery for side-by-side checks.</p>
<a class="btn btn-secondary" href="<?= starter_link("themes", context) ?>">Open Gallery</a>
<a class="btn btn-secondary" href="<?= app_link("themes", context) ?>">Open Gallery</a>
</div>
</div>
</div>
@@ -98,9 +99,16 @@ RENDER(Request& context)
props["secondary_text"] = "View GitHub";
print(component("../components/example/marketing_blocks:CTA_SECTION", props, context));
DTree island_props;
island_props["name"] = "StarterGuidelines";
island_props["id"] = "starter-guidelines-island";
island_props["props"]["route"] = context.call["route"]["l_path"].to_string();
island_props["props"]["enhancement"] = "progressive";
<>
<div class="card">
<h2>Component Development Guidelines</h2>
<?: component("../components/theme/web_affordances:island", island_props, context) ?>
<div class="guidelines-grid">
<div class="guideline-item" style="border-left-color: var(--primary);"><span class="guideline-icon">🏷️</span><span>Use semantic HTML5 elements</span></div>
<div class="guideline-item" style="border-left-color: var(--secondary);"><span class="guideline-icon">🎨</span><span>Implement CSS custom properties for theming</span></div>
+5 -4
View File
@@ -1,10 +1,11 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Components";
starter_register_css("views/marketing.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/marketing.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Components";
<>
<h1>Component System</h1>
@@ -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>
+3 -4
View File
@@ -1,16 +1,15 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Ajaxy";
context.call["app"]["page_title"] = "Ajaxy";
<>
<h1>Ajax Demo</h1>
<div id="page2-section1">
<p>This is the content of Page 2. You can add more information here.</p>
<p>Sed at dolor leo. Morbi a tellus sed nisl dictum ultricies sit amet at purus. Nam mattis metus sed nunc egestas convallis.</p>
<br/>
<button id="page2-load-button" data-load-url="<?= starter_link("page2-section1", context) ?>" type="button">Load new text</button>
<button id="page2-load-button" data-load-url="<?= app_link("page2-section1", context) ?>" type="button">Load new text</button>
</div>
<script>
(function () {
@@ -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;
}
}
+9 -13
View File
@@ -1,26 +1,25 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Themes";
starter_register_css("views/themes.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/themes.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Themes";
String current_theme = context.cfg.get_by_path("theme/key").to_string();
<>
<div class="theme-gallery-shell">
<section class="theme-gallery-hero card">
<span class="theme-gallery-kicker">Starter Theme Gallery</span>
<h1>Compare Themes Side by Side</h1>
<p>The gallery renders the same preview content in each theme family. This makes it easier to judge shell fit, typography, token balance, and embed behavior.</p>
<h1>Compare Themes</h1>
<p>Open each theme preview directly to judge shell fit, typography, and token balance.</p>
</section>
<div class="theme-gallery-grid">
<? context.cfg.get_by_path("theme/options").each([&](DTree theme_info, String theme_key) {
StringMap params;
params["theme"] = theme_key;
String preview_href = starter_link("theme-preview", params, context);
params["embed"] = "1";
String iframe_href = starter_link("theme-preview", params, context);
String preview_href = app_link("theme-preview", params, context);
StringMap home_params;
home_params["theme"] = theme_key;
?><section class="theme-gallery-card<?= theme_key == current_theme ? " is-active" : "" ?>">
@@ -33,10 +32,7 @@ RENDER(Request& context)
</div>
<div class="theme-gallery-actions">
<a class="btn" href="<?= preview_href ?>">Open Preview</a>
<a class="btn btn-secondary" href="<?= starter_link("", home_params, context) ?>">Open Home</a>
</div>
<div class="theme-gallery-frame-wrap">
<iframe title="<?= theme_info["label"].to_string() ?> preview" src="<?= iframe_href ?>"></iframe>
<a class="btn btn-secondary" href="<?= app_link("", home_params, context) ?>">Open Home</a>
</div>
</section><?
}); ?>
@@ -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);
}
+2 -1
View File
@@ -1,4 +1,5 @@
RENDER(Request& context)
{
context.set_status(302, "Found");
context.header["Location"] = "/info/";
}
}
+43 -2
View File
@@ -18,13 +18,19 @@ RENDER(Request& context)
site_tests_page_start("Core APIs", "Pure helper coverage for strings, UTF-8 splitting, DTree, and JSON encoding/decoding.");
auto comma_parts = split("alpha,beta,gamma", ",");
check("split()", comma_parts.size() == 3 && comma_parts[1] == "beta", join(comma_parts, " | "));
auto empty_delim_parts = split("alpha", "");
check("split()", comma_parts.size() == 3 && comma_parts[1] == "beta" && empty_delim_parts.size() == 1 && empty_delim_parts[0] == "alpha", join(comma_parts, " | "));
auto spaced_parts = split_space(" one two three ");
check("split_space()", spaced_parts.size() == 3 && spaced_parts[2] == "three", join(spaced_parts, " / "));
check("trim()", trim(" padded value ") == "padded value", trim(" padded value "));
StringMap kv = split_kv("\n#comment\nalpha = one\nempty=\n", '=', true, false);
StringMap http_headers = split_http_headers("GET /demo.uce?x=1 HTTP/1.1\r\nHost: example.test\r\nX-Empty:\r\n");
StringMap leading_crlf_headers = split_http_headers("\r\nGET /lead.uce HTTP/1.1\r\nHost: lead.example\r\n");
StringMap header_only = split_http_headers("Host: example.test\nX-Token: abc\n");
check("trim() / split_kv() / split_http_headers()", trim(" padded value ") == "padded value" && kv["alpha"] == "one" && kv["empty"] == "" && http_headers["REQUEST_METHOD"] == "GET" && http_headers["DOCUMENT_URI"] == "/demo.uce" && http_headers["QUERY_STRING"] == "x=1" && http_headers["HTTP_X_EMPTY"] == "" && leading_crlf_headers["REQUEST_METHOD"] == "GET" && leading_crlf_headers["DOCUMENT_URI"] == "/lead.uce" && leading_crlf_headers["HTTP_HOST"] == "lead.example" && header_only["REQUEST_METHOD"] == "" && header_only["HTTP_HOST"] == "example.test" && header_only["HTTP_X_TOKEN"] == "abc", trim(" padded value ") + " / " + var_dump(kv) + " / " + var_dump(http_headers) + " / " + var_dump(leading_crlf_headers) + " / " + var_dump(header_only));
check("replace()", replace("alpha-beta-beta", "beta", "done") == "alpha-done-done", replace("alpha-beta-beta", "beta", "done"));
check("html_escape() attribute-safe quotes", html_escape("<&>\"Don't") == "&lt;&amp;&gt;&quot;Don&#39;t", html_escape("<&>\"Don't"));
check("regex_match()", regex_match("[A-Z][a-z]+", "Alice") && !regex_match("[A-Z][a-z]+", "Alice!"), "full-string validation");
DTree regex_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", "Contact ops@example.test");
@@ -42,6 +48,41 @@ RENDER(Request& context)
check("str_starts_with()", str_starts_with("websocket-suite", "websocket"), "websocket-suite starts with websocket");
check("str_ends_with()", str_ends_with("component.uce", ".uce"), "component.uce ends with .uce");
check("to_lower() / to_upper()", to_lower("MiXeD") == "mixed" && to_upper("MiXeD") == "MIXED", to_lower("MiXeD") + " / " + to_upper("MiXeD"));
check("request context params", context.params["SCRIPT_URL"] != "" && context.params["BASE_URL"] != "" && context.params["ROUTE_PATH"] != "" && context.params["ROUTE_PAGE"] != "" && context.params["ROUTE_VALID"] == "1", "script=" + context.params["SCRIPT_URL"] + " base=" + context.params["BASE_URL"] + " route=" + context.params["ROUTE_PATH"] + " page=" + context.params["ROUTE_PAGE"] + " valid=" + context.params["ROUTE_VALID"]);
String saved_query_string = context.params["QUERY_STRING"];
context.params["QUERY_STRING"] = "workspace/projects&theme=dark";
check("request_query_path() delegates to request_query_route()", request_query_path(context) == request_query_route(context)["l_path"].to_string() && request_query_path(context) == "workspace/projects", request_query_path(context) + " / " + json_encode(request_query_route(context)));
context.params["QUERY_STRING"] = saved_query_string;
check("route path sanitizers", route_path_normalize("/workspace/projects/") == "workspace/projects" && route_path_is_safe("workspace/projects") && !route_path_is_safe("../demo") && !route_path_is_safe("workspace/../demo") && !route_path_is_safe("workspace/file.uce") && route_path_sanitize("../demo") == "" && route_path_sanitize("") == "index", route_path_sanitize("/workspace/projects/"));
StringList route_parts = {"dashboard", "index", "dashboard", "themes"};
auto unique_routes = list_unique(route_parts);
auto sorted_routes = list_sort(unique_routes);
auto upper_routes = map(sorted_routes, [](String item) { return(to_upper(item)); });
auto dashboard_routes = filter(route_parts, [](String item) { return(item == "dashboard"); });
check("map/filter/list_unique/sort/find/some/every", unique_routes.size() == 3 && sorted_routes[0] == "dashboard" && upper_routes[2] == "THEMES" && dashboard_routes.size() == 2 && list_find(route_parts, [](String item) { return(str_starts_with(item, "them")); }, "missing") == "themes" && list_some(route_parts, [](String item) { return(item == "index"); }) && list_every(unique_routes, [](String item) { return(item != ""); }), join(upper_routes, ","));
DTree nav;
DTree nav_home;
nav_home["title"] = "Home";
nav_home["section"] = "main";
nav.push(nav_home);
DTree nav_dash;
nav_dash["title"] = "Dashboard";
nav_dash["section"] = "app";
nav.push(nav_dash);
DTree nav_themes;
nav_themes["title"] = "Themes";
nav_themes["section"] = "app";
nav.push(nav_themes);
DTree empty_tree;
DTree empty_pop = empty_tree.pop();
DTree app_nav = dtree_filter(nav, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
DTree nav_titles = dtree_map(app_nav, [](DTree item, String key) { DTree title; title = item["title"].to_string(); return(title); });
DTree grouped_nav = dtree_group_by(nav, [](DTree item, String key) { return(item["section"].to_string()); });
DTree nav_dash_summary = dtree_pick(nav_dash, {"title"});
DTree nav_dash_public = dtree_omit(nav_dash, {"section"});
check("DTree collection helpers", empty_pop.to_string() == "" && app_nav.is_list() && nav_titles["0"].to_string() == "Dashboard" && grouped_nav["app"]["1"]["title"].to_string() == "Themes" && join(dtree_keys(nav_dash_summary), ",") == "title" && dtree_values(nav_dash_public)["0"].to_string() == "Dashboard", json_encode(grouped_nav));
String binary_payload = "core";
binary_payload.push_back((char)0x00);
+2 -2
View File
@@ -25,7 +25,7 @@ RENDER(Request& context)
set_cookie("site-tests-cookie", "cookie-value", time() + 300);
context.header["X-Site-Tests"] = "http-suite";
String query_src = "alpha=1&beta=two%20words&gamma=ok";
String query_src = "alpha=1&beta=two%20words&gamma=ok&token=a%3Db&keyless&empty=&trailing=ok&";
StringMap query = parse_query(query_src);
String uri_input = "alpha beta/?x=1&y=2";
String encoded = uri_encode(uri_input);
@@ -43,7 +43,7 @@ RENDER(Request& context)
check("uri_encode() / uri_decode()", decoded == uri_input, encoded + " => " + decoded);
check("uri_decode() malformed percent literals", malformed_percent == "% %A %GG ok done", malformed_percent);
check("parse_uri() empty input", empty_uri.parts["raw"] == "", var_dump(empty_uri));
check("parse_query()", query["alpha"] == "1" && query["beta"] == "two words", query_dump);
check("parse_query()", query["alpha"] == "1" && query["beta"] == "two words" && query["token"] == "a=b" && query["keyless"] == "" && query["empty"] == "" && query["trailing"] == "ok" && query.count("") == 0, query_dump);
check("set_cookie()", set_cookie_dump.find("site-tests-cookie") != String::npos && set_cookie_dump.find("cookie-value") != String::npos && set_cookie_dump.find("HttpOnly") != String::npos && set_cookie_dump.find("SameSite=Lax") != String::npos, set_cookie_dump);
check("response header mutation", context.header["X-Site-Tests"] == "http-suite", header_dump);
check("session_start()", session_id != "", "session_id=" + session_id);
+21 -11
View File
@@ -2,19 +2,29 @@
RENDER(Request& context)
{
String tests_dir = dirname(context.params["SCRIPT_FILENAME"]);
DTree manifest = site_tests_manifest(tests_dir + "/manifest.txt");
StringList entries = list_sort(ls(tests_dir));
site_tests_page_start("Coverage Index", "Public and local-only UCE regression coverage pages.");
?><div class="tests-grid"><?
site_tests_card("core.uce", "Core APIs", "Pure helper coverage for strings, regex, UTF-8, DTree, and JSON.", "public");
site_tests_card("preprocessor.uce", "Preprocessor", "Literal-output parser regression coverage.", "public");
site_tests_card("http.uce", "HTTP And Session", "Request, response, cookie, and session helpers.", "public");
site_tests_card("components.uce", "Components", "component(), props, and component rendering.", "public");
site_tests_card("markdown.uce", "Markdown", "Markdown parsing, rendering, and component hooks.", "public");
site_tests_card("units.uce", "Units", "unit_call(), lifecycle hooks, and unit metadata.", "public");
site_tests_card("websockets.ws.uce", "WebSockets", "Browser-driven WebSocket helper checks.", "public websocket");
site_tests_card("io.uce", "Filesystem", "Filesystem helpers that are restricted outside trusted networks.", "internal");
site_tests_card("zip.uce", "ZIP", "Archive helpers that create and extract temporary server-side files.", "internal");
site_tests_card("services.uce", "Sockets And Services", "Network/service helpers that are restricted outside trusted networks.", "internal");
site_tests_card("tasks.uce", "Tasks", "Background task helper coverage.", "internal");
for(String entry : entries)
{
if(!str_ends_with(entry, ".uce"))
continue;
DTree meta = manifest[entry];
String title = meta["title"].to_string();
String description = meta["description"].to_string();
String tags = meta["tags"].to_string();
if(title == "")
{
site_tests_card(entry, "Missing test metadata: " + entry, "Add this file to site/tests/manifest.txt so the dashboard and network suite stay aligned.", "metadata missing");
continue;
}
if(meta["index"].to_string() == "0")
continue;
site_tests_card(entry, title, description, tags);
}
?></div><?
site_tests_page_end();
}
+17
View File
@@ -0,0 +1,17 @@
# file|title|description|tags|expected|suite|index
index.uce|Coverage Index|Public and local-only UCE regression coverage pages.|http suite uce public|Coverage Index|1|1
core.uce|Core APIs|Pure helper coverage for strings, regex, UTF-8, DTree, and JSON.|http suite uce public|Core APIs|1|1
preprocessor.uce|Preprocessor|Literal-output parser regression coverage.|http suite uce public|top-level )" marker|1|1
http.uce|HTTP And Session|Request, response, cookie, and session helpers.|http suite uce public|HTTP And Session|1|1
components.uce|Components|component(), props, and component rendering.|http suite uce public|Components|1|1
markdown.uce|Markdown|Markdown parsing, rendering, and component hooks.|http suite uce public|Markdown|1|1
units.uce|Units|unit_call(), lifecycle hooks, and unit metadata.|http suite uce public|Units|1|1
websockets.ws.uce|WebSockets|Browser-driven WebSocket helper checks.|http suite uce public websocket|WebSockets|1|1
io.uce|Filesystem|Filesystem helpers that are restricted outside trusted networks.|http suite uce internal|Filesystem|1|1
sqlite.uce|SQLite|SQLite connector with prepared named parameters and DTree rows.|http suite uce internal sqlite|SQLite|1|1
zip.uce|ZIP|Archive helpers that create and extract temporary server-side files.|http suite uce internal|ZIP|1|1
services.uce|Sockets And Services|Network/service helpers that are restricted outside trusted networks.|http suite uce internal|Sockets And Services|1|1
tasks.uce|Tasks|Background task helper coverage.|http suite uce internal|Tasks|1|1
security_headers.uce|Security Header Sanitizer|Low-level response header sanitizer fixture covered by the security smoke suite.|security http internal fixture|security header sanitizer test|0|1
cli.uce|CLI Fixture|Local CLI socket fixture; HTTP render intentionally returns 404.|cli internal fixture|uce-unit-cli|0|0
call_helpers.uce|Unit Call Fixture|Fixture for unit_call and unit_render helper tests.|internal fixture|UNIT_RENDER_FIXTURE|0|0
+7
View File
@@ -1,5 +1,11 @@
#include "testlib.h"
ONCE(Request& context)
@fragment preprocessor-test
{
<>fragment attr once</>
}
String preprocessor_nested_literal()
{
ob_start();
@@ -35,6 +41,7 @@ RENDER(Request& context)
<?
check("raw string terminator in nested literal", contains(nested, "nested )\" marker"), nested);
check("entrypoint @fragment attribute captures output", context.call["fragments"]["preprocessor-test"].to_string() == "fragment attr once", context.call["fragments"]["preprocessor-test"].to_string());
check("inline code island after dangerous literal", true, "parser returned to C++ after rendering literal content containing )\"");
site_tests_summary(passed, failed, skipped, "Literal content containing the C++ raw-string terminator sequence must compile and render unchanged.");
+11
View File
@@ -86,6 +86,17 @@ RENDER(Request& context)
);
}
MySQL placeholder_guard;
StringMap mysql_params;
mysql_params["id"] = "1";
placeholder_guard.query("select ?", mysql_params);
String mysql_placeholder_error = placeholder_guard.error();
mark(
"mysql_query() rejects positional placeholders",
mysql_placeholder_error.find("positional ? placeholders are not supported") != String::npos ? "pass" : "fail",
mysql_placeholder_error
);
MySQL mysql;
mysql.connect("127.0.0.1", "root", "");
String mysql_error_text = trim(mysql.error());
+93
View File
@@ -0,0 +1,93 @@
#include "testlib.h"
RENDER(Request& context)
{
u64 passed = 0;
u64 failed = 0;
u64 skipped = 0;
auto check = [&](String name, bool ok, String detail)
{
site_tests_case(name, ok ? "pass" : "fail", detail);
if(ok)
passed++;
else
failed++;
};
site_tests_page_start("SQLite", "SQLite connector coverage for prepared params, result typing, metadata, and cleanup.");
String db_path = "/tmp/uce-site-tests-sqlite.sqlite";
file_unlink(db_path);
file_unlink(db_path + "-wal");
file_unlink(db_path + "-shm");
SQLite* db = sqlite_connect(db_path);
check("sqlite_connect()", db && db->connection != 0 && sqlite_error(db) == "connected", sqlite_error(db));
sqlite_query(db, "create table users(id integer primary key autoincrement, email text not null unique, visits integer not null, rating real not null)");
check("sqlite create table", sqlite_error(db) == "ok", sqlite_error(db));
sqlite_query(db, "create table dropped(id integer); insert into dropped values(1);");
String multi_statement_error = sqlite_error(db);
StringMap dropped_params;
dropped_params["name"] = "dropped";
DTree dropped_tables = sqlite_query(db, "select name from sqlite_master where type = 'table' and name = :name", dropped_params);
check("sqlite_query() rejects multi-statement SQL", multi_statement_error.find("exactly one SQL statement") != String::npos && dropped_tables["0"]["name"].to_string() == "", multi_statement_error + " tables=" + json_encode(dropped_tables));
StringMap ada;
ada["email"] = "ada@example.test";
ada["visits"] = "7";
ada["rating"] = "4.5";
sqlite_query(db, "insert into users(email, visits, rating) values(:email, :visits, :rating)", ada);
u64 ada_id = sqlite_insert_id(db);
check("sqlite named insert", ada_id == 1 && sqlite_affected_rows(db) == 1, "id=" + std::to_string(ada_id) + " affected=" + std::to_string((u64)sqlite_affected_rows(db)) + " error=" + sqlite_error(db));
StringMap bob;
bob["email"] = "bob@example.test";
bob["visits"] = "3";
bob["rating"] = "2.25";
sqlite_query(db, "insert into users(email, visits, rating) values(:email, :visits, :rating)", bob);
check("sqlite second insert", sqlite_insert_id(db) == 2 && sqlite_affected_rows(db) == 1, "id=" + std::to_string(sqlite_insert_id(db)));
StringMap query_params;
query_params["min_visits"] = "4";
DTree rows = sqlite_query(db, "select id, email, visits, rating from users where visits >= :min_visits order by id", query_params);
check("sqlite_query() rows", rows.is_array() && rows["0"]["email"].to_string() == "ada@example.test" && rows["0"]["visits"].to_s64() == 7 && rows["0"]["rating"].to_f64() > 4.49, json_encode(rows));
sqlite_query(db, "select id from users where id = ?", query_params);
String positional_error = sqlite_error(db);
check("sqlite_query() rejects positional placeholders", positional_error.find("positional ? placeholders are not supported") != String::npos, positional_error);
sqlite_query(db, "select id from users where id = @id", query_params);
String non_colon_error = sqlite_error(db);
check("sqlite_query() rejects non-colon placeholders", non_colon_error.find("only supports :name placeholders") != String::npos, non_colon_error);
sqlite_query(db, "update users set visits = visits + 1 where email = :email", ada);
check("sqlite_affected_rows()", sqlite_affected_rows(db) == 1, std::to_string((u64)sqlite_affected_rows(db)));
sqlite_query(db, "select nope from missing_table");
check("sqlite_error()", sqlite_error(db).find("sqlite prepare failed") != String::npos && sqlite_error(db).find("no such table") != String::npos, sqlite_error(db));
sqlite_disconnect(db);
String cleanup_db_path = db_path + ".cleanup";
file_unlink(cleanup_db_path);
file_unlink(cleanup_db_path + "-wal");
file_unlink(cleanup_db_path + "-shm");
SQLite* cleanup_db = sqlite_connect(cleanup_db_path);
sqlite_query(cleanup_db, "create table cleanup_probe(id integer)");
cleanup_sqlite_connections();
check("sqlite request cleanup releases tracked wrappers", context.resources.sqlite_connections.size() == 0, "tracked=" + std::to_string((u64)context.resources.sqlite_connections.size()));
sqlite_disconnect(cleanup_db);
file_unlink(cleanup_db_path);
file_unlink(cleanup_db_path + "-wal");
file_unlink(cleanup_db_path + "-shm");
file_unlink(db_path);
file_unlink(db_path + "-wal");
file_unlink(db_path + "-shm");
site_tests_summary(passed, failed, skipped, "SQLite tests write only under /tmp/uce-site-tests-sqlite.sqlite and clean up WAL/SHM sidecars.");
site_tests_page_end();
}
+23
View File
@@ -64,6 +64,29 @@ void site_tests_card(String href, String title, String description, String tags
print("</a>");
}
DTree site_tests_manifest(String manifest_path = "manifest.txt")
{
DTree manifest;
for(String line : split(file_get_contents(manifest_path), "\n"))
{
line = trim(line);
if(line == "" || line[0] == '#')
continue;
StringList parts = split(line, "|");
if(parts.size() < 7)
continue;
String file = trim(parts[0]);
manifest[file]["file"] = file;
manifest[file]["title"] = trim(parts[1]);
manifest[file]["description"] = trim(parts[2]);
manifest[file]["tags"] = trim(parts[3]);
manifest[file]["expected"] = trim(parts[4]);
manifest[file]["suite"] = trim(parts[5]);
manifest[file]["index"] = trim(parts[6]);
}
return(manifest);
}
void site_tests_summary(u64 passed, u64 failed, u64 skipped, String note = "")
{
print("<div class=\"tests-summary\">");