fix: harden UCE runtime and starter
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree p;
|
||||
p.set(context.params);
|
||||
|
||||
StringList routes = {"index", "dashboard", "themes", "dashboard", "workspace/projects"};
|
||||
StringList unique_routes = list_unique(routes);
|
||||
StringList sorted_routes = list_sort(unique_routes);
|
||||
StringList labels = map(sorted_routes, [](String route) { return(to_upper(replace(route, "/", " / "))); });
|
||||
|
||||
DTree cards;
|
||||
DTree card;
|
||||
card["title"] = "Dashboard"; card["section"] = "app"; card["href"] = "?dashboard"; cards.push(card); card.clear();
|
||||
card["title"] = "Themes"; card["section"] = "app"; card["href"] = "?themes"; cards.push(card); card.clear();
|
||||
card["title"] = "Docs"; card["section"] = "reference"; card["href"] = "../doc/index.uce"; cards.push(card);
|
||||
|
||||
DTree app_cards = dtree_filter(cards, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
|
||||
DTree titles = dtree_map(app_cards, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); });
|
||||
DTree grouped = dtree_group_by(cards, [](DTree item, String key) { return(item["section"].to_string()); });
|
||||
|
||||
<><html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
|
||||
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="index.uce">UCE Test Suite</a><a class="docs-link" href="../doc/index.uce?p=map">Collection Docs →</a></h1>
|
||||
<h2>Collection Helpers</h2>
|
||||
<p>Small data-shaping helpers are useful for route lists, nav records, cards, and other render-adjacent structures.</p>
|
||||
<div class="system-info"><h3>StringList route labels</h3><pre><?= join(labels, "\n") ?></pre></div>
|
||||
<div class="system-info"><h3>DTree app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
|
||||
<div class="system-info"><h3>Grouped cards</h3><pre><?= json_encode(grouped) ?></pre></div>
|
||||
<details>
|
||||
<summary>Request Parameters</summary>
|
||||
<pre><?= var_dump(p) ?></pre>
|
||||
</details>
|
||||
</body>
|
||||
</html></>
|
||||
}
|
||||
@@ -42,6 +42,7 @@ RENDER(Request& context)
|
||||
<? render_card("preprocessor-comments.uce", "Preprocessor Comments", "Regression coverage for comment parsing in templates"); ?>
|
||||
<? render_card("regex.uce", "Regular Expressions", "PCRE2 matching, captures, replacement, and splitting"); ?>
|
||||
<? render_card("string.uce", "String", "String operations"); ?>
|
||||
<? render_card("collections.uce", "Collection Helpers", "Map, filter, group, pick, and omit render data"); ?>
|
||||
<? render_card("str_replace.uce", "String Replace", "Search and replace in strings"); ?>
|
||||
<? render_card("utf8.uce", "UTF-8", "Unicode string handling"); ?>
|
||||
<? render_card("random.uce", "RNG / Noise", "Random generation and noise"); ?>
|
||||
@@ -60,6 +61,7 @@ RENDER(Request& context)
|
||||
<? if(allow_server_demos) { render_card("file_append.uce", "File Append", "Append data to files"); } ?>
|
||||
<? if(allow_server_demos) { render_card("shell.uce", "Shell", "Execute shell commands"); } ?>
|
||||
<? if(allow_server_demos) { render_card("memcached.uce", "Memcached", "Memcached key-value store"); } ?>
|
||||
<? if(allow_server_demos) { render_card("sqlite.uce", "SQLite", "Embedded SQLite database connector"); } ?>
|
||||
<? if(allow_server_demos) { render_card("mysql.uce", "MySQL", "MySQL database connector"); } ?>
|
||||
|
||||
<div class="grid-heading">Advanced</div>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
if(!test_demo_request_allowed(context))
|
||||
{
|
||||
test_demo_render_restricted_html(context, "SQLite demo", "write to a server-side SQLite database");
|
||||
return;
|
||||
}
|
||||
|
||||
String db_path = "/tmp/uce-demo-sqlite.sqlite";
|
||||
SQLite* db = sqlite_connect(db_path);
|
||||
sqlite_query(db, "create table if not exists notes(id integer primary key autoincrement, body text not null, created_at text not null)");
|
||||
|
||||
if(context.params["REQUEST_METHOD"] == "POST" && trim(context.post["body"]) != "")
|
||||
{
|
||||
StringMap params;
|
||||
params["body"] = trim(context.post["body"]);
|
||||
params["created_at"] = time_format_utc("%Y-%m-%d %H:%M:%S");
|
||||
sqlite_query(db, "insert into notes(body, created_at) values(:body, :created_at)", params);
|
||||
sqlite_disconnect(db);
|
||||
redirect("sqlite.uce", 303);
|
||||
return;
|
||||
}
|
||||
|
||||
DTree notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10");
|
||||
String error = sqlite_error(db);
|
||||
?><html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
|
||||
<link rel="stylesheet" href="style.css?v=<?= time() ?>"></link>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="index.uce">UCE Demo</a> / SQLite</h1>
|
||||
<div class="system-info">
|
||||
<p>This demo stores rows in <code><?= db_path ?></code> with runtime SQLite helpers and named prepared parameters.</p>
|
||||
<form method="post">
|
||||
<input name="body" placeholder="note text"></input>
|
||||
<button type="submit">Add note</button>
|
||||
</form>
|
||||
<? if(error != "ok" && error != "connected") { ?><pre><?= error ?></pre><? } ?>
|
||||
</div>
|
||||
<div class="test-grid">
|
||||
<? notes.each([&](DTree note, String key) { ?>
|
||||
<div class="test-card">
|
||||
<strong>#<?= note["id"].to_string() ?> <?= note["created_at"].to_string() ?></strong>
|
||||
<span><?= note["body"].to_string() ?></span>
|
||||
</div>
|
||||
<? }); ?>
|
||||
</div>
|
||||
</body>
|
||||
</html><?
|
||||
sqlite_disconnect(db);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
Output / Invocation Functions
|
||||
|
||||
coming_from_react
|
||||
1_RENDER
|
||||
1_CLI
|
||||
cli_input
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
SQLite
|
||||
|
||||
sqlite_connect
|
||||
sqlite_disconnect
|
||||
sqlite_error
|
||||
sqlite_query
|
||||
sqlite_insert_id
|
||||
sqlite_affected_rows
|
||||
@@ -7,6 +7,12 @@ filter
|
||||
first
|
||||
join
|
||||
json_encode
|
||||
list_every
|
||||
list_find
|
||||
map
|
||||
list_some
|
||||
list_sort
|
||||
list_unique
|
||||
nibble
|
||||
print
|
||||
strpos
|
||||
|
||||
@@ -3,6 +3,13 @@ Types
|
||||
0_Request
|
||||
array_merge
|
||||
0_DTree
|
||||
dtree_filter
|
||||
dtree_group_by
|
||||
dtree_keys
|
||||
dtree_map
|
||||
dtree_omit
|
||||
dtree_pick
|
||||
dtree_values
|
||||
get_by_path
|
||||
set_status
|
||||
String
|
||||
|
||||
@@ -2,6 +2,14 @@ URI Functions
|
||||
|
||||
encode_query
|
||||
redirect
|
||||
request_context_params
|
||||
request_base_url
|
||||
request_query_path
|
||||
request_query_route
|
||||
request_script_url
|
||||
route_path_is_safe
|
||||
route_path_normalize
|
||||
route_path_sanitize
|
||||
session_id_create
|
||||
parse_query
|
||||
uri_decode
|
||||
|
||||
+412
-37
@@ -2,61 +2,436 @@
|
||||
Request
|
||||
|
||||
:sig
|
||||
Request& context;
|
||||
Request& context
|
||||
|
||||
:see
|
||||
>types
|
||||
request_context_params
|
||||
request_script_url
|
||||
request_base_url
|
||||
request_query_path
|
||||
request_query_route
|
||||
set_status
|
||||
component
|
||||
component_render
|
||||
unit_render
|
||||
ws_message
|
||||
unit_call
|
||||
session_start
|
||||
set_cookie
|
||||
parse_query
|
||||
parse_multipart
|
||||
ws_message
|
||||
ws_connection_id
|
||||
ws_connections
|
||||
ws_send
|
||||
0_DTree
|
||||
StringMap
|
||||
UploadedFile
|
||||
|
||||
:content
|
||||
`Request& context` is the request-local state object passed into UCE handlers. It carries incoming request data, response state, runtime metadata, and helper trees such as `context.cfg`, `context.props`, and `context.connection`.
|
||||
`Request& context` is the request-local state object passed into every UCE handler:
|
||||
|
||||
## Core Fields
|
||||
```cpp
|
||||
RENDER(Request& context) { ... }
|
||||
COMPONENT(Request& context) { ... }
|
||||
WS(Request& context) { ... }
|
||||
CLI(Request& context) { ... }
|
||||
SERVE_HTTP(Request* req) { ... }
|
||||
```
|
||||
|
||||
- `ServerState* server`: current server state
|
||||
- `StringMap params`: FastCGI server parameters
|
||||
- `StringMap get`: current request GET variables
|
||||
- `StringMap post`: current request POST variables
|
||||
- `StringMap cookies`: cookies sent by the browser
|
||||
- `StringMap session`: current session data
|
||||
- `String session_id`: session cookie ID
|
||||
- `String session_name`: session cookie name
|
||||
- `DTree cfg`: request-local configuration tree
|
||||
- `DTree call`: invocation-local structured data for helpers such as component and unit calls
|
||||
- `DTree connection`: broker-owned WebSocket connection state that persists across `WS(Request& context)` calls for the same socket
|
||||
- `std::vector<UploadedFile> uploaded_files`: files uploaded in the current request
|
||||
- `StringMap header`: headers to send back to the browser
|
||||
- `StringList set_cookies`: cookies queued for the response
|
||||
- `u64 random_seed`: current request noise seed
|
||||
- `u64 random_index`: current request noise index position
|
||||
It is the main bridge between the runtime and page code. It contains incoming request data, response state, output buffers, per-request scratch trees, session/cookie state, WebSocket metadata, and runtime diagnostics.
|
||||
|
||||
## Response Control
|
||||
## Handler Lifetime
|
||||
|
||||
`context.set_status(s32 code[, String reason])` sets the HTTP status line and updates `context.flags.status`. When `reason` is omitted, UCE uses a built-in standard reason phrase for common status codes.
|
||||
A fresh `Request` is created for each HTTP/CLI/custom-server request. Component and unit calls normally share that same object, so state placed on `context.call`, `context.header`, `context.session`, or `context.cfg` is visible to later components in the same request.
|
||||
|
||||
## Flags And Stats
|
||||
For WebSockets, each incoming message is delivered as its own `Request`, but `context.connection` points at broker-owned per-socket state that persists for the lifetime of that WebSocket connection.
|
||||
|
||||
- `bool flags.log_request`: controls whether the request should be logged
|
||||
- `u32 stats.bytes_written`
|
||||
- `f64 stats.time_init`
|
||||
- `f64 stats.time_start`
|
||||
- `f64 stats.time_end`
|
||||
`ONCE(Request& context)` hooks run once per request, per resolved unit file, before the first `RENDER`, `COMPONENT`, `CLI`, or matching entrypoint from that unit.
|
||||
|
||||
## Common Usage Notes
|
||||
## Incoming Request Maps
|
||||
|
||||
- `context.cfg` is the usual place for structured configuration. Use `context.cfg.get_by_path("path/to/value")` for deep reads.
|
||||
- `context.props` carries invocation data for component calls and unit calls.
|
||||
- `context.in` carries the current request body, and for `WS(Request& context)` it is the current WebSocket message payload.
|
||||
- `context.params["WS_..."]` exposes the current WebSocket message metadata directly on the request parameter map.
|
||||
- `context.connection` is only meaningful for WebSocket traffic and persists for the lifetime of the connection.
|
||||
### `context.params` — server/runtime parameters
|
||||
|
||||
`unit_render(String file_name, [Request& context])` invokes another UCE file using the current or supplied request context.
|
||||
Type: `StringMap`
|
||||
|
||||
This is the low-level parameter map from FastCGI/direct HTTP plus UCE-populated convenience fields. It is closest to PHP `$_SERVER`.
|
||||
|
||||
Common CGI/FastCGI-style keys include:
|
||||
|
||||
- `REQUEST_METHOD`: `GET`, `POST`, etc.
|
||||
- `REQUEST_URI`: raw request URI where available
|
||||
- `DOCUMENT_URI`: normalized request path where available
|
||||
- `SCRIPT_NAME`: script path where available
|
||||
- `SCRIPT_FILENAME`: resolved filesystem path of the active UCE unit
|
||||
- `QUERY_STRING`: raw query string
|
||||
- `DOCUMENT_ROOT`: web root used by the frontend/backend
|
||||
- `CONTENT_TYPE`: request body content type
|
||||
- `CONTENT_LENGTH`: request body length
|
||||
- `HTTP_COOKIE`: raw cookie header
|
||||
- `HTTP_HOST`, `HTTP_USER_AGENT`, `HTTP_ACCEPT`, and other `HTTP_...` headers supplied by the frontend
|
||||
|
||||
UCE also populates convenience route/link fields before handlers run:
|
||||
|
||||
- `SCRIPT_URL`: canonical script URL; `/index.uce` is collapsed to the containing directory URL
|
||||
- `BASE_URL`: canonical directory URL for the script
|
||||
- `ROUTE_PATH`: sanitized first keyless query-string segment, defaulting to `index` when no route was supplied; empty when unsafe input was rejected
|
||||
- `ROUTE_PAGE`: first segment of `ROUTE_PATH`
|
||||
- `ROUTE_PATH_RAW`: normalized but untrusted route input, for diagnostics only
|
||||
- `ROUTE_VALID`: `1` when route input is safe, `0` when the supplied route was rejected
|
||||
|
||||
See `request_context_params`, `request_script_url`, `request_base_url`, `request_query_path`, `request_query_route`, and `route_path_sanitize`.
|
||||
|
||||
### `context.get`
|
||||
|
||||
Type: `StringMap`
|
||||
|
||||
Parsed query-string key/value parameters. This is populated from `context.params["QUERY_STRING"]` with `parse_query()`.
|
||||
|
||||
```cpp
|
||||
String theme = first(context.get["theme"], "default");
|
||||
```
|
||||
|
||||
For front-controller route URLs such as `/?dashboard&theme=dark`, the keyless `dashboard` segment is represented by sanitized `ROUTE_PATH`; named parameters such as `theme=dark` are available in `context.get`.
|
||||
|
||||
### `context.post`
|
||||
|
||||
Type: `StringMap`
|
||||
|
||||
Parsed request body parameters for ordinary `POST` requests. URL-encoded bodies are parsed with `parse_query()`. Multipart form data is parsed with `parse_multipart()` and uploaded files are listed in `context.uploaded_files`.
|
||||
|
||||
```cpp
|
||||
if(context.params["REQUEST_METHOD"] == "POST")
|
||||
String email = context.post["email"];
|
||||
```
|
||||
|
||||
### `context.cookies`
|
||||
|
||||
Type: `StringMap`
|
||||
|
||||
Cookies sent by the client, parsed from `HTTP_COOKIE`. `set_cookie()` also updates this map after queuing a response cookie, so later code in the same request can observe the new value.
|
||||
|
||||
### `context.in`
|
||||
|
||||
Type: `String`
|
||||
|
||||
Raw request body. For WebSocket handlers, this is the current message payload.
|
||||
|
||||
Use this for JSON APIs:
|
||||
|
||||
```cpp
|
||||
DTree body = json_decode(context.in);
|
||||
```
|
||||
|
||||
### `context.uploaded_files`
|
||||
|
||||
Type: `std::vector<UploadedFile>`
|
||||
|
||||
Each `UploadedFile` contains:
|
||||
|
||||
- `file_name`: original submitted filename
|
||||
- `tmp_name`: temporary server-side upload path
|
||||
- `size`: uploaded byte count
|
||||
|
||||
Use this with multipart form posts.
|
||||
|
||||
## Session State
|
||||
|
||||
### `context.session`
|
||||
|
||||
Type: `StringMap`
|
||||
|
||||
Session data loaded by `session_start()`. UCE does not load sessions automatically for every request; call `session_start()` before reading/writing session data.
|
||||
|
||||
```cpp
|
||||
session_start();
|
||||
context.session["user_id"] = "42";
|
||||
```
|
||||
|
||||
At the end of a successful request, modified session data is saved automatically if a session is active.
|
||||
|
||||
### `context.session_id` and `context.session_name`
|
||||
|
||||
The active session ID and cookie name after `session_start()`.
|
||||
|
||||
Related helpers:
|
||||
|
||||
- `session_start()`
|
||||
- `session_destroy()`
|
||||
- `session_id_create()`
|
||||
- `set_cookie()`
|
||||
|
||||
## Per-request Structured Trees
|
||||
|
||||
### `context.call`
|
||||
|
||||
Type: `DTree`
|
||||
|
||||
General request-local scratch/configuration tree. It is shared by the page, components, and unit calls participating in the current request. Use it for app-level request state, fragments, router results, page type, page title, and other values that need to be read by later components.
|
||||
|
||||
Examples from front-controller style apps:
|
||||
|
||||
```cpp
|
||||
context.call["route"] = request_query_route(context);
|
||||
context.call["app"]["page_type"] = "html";
|
||||
context.call["fragments"]["main"] = captured_html;
|
||||
```
|
||||
|
||||
Prefer clear top-level names when state is app-wide (`route`, `fragments`) and nested app names only when the state is truly owned by that app (`app/page_title`, `app/page_type`).
|
||||
|
||||
### `context.cfg`
|
||||
|
||||
Type: `DTree`
|
||||
|
||||
Request-local structured configuration. The runtime does not fill this with application config by default; application code may assign it during boot/setup:
|
||||
|
||||
```cpp
|
||||
context.cfg = get_config();
|
||||
```
|
||||
|
||||
This is separate from `context.server->config`, which is the runtime/server string config from `/etc/uce/settings.cfg`.
|
||||
|
||||
Use `get_by_path()` for non-mutating deep reads:
|
||||
|
||||
```cpp
|
||||
String site_name = context.cfg.get_by_path("site/name").to_string();
|
||||
```
|
||||
|
||||
### `context.props`
|
||||
|
||||
Type: `DTree`
|
||||
|
||||
Invocation-local props for `component()`, `component_render()`, and macro-style `unit_call()` entrypoints. During a component call, the runtime temporarily replaces `context.props` with the props passed to that component and restores the previous value after the call returns.
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
props["title"] = "Dashboard";
|
||||
print(component("components/card", props, context));
|
||||
```
|
||||
|
||||
### `context.connection`
|
||||
|
||||
Type: `DTree`
|
||||
|
||||
WebSocket connection-local state. Mutations persist across `WS(Request& context)` calls for the same socket.
|
||||
|
||||
```cpp
|
||||
context.connection["message_count"] = context.connection["message_count"].to_u64() + 1;
|
||||
```
|
||||
|
||||
Only meaningful for WebSocket handlers.
|
||||
|
||||
## Response State
|
||||
|
||||
### `context.response_code`
|
||||
|
||||
Type: `String`
|
||||
|
||||
The raw status line. Usually use `context.set_status(...)` instead of writing this directly.
|
||||
|
||||
### `context.header`
|
||||
|
||||
Type: `StringMap`
|
||||
|
||||
Response headers to emit. Header names are case-sensitive as written.
|
||||
|
||||
```cpp
|
||||
context.header["Content-Type"] = "application/json";
|
||||
context.header["Location"] = "/info/";
|
||||
```
|
||||
|
||||
### `context.set_cookies`
|
||||
|
||||
Type: `StringList`
|
||||
|
||||
Queued `Set-Cookie` header lines. Prefer `set_cookie()` instead of editing this directly.
|
||||
|
||||
### `context.set_status(code[, reason])`
|
||||
|
||||
Sets the HTTP response status and `context.flags.status`.
|
||||
|
||||
```cpp
|
||||
context.set_status(404, "Not Found");
|
||||
context.set_status(302, "Found");
|
||||
context.header["Location"] = app_link("dashboard", context);
|
||||
```
|
||||
|
||||
Related helpers:
|
||||
|
||||
- `redirect(url[, code])`
|
||||
- `set_cookie(...)`
|
||||
|
||||
## Output Buffers
|
||||
|
||||
### `context.ob_stack` and `context.ob`
|
||||
|
||||
Internal output-buffer stack. Most code should use helpers instead of touching these directly:
|
||||
|
||||
- `print(...)`
|
||||
- `out(...)`
|
||||
- `ob_start()`
|
||||
- `ob_get()`
|
||||
- `ob_get_close()`
|
||||
- `ob_close()`
|
||||
|
||||
Common capture pattern:
|
||||
|
||||
```cpp
|
||||
ob_start();
|
||||
print(component("views/dashboard", context));
|
||||
String html = ob_get_close();
|
||||
context.call["fragments"]["main"] = html;
|
||||
```
|
||||
|
||||
### `context.out` and `context.err`
|
||||
|
||||
Runtime output/error artifacts used by some transports and failure paths. Normal page rendering should use `print()` / output buffers.
|
||||
|
||||
## Request Flags
|
||||
|
||||
`context.flags` contains runtime booleans and the numeric status:
|
||||
|
||||
- `log_request`: whether the request should be logged
|
||||
- `is_finished`: internal completion marker
|
||||
- `status`: numeric HTTP status set by `set_status()`
|
||||
- `output_closed`: internal transport state
|
||||
- `params_closed`: internal transport state
|
||||
- `input_closed`: internal transport state
|
||||
|
||||
Most page code only reads `flags.status`, if anything.
|
||||
|
||||
## Request Stats
|
||||
|
||||
`context.stats` contains counters/timing for the request:
|
||||
|
||||
- `bytes_written`
|
||||
- `time_init`
|
||||
- `time_start`
|
||||
- `time_end`
|
||||
- `mem_high`
|
||||
- `mem_alloc`
|
||||
- `invoke_count`
|
||||
|
||||
These are useful for diagnostics, demos, and runtime instrumentation.
|
||||
|
||||
## Random / Noise State
|
||||
|
||||
- `random_seed`
|
||||
- `random_index`
|
||||
|
||||
Used by UCE noise/random helpers to provide request-local deterministic progression.
|
||||
|
||||
Related helpers include functions in the noise/hash area such as `gen_int`, `gen_float`, `gen_noise64`, and `gen_sha1`.
|
||||
|
||||
## WebSocket Fields
|
||||
|
||||
In `WS(Request& context)`, the runtime mirrors WebSocket metadata into `context.params` and `context.resources`.
|
||||
|
||||
Convenience `context.params` keys include:
|
||||
|
||||
- `WS_MESSAGE`
|
||||
- `WS_CONNECTION_ID`
|
||||
- `WS_SCOPE`
|
||||
- `WS_CONNECTION_COUNT`
|
||||
- `WS_OPCODE`
|
||||
- `WS_MESSAGE_TYPE`
|
||||
- `WS_DOCUMENT_URI`
|
||||
|
||||
Prefer WebSocket helper functions where possible:
|
||||
|
||||
- `ws_message()`
|
||||
- `ws_connection_id()`
|
||||
- `ws_scope()`
|
||||
- `ws_opcode()`
|
||||
- `ws_is_binary()`
|
||||
- `ws_connections()`
|
||||
- `ws_connection_count()`
|
||||
- `ws_send()`
|
||||
- `ws_send_to()`
|
||||
- `ws_close()`
|
||||
|
||||
Use `context.connection` for per-socket structured state.
|
||||
|
||||
## Runtime / Resource Fields
|
||||
|
||||
### `context.server`
|
||||
|
||||
Pointer to server state. Useful mainly for low-level/runtime code. Runtime config lives at:
|
||||
|
||||
```cpp
|
||||
context.server->config["KEY"]
|
||||
```
|
||||
|
||||
This is a `StringMap`, separate from app-owned `context.cfg`.
|
||||
|
||||
### `context.resources`
|
||||
|
||||
Internal runtime resources and transport state. Includes sockets, MySQL handles, WebSocket state, current unit file, and parser buffers. Application code should normally use public helpers instead of editing this directly.
|
||||
|
||||
Notable fields:
|
||||
|
||||
- `is_websocket`
|
||||
- `is_cli`
|
||||
- `websocket_connection_id`
|
||||
- `websocket_scope`
|
||||
- `websocket_scope_connection_ids`
|
||||
- `current_unit_file`
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Minimal page
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
<><h1>Hello <?= context.get["name"] ?></h1></>
|
||||
}
|
||||
```
|
||||
|
||||
### JSON endpoint
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
context.header["Content-Type"] = "application/json";
|
||||
DTree response;
|
||||
response["ok"].set_bool(true);
|
||||
print(json_encode(response));
|
||||
}
|
||||
```
|
||||
|
||||
### Redirect
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
context.set_status(302, "Found");
|
||||
context.header["Location"] = "/info/";
|
||||
}
|
||||
```
|
||||
|
||||
### Component props
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
props["title"] = "Welcome";
|
||||
print(component("components/card", props, context));
|
||||
```
|
||||
|
||||
### Front-controller route
|
||||
|
||||
```cpp
|
||||
context.call["route"] = request_query_route(context);
|
||||
String route_path = context.call["route"]["l_path"].to_string();
|
||||
```
|
||||
|
||||
Or use runtime-populated params directly:
|
||||
|
||||
```cpp
|
||||
String route_path = context.params["ROUTE_PATH"];
|
||||
```
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, and `http_response_code()`
|
||||
- JavaScript / Node.js: Express `req` and `res`, Fetch `Request`, `Headers`, cookies or session middleware, and per-connection state in WebSocket handlers
|
||||
- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, output buffering, and `http_response_code()`
|
||||
- JavaScript / Node.js: Express `req`/`res`, Fetch `Request`/`Response`, route params, middleware-populated locals, and per-socket WebSocket state
|
||||
|
||||
@@ -26,6 +26,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
|
||||
- Inside a literal block, `<?: expression ?>` emits `print(expression);` without HTML escaping.
|
||||
- `#load "other.uce"` injects another UCE unit at compile time.
|
||||
- `RENDER(Request& context)`, `COMPONENT(Request& context)`, `CLI(Request& context)`, `ONCE(Request& context)`, `INIT(Request& context)`, and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`.
|
||||
- `ONCE`, `RENDER`, and `COMPONENT` may be followed by a preprocessor attribute line such as `@fragment head` before the opening `{`. The handler's output is then captured and appended to `context.call["fragments"]["head"]` instead of being emitted at the call site. `ONCE` defaults to `@fragment once` when no fragment is specified.
|
||||
- `COMPONENT:NAME(Request& context)` is rewritten by the custom pass into an exported named component handler.
|
||||
- `EXPORT` is also a normal C++ macro, but the custom pass additionally records exported declarations for metadata.
|
||||
|
||||
@@ -42,6 +43,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
|
||||
- `<?: ... ?>` becomes `print(...);` and is intended for trusted markup or already-escaped content.
|
||||
- `#load "file.uce"` is replaced with a generated C++ `#include` that points at the loaded unit's preprocessed `.cpp` file under `BIN_DIRECTORY`.
|
||||
- Lines beginning with `EXPORT` are scanned so their declarations can be written to a sibling `.exports.txt` file.
|
||||
- `@fragment slot-name` lines immediately following `ONCE`, `RENDER`, or `COMPONENT` are removed and replaced with an output-capture guard at the start of the handler body.
|
||||
- Lines beginning with `RENDER:NAME(...)` are rewritten into exported `__uce_render_NAME(...)` functions.
|
||||
- Lines beginning with `COMPONENT:NAME(...)` are rewritten into exported `__uce_component_NAME(...)` functions for the component helpers.
|
||||
- The final generated source is written to `BIN_DIRECTORY + src_path + "/" + source_file + ".cpp"`.
|
||||
@@ -130,6 +132,18 @@ ONCE(Request& context)
|
||||
}
|
||||
```
|
||||
|
||||
One-time page assets captured for a template-controlled slot:
|
||||
|
||||
```cpp
|
||||
ONCE(Request& context)
|
||||
@fragment head
|
||||
{
|
||||
?><link rel="stylesheet" href="/assets/page.css" /><?
|
||||
}
|
||||
```
|
||||
|
||||
The page template can then render `context.call["fragments"]["head"]` inside `<head>`.
|
||||
|
||||
## Rules
|
||||
|
||||
- Literal mode can start on either `<>` or `?>`.
|
||||
@@ -155,6 +169,8 @@ ONCE(Request& context)
|
||||
|
||||
- Inspect the generated file under `BIN_DIRECTORY` first. That file shows the exact C++ produced by the UCE preprocessor.
|
||||
- Compiler errors usually point back to the `.uce` source because the preprocessor inserts `#line 1`, but the generated `.cpp` is still the best place to inspect expansion problems.
|
||||
- Compile failures are reported with the source path, generated C++ path, compile-output artifact path, an excerpt when UCE can identify a line, and the raw compiler output from the configured compile script.
|
||||
- Runtime request failures include the request/script path, generated C++ path, a hint about inspecting template delimiters and recent component/unit calls, and a native trace when available.
|
||||
- If a `#load` include looks wrong, check the current file's directory, the configured `BIN_DIRECTORY`, and whether the loaded page already produced its own generated `.cpp`.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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"); });
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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)); });
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,31 @@
|
||||
:title
|
||||
map
|
||||
|
||||
:sig
|
||||
StringList map(StringList items, function<String (String)> f)
|
||||
vector<R> map(vector<T> items, function<R (T)> f)
|
||||
|
||||
:params
|
||||
items : list of items to transform
|
||||
f : a function that returns the transformed value for each item
|
||||
return value : a new list containing the transformed values
|
||||
|
||||
:see
|
||||
>string
|
||||
filter
|
||||
StringList
|
||||
0_DTree
|
||||
|
||||
:content
|
||||
Returns a new list by calling `f` for each item in `items`.
|
||||
|
||||
```cpp
|
||||
auto upper = map(names, [](String s) { return(to_upper(s)); });
|
||||
```
|
||||
|
||||
Use `filter()` when you want to keep only matching items, and `map()` when you want to transform each item.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- JavaScript / React: `Array.prototype.map()`
|
||||
- PHP: `array_map()`
|
||||
@@ -13,7 +13,16 @@ return value : a list of rows returned from executing the query
|
||||
:content
|
||||
Executes a MySQL query and returns the resulting data, if any.
|
||||
|
||||
`params` provides the query parameter values used by the statement.
|
||||
`params` provides the query parameter values used by the statement. Use named `:name` placeholders only; positional `?` placeholders are rejected.
|
||||
|
||||
```cpp
|
||||
StringMap params;
|
||||
params["email"] = "ada@example.test";
|
||||
DTree rows = mysql_query(m,
|
||||
"select id, email from users where email = :email",
|
||||
params
|
||||
);
|
||||
```
|
||||
|
||||
The result is returned as a `DTree`, which makes it easy to iterate through rows and read fields with the usual `DTree` accessors.
|
||||
|
||||
|
||||
@@ -7,10 +7,31 @@ return value : a StringMap containing the parameters
|
||||
|
||||
:see
|
||||
>uri
|
||||
encode_query
|
||||
request_context_params
|
||||
|
||||
:content
|
||||
Decodes a query-string fragment such as `a=b&c=d` into a `StringMap`.
|
||||
|
||||
Parsing rules:
|
||||
|
||||
- pairs are separated on `&`
|
||||
- each pair is split on the first raw `=` only
|
||||
- both key and value are URL-decoded
|
||||
- keyless flags such as `preview` are present with an empty string value
|
||||
- empty pairs, including a trailing `&`, are ignored
|
||||
- repeated keys use the last value seen
|
||||
|
||||
Examples:
|
||||
|
||||
```cpp
|
||||
StringMap q = parse_query("alpha=1&token=a%3Db&preview&empty=");
|
||||
// q["alpha"] == "1"
|
||||
// q["token"] == "a=b"
|
||||
// q["preview"] == ""
|
||||
// q["empty"] == ""
|
||||
```
|
||||
|
||||
This is useful when you need to work with URL parameter data outside the normal request parsing flow.
|
||||
|
||||
Related:
|
||||
|
||||
@@ -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);
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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"]`.
|
||||
@@ -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.
|
||||
@@ -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));
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
:sig
|
||||
DTree sqlite_query(SQLite* db, String q)
|
||||
DTree sqlite_query(SQLite* db, String q, StringMap params)
|
||||
|
||||
:params
|
||||
db : pointer to an active SQLite connection
|
||||
q : SQL statement
|
||||
params : optional named parameter map
|
||||
return value : list of result rows as a DTree
|
||||
|
||||
:see
|
||||
>sqlite
|
||||
sqlite_connect
|
||||
sqlite_error
|
||||
sqlite_insert_id
|
||||
sqlite_affected_rows
|
||||
0_DTree
|
||||
|
||||
:content
|
||||
Executes one SQLite statement and returns result rows as a `DTree` array. Multi-statement SQL strings are rejected so migrations cannot silently run only their first statement.
|
||||
|
||||
Use named parameters with `:name` placeholders only. Positional `?` placeholders and SQLite's other named marker forms (`@name`, `$name`) are rejected so UCE SQLite queries use the same placeholder style as the MySQL helper. UCE binds parameters with SQLite prepared statements; it does not substitute values into the SQL string.
|
||||
|
||||
```cpp
|
||||
SQLite* db = sqlite_connect("/tmp/app.sqlite");
|
||||
|
||||
StringMap params;
|
||||
params["email"] = "ada@example.test";
|
||||
DTree rows = sqlite_query(db,
|
||||
"select id, email from users where email = :email",
|
||||
params
|
||||
);
|
||||
```
|
||||
|
||||
Result rows are objects keyed by column name. SQLite integer, float, text, blob, and null values are converted to DTree values. Blob values are returned as byte strings.
|
||||
|
||||
For statements that do not return rows, inspect `sqlite_affected_rows()` or `sqlite_insert_id()` after the call.
|
||||
@@ -17,13 +17,19 @@ Theme rendering is split the same way as the PHP starter:
|
||||
- `themes/common/page.json.uce`
|
||||
- `themes/<theme>/page.html.uce`
|
||||
|
||||
Those page templates are the authoritative shell layer, and in the UCE port they are proper `COMPONENT(...)` units rather than standalone page entrypoints. `index.uce` resolves the routed view, captures the `main` fragment, then hands off once into `themes/common/page.*.uce` or `themes/<theme>/page.html.uce`, matching the PHP starter's page-layer flow without an extra shell implementation.
|
||||
Those page templates are the authoritative shell layer, and in the UCE port they are proper `COMPONENT(...)` units rather than standalone page entrypoints. `index.uce` owns the app-local router, captures the `main` fragment, then hands off to `themes/page.uce`. That component resolves `context.call["app"]["page_type"]` by checking `themes/<current-theme>/page.<page_type>.uce` first and then `themes/common/page.<page_type>.uce`, matching the PHP starter's page-layer flow without an extra shell implementation.
|
||||
|
||||
The example uses query-string routing in the same style as the PHP starter:
|
||||
The router is intentionally normal UCE code instead of a runtime feature. It checks `views/<path>.uce`, then `views/<path>/index.uce`, then parent index handlers such as `views/workspace/index.uce` with the last segment stored as `context.call["route"]["param"]`. Matching view files are invoked with `component()`, and view files expose `COMPONENT(Request& context)` rather than `RENDER(Request& context)` because they are intended to render only through the central router. This gives the starter hierarchical/file-based routing while keeping policy in the app.
|
||||
|
||||
- `index.uce`
|
||||
- `index.uce?page1`
|
||||
- `index.uce?themes&theme=portal-dark`
|
||||
- `index.uce?workspace/projects`
|
||||
Starter-local web affordances live in `components/theme/web_affordances.uce`; currently this provides `COMPONENT:island` for progressive enhancement without adding global UCE runtime APIs. Component-specific CSS/JS is emitted by `ONCE(Request& context)` in the component unit that owns it, or by a small shared asset component when multiple sibling components need the same files. The router's 404 body is also a normal component at `components/basic/notfound.uce`, keeping page fragments out of the front controller.
|
||||
|
||||
The example uses query-string routing in the same style as the PHP starter, but the canonical public URL is the directory path because `index.uce` is reached through nginx's default index/try-file behavior. Links generated by the starter therefore target:
|
||||
|
||||
- `/examples/uce-starter/`
|
||||
- `/examples/uce-starter/?page1`
|
||||
- `/examples/uce-starter/?themes&theme=portal-dark`
|
||||
- `/examples/uce-starter/?workspace/projects`
|
||||
|
||||
Direct requests to `/examples/uce-starter/index.uce` still work, but self-links are canonicalized back to `/examples/uce-starter/`.
|
||||
|
||||
The demo account pages use a small file-backed user store under `/tmp/uce-starter-data/` with session-based login state.
|
||||
|
||||
@@ -4,7 +4,7 @@ COMPONENT(Request& context)
|
||||
{
|
||||
String title = first(context.props["title"].to_string(), "Sign in with OAuth");
|
||||
String subtitle = first(context.props["subtitle"].to_string(), "Choose your preferred authentication method");
|
||||
String callback_url = first(context.props["callback_url"].to_string(), starter_link("auth/callback", context));
|
||||
String callback_url = first(context.props["callback_url"].to_string(), app_link("auth/callback", context));
|
||||
|
||||
DTree services = context.props["services"];
|
||||
if(services.get_type_name() != "array" || services["google"]["name"].to_string() == "")
|
||||
@@ -46,7 +46,7 @@ COMPONENT(Request& context)
|
||||
String service_name = service["name"].to_string();
|
||||
String scope = service["scope"].to_string();
|
||||
String auth_url = service["auth_url"].to_string();
|
||||
String store_url = starter_link("auth/store-oauth-session", context);
|
||||
String store_url = app_link("auth/store-oauth-session", context);
|
||||
?><div data-service="<?= service_key ?>">
|
||||
<button type="button" class="btn" onclick='initiateOAuth_<?= handler_name ?>()'>
|
||||
<i class="<?= service["icon"].to_string() ?>" style="color: <?= service["color"].to_string() ?>"></i>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
|
||||
<>
|
||||
<div id="cookie-consent" style="
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,29 +1,63 @@
|
||||
#load "lib/app.uce"
|
||||
|
||||
#load "lib/app.uce"
|
||||
|
||||
DTree starter_router_result(String file, String param = "")
|
||||
{
|
||||
DTree result;
|
||||
result["file"] = file;
|
||||
if(param != "")
|
||||
result["param"] = param;
|
||||
return(result);
|
||||
}
|
||||
|
||||
DTree starter_router_resolve(Request& context)
|
||||
{
|
||||
String lpath = context.call["route"]["l_path"].to_string();
|
||||
if(lpath == "")
|
||||
return(DTree());
|
||||
|
||||
String exact = "views/" + lpath + ".uce";
|
||||
if(file_exists(exact))
|
||||
return(starter_router_result(exact));
|
||||
|
||||
String directory_index = "views/" + lpath + "/index.uce";
|
||||
if(file_exists(directory_index))
|
||||
return(starter_router_result(directory_index));
|
||||
|
||||
auto parts = split(lpath, "/");
|
||||
while(parts.size() > 1)
|
||||
{
|
||||
String param = parts.back();
|
||||
parts.pop_back();
|
||||
String parent = join(parts, "/");
|
||||
String parent_index = "views/" + parent + "/index.uce";
|
||||
if(file_exists(parent_index))
|
||||
return(starter_router_result(parent_index, param));
|
||||
}
|
||||
|
||||
return(DTree());
|
||||
}
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
app_init(context);
|
||||
|
||||
DTree resolved = app_resolve_view(context);
|
||||
DTree resolved = starter_router_resolve(context);
|
||||
|
||||
ob_start();
|
||||
if(resolved["file"].to_string() != "")
|
||||
{
|
||||
if(resolved["param"].to_string() != "")
|
||||
context.call["app"]["route"]["param"] = resolved["param"];
|
||||
unit_render(resolved["file"].to_string(), context);
|
||||
context.call["route"]["param"] = resolved["param"];
|
||||
print(component(resolved["file"].to_string(), context));
|
||||
}
|
||||
else
|
||||
{
|
||||
app_not_found("The requested page does not exist.", context);
|
||||
<>
|
||||
<section class="card">
|
||||
<h1>404 Not Found</h1>
|
||||
<p><?= context.call["app"]["error"].to_string() ?></p>
|
||||
</section>
|
||||
</>
|
||||
DTree notfound_props;
|
||||
notfound_props["message"] = "The requested page does not exist.";
|
||||
print(component("components/basic/notfound", notfound_props, context));
|
||||
}
|
||||
|
||||
String main_html = ob_get_close();
|
||||
context.call["app"]["fragments"]["main"] = main_html;
|
||||
app_render_page(context);
|
||||
context.call["fragments"]["main"] = main_html;
|
||||
print(component("themes/page", context));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(function (root, factory) { // I really hate this convoluted bullshit
|
||||
(function (root, factory) {
|
||||
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
||||
module.exports = factory();
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
|
||||
@@ -1,158 +1,18 @@
|
||||
#load "../config/settings.uce"
|
||||
#load "user.class.h"
|
||||
|
||||
String app_fs_root(Request& context)
|
||||
{
|
||||
String root = context.call["app"]["fs_root"].to_string();
|
||||
if(root == "")
|
||||
root = cwd_get();
|
||||
return(root);
|
||||
}
|
||||
|
||||
String app_script_url(Request& context)
|
||||
{
|
||||
String url = context.call["app"]["script_url"].to_string();
|
||||
if(url == "")
|
||||
url = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
|
||||
return(url);
|
||||
}
|
||||
|
||||
String app_base_url(Request& context)
|
||||
{
|
||||
String base = context.call["app"]["base_url"].to_string();
|
||||
if(base == "")
|
||||
{
|
||||
base = dirname(app_script_url(context));
|
||||
if(base == "")
|
||||
base = "/";
|
||||
if(base[base.length() - 1] != '/')
|
||||
base.append(1, '/');
|
||||
}
|
||||
return(base);
|
||||
}
|
||||
|
||||
String app_fs_path(String relative, Request& context)
|
||||
{
|
||||
return(path_join(app_fs_root(context), relative));
|
||||
}
|
||||
|
||||
String app_link(String path, Request& context);
|
||||
String app_link(String path, StringMap params, Request& context);
|
||||
void app_init(Request& context);
|
||||
|
||||
String app_first_route_segment(Request& context)
|
||||
{
|
||||
String query = context.params["QUERY_STRING"];
|
||||
for(auto part : split(query, "&"))
|
||||
{
|
||||
if(part == "")
|
||||
continue;
|
||||
if(part.find("=") == String::npos)
|
||||
return(uri_decode(part));
|
||||
}
|
||||
return("");
|
||||
}
|
||||
|
||||
DTree app_make_route(Request& context)
|
||||
{
|
||||
DTree route;
|
||||
String path = app_first_route_segment(context);
|
||||
path = trim(path);
|
||||
while(path.length() > 0 && path[0] == '/')
|
||||
path = path.substr(1);
|
||||
while(path.length() > 0 && path[path.length() - 1] == '/')
|
||||
path = path.substr(0, path.length() - 1);
|
||||
if(path == "")
|
||||
path = "index";
|
||||
route["l_path"] = path;
|
||||
route["page"] = nibble(path, "/");
|
||||
if(route["page"].to_string() == "")
|
||||
route["page"] = "index";
|
||||
return(route);
|
||||
}
|
||||
|
||||
DTree app_resolve_view(Request& context, String base_dir = "views")
|
||||
{
|
||||
DTree result;
|
||||
DTree route = context.call["app"]["route"];
|
||||
String lpath = first(route["l_path"].to_string(), "index");
|
||||
String base = trim(base_dir);
|
||||
if(base != "" && base[base.length() - 1] == '/')
|
||||
base = base.substr(0, base.length() - 1);
|
||||
|
||||
String exact = base + "/" + lpath + ".uce";
|
||||
if(file_exists(exact))
|
||||
{
|
||||
result["file"] = exact;
|
||||
return(result);
|
||||
}
|
||||
|
||||
String dir_index = base + "/" + lpath + "/index.uce";
|
||||
if(file_exists(dir_index))
|
||||
{
|
||||
result["file"] = dir_index;
|
||||
return(result);
|
||||
}
|
||||
|
||||
auto parts = split(lpath, "/");
|
||||
if(parts.size() > 1)
|
||||
{
|
||||
String param = parts.back();
|
||||
parts.pop_back();
|
||||
String parent = join(parts, "/");
|
||||
String parent_index = base + "/" + parent + "/index.uce";
|
||||
if(file_exists(parent_index))
|
||||
{
|
||||
result["file"] = parent_index;
|
||||
result["param"] = param;
|
||||
return(result);
|
||||
}
|
||||
}
|
||||
|
||||
return(result);
|
||||
}
|
||||
|
||||
void app_register_css(String path, Request& context)
|
||||
{
|
||||
context.call["app"]["assets"]["css"][path] = path;
|
||||
}
|
||||
|
||||
void app_register_js(String path, Request& context)
|
||||
{
|
||||
context.call["app"]["assets"]["js"][path] = path;
|
||||
}
|
||||
|
||||
String app_asset_url(String path, Request& context)
|
||||
{
|
||||
String url = app_base_url(context) + path;
|
||||
String fs_path = app_fs_path(path, context);
|
||||
String url = context.params["BASE_URL"] + path;
|
||||
String fs_path = path_join(dirname(context.params["SCRIPT_FILENAME"]), path);
|
||||
if(file_exists(fs_path))
|
||||
url += "?v=" + std::to_string((u64)file_mtime(fs_path));
|
||||
return(url);
|
||||
}
|
||||
|
||||
void app_render_registered_css(Request& context)
|
||||
{
|
||||
context.call["app"]["assets"]["css"].each([&](DTree item, String key) {
|
||||
String path = item.to_string();
|
||||
if(path != "")
|
||||
{
|
||||
<><link rel="stylesheet" href="<?= app_asset_url(path, context) ?>" /></>
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void app_render_registered_js(Request& context)
|
||||
{
|
||||
context.call["app"]["assets"]["js"].each([&](DTree item, String key) {
|
||||
String path = item.to_string();
|
||||
if(path != "")
|
||||
{
|
||||
<><script src="<?= app_asset_url(path, context) ?>"></script></>
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String app_link(String path, Request& context)
|
||||
{
|
||||
StringMap params;
|
||||
@@ -170,249 +30,38 @@ String app_link(String path, StringMap params, Request& context)
|
||||
query += "&" + extra;
|
||||
else if(query == "")
|
||||
query = extra;
|
||||
String url = app_script_url(context);
|
||||
String url = context.params["SCRIPT_URL"];
|
||||
if(query != "")
|
||||
url += "?" + query;
|
||||
return(url);
|
||||
}
|
||||
|
||||
void app_redirect(String path, Request& context)
|
||||
{
|
||||
context.set_status(302, "Found");
|
||||
context.header["Location"] = app_link(path, context);
|
||||
}
|
||||
|
||||
void app_not_found(String message, Request& context)
|
||||
{
|
||||
context.set_status(404, "Not Found");
|
||||
context.call["app"]["error"] = message;
|
||||
context.call["app"]["page_title"] = "404 Not Found";
|
||||
}
|
||||
|
||||
String app_menu_href(String menu_key, DTree menu_item, Request& context)
|
||||
{
|
||||
if(menu_item["external"].to_string() != "")
|
||||
return("/" + menu_key);
|
||||
return(app_link(menu_key, context));
|
||||
}
|
||||
|
||||
String app_html_class(Request& context)
|
||||
{
|
||||
String classes = "no-js";
|
||||
if(context.cfg.get_by_path("theme/mode").to_string() == "dark")
|
||||
classes += " dark-theme";
|
||||
return(classes);
|
||||
}
|
||||
|
||||
String app_page_main_html(Request& context)
|
||||
{
|
||||
String main_html = context.props["main_html"].to_string();
|
||||
if(main_html == "")
|
||||
main_html = context.call["app"]["fragments"]["main"].to_string();
|
||||
return(main_html);
|
||||
}
|
||||
|
||||
bool app_bool_value(DTree value, bool fallback = false)
|
||||
{
|
||||
String raw = trim(value.to_string());
|
||||
if(raw == "")
|
||||
return(fallback);
|
||||
return(
|
||||
raw != "0" &&
|
||||
raw != "false" &&
|
||||
raw != "FALSE" &&
|
||||
raw != "False" &&
|
||||
raw != "(false)" &&
|
||||
raw != "no" &&
|
||||
raw != "NO" &&
|
||||
raw != "No"
|
||||
);
|
||||
}
|
||||
|
||||
bool app_request_embed_mode(Request& context)
|
||||
{
|
||||
return(app_bool_value(context.call["app"]["embed_mode"]));
|
||||
}
|
||||
|
||||
bool app_page_embed_mode(Request& context)
|
||||
{
|
||||
String embed_value = context.props["embed_mode"].to_string();
|
||||
if(embed_value != "")
|
||||
return(app_bool_value(context.props["embed_mode"]));
|
||||
return(app_request_embed_mode(context));
|
||||
}
|
||||
|
||||
String app_theme_page_component(Request& context)
|
||||
{
|
||||
String page_type = first(context.call["app"]["page_type"].to_string(), "html");
|
||||
if(page_type == "blank")
|
||||
return("themes/common/page.blank.uce");
|
||||
if(page_type == "json")
|
||||
return("themes/common/page.json.uce");
|
||||
|
||||
String theme_path = context.cfg.get_by_path("theme/path").to_string();
|
||||
if(theme_path == "")
|
||||
return("");
|
||||
if(theme_path[theme_path.length() - 1] != '/')
|
||||
theme_path.append(1, '/');
|
||||
return(theme_path + "page.html.uce");
|
||||
}
|
||||
|
||||
void app_render_page(Request& context)
|
||||
{
|
||||
if(context.flags.status >= 300 && context.flags.status < 400)
|
||||
return;
|
||||
|
||||
DTree page_props;
|
||||
page_props["main_html"] = context.call["app"]["fragments"]["main"];
|
||||
if(context.call["app"]["json"].get_type_name() == "array")
|
||||
page_props["json"] = context.call["app"]["json"];
|
||||
|
||||
String page_component = app_theme_page_component(context);
|
||||
if(page_component != "" && file_exists(page_component))
|
||||
{
|
||||
print(component(page_component, page_props, context));
|
||||
return;
|
||||
}
|
||||
|
||||
context.set_status(500, "Internal Server Error");
|
||||
context.header["Content-Type"] = "text/plain; charset=utf-8";
|
||||
print("UCE app is missing the page template component: " + page_component);
|
||||
}
|
||||
|
||||
// Backward-compatible aliases for older starter example units.
|
||||
using StarterUser = AppUser;
|
||||
|
||||
void starter_boot(Request& context)
|
||||
{
|
||||
app_init(context);
|
||||
}
|
||||
|
||||
String starter_link(String path, Request& context)
|
||||
{
|
||||
return(app_link(path, context));
|
||||
}
|
||||
|
||||
String starter_link(String path, StringMap params, Request& context)
|
||||
{
|
||||
return(app_link(path, params, context));
|
||||
}
|
||||
|
||||
void starter_register_css(String path, Request& context)
|
||||
{
|
||||
app_register_css(path, context);
|
||||
}
|
||||
|
||||
void starter_register_js(String path, Request& context)
|
||||
{
|
||||
app_register_js(path, context);
|
||||
}
|
||||
|
||||
String starter_asset_url(String path, Request& context)
|
||||
{
|
||||
return(app_asset_url(path, context));
|
||||
}
|
||||
|
||||
void starter_render_registered_css(Request& context)
|
||||
{
|
||||
app_render_registered_css(context);
|
||||
}
|
||||
|
||||
void starter_render_registered_js(Request& context)
|
||||
{
|
||||
app_render_registered_js(context);
|
||||
}
|
||||
|
||||
bool starter_page_embed_mode(Request& context)
|
||||
{
|
||||
return(app_page_embed_mode(context));
|
||||
}
|
||||
|
||||
String starter_page_main_html(Request& context)
|
||||
{
|
||||
return(app_page_main_html(context));
|
||||
}
|
||||
|
||||
String starter_html_class(Request& context)
|
||||
{
|
||||
return(app_html_class(context));
|
||||
}
|
||||
|
||||
String starter_menu_href(String menu_key, DTree menu_item, Request& context)
|
||||
{
|
||||
return(app_menu_href(menu_key, menu_item, context));
|
||||
}
|
||||
|
||||
void starter_render_page(Request& context)
|
||||
{
|
||||
app_render_page(context);
|
||||
}
|
||||
|
||||
void starter_redirect(String path, Request& context)
|
||||
{
|
||||
app_redirect(path, context);
|
||||
}
|
||||
|
||||
void starter_not_found(String message, Request& context)
|
||||
{
|
||||
app_not_found(message, context);
|
||||
}
|
||||
|
||||
DTree starter_make_route(Request& context)
|
||||
{
|
||||
return(app_make_route(context));
|
||||
}
|
||||
|
||||
DTree starter_resolve_view(Request& context, String base_dir = "views")
|
||||
{
|
||||
return(app_resolve_view(context, base_dir));
|
||||
}
|
||||
|
||||
void app_init(Request& context)
|
||||
{
|
||||
if(context.call["app"]["booted"].to_string() == "1")
|
||||
return;
|
||||
|
||||
context.call["app"]["booted"] = "1";
|
||||
context.call["app"]["fs_root"] = cwd_get();
|
||||
context.call["app"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
|
||||
|
||||
String base_url = dirname(context.call["app"]["script_url"].to_string());
|
||||
if(base_url == "")
|
||||
base_url = "/";
|
||||
if(base_url[base_url.length() - 1] != '/')
|
||||
base_url.append(1, '/');
|
||||
context.call["app"]["base_url"] = base_url;
|
||||
context.call["route"]["raw_path"] = context.params["ROUTE_PATH_RAW"];
|
||||
context.call["route"]["l_path"] = context.params["ROUTE_PATH"];
|
||||
context.call["route"]["page"] = context.params["ROUTE_PAGE"];
|
||||
context.call["route"]["valid"].set_bool(context.params["ROUTE_VALID"] != "0");
|
||||
|
||||
DTree config = get_config();
|
||||
String requested_theme = first(context.get["theme"], context.cookies["app_theme"], config["theme"]["key"].to_string());
|
||||
if(config["theme"]["options"][requested_theme].to_string() == "" && config["theme"]["options"][requested_theme].get_type_name() != "array")
|
||||
session_start();
|
||||
String requested_theme = first(context.get["theme"], context.session["app_theme"], config["theme"]["key"].to_string());
|
||||
if(config["theme"]["options"][requested_theme].get_type_name() != "array")
|
||||
requested_theme = config["theme"]["key"].to_string();
|
||||
if(context.get["theme"] != "")
|
||||
context.session["app_theme"] = requested_theme;
|
||||
|
||||
config["theme"]["options"][requested_theme].each([&](DTree item, String key) {
|
||||
DTree selected_theme = config["theme"]["options"][requested_theme];
|
||||
selected_theme.each([&](DTree item, String key) {
|
||||
config["theme"][key] = item;
|
||||
});
|
||||
config["theme"]["key"] = requested_theme;
|
||||
context.cfg = config;
|
||||
context.call["cfg"].set_reference(&context.cfg);
|
||||
context.call["app"]["config"].set_reference(&context.cfg);
|
||||
context.call["app"]["route"] = app_make_route(context);
|
||||
|
||||
context.call["app"]["page_type"] = "html";
|
||||
context.call["app"]["page_title"] = first(config["menu"][context.call["app"]["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
|
||||
context.call["app"]["embed_mode"].set_bool(context.get["embed"] != "");
|
||||
|
||||
if(context.get["theme"] != "" && context.get["theme"] == requested_theme)
|
||||
{
|
||||
set_cookie("app_theme", requested_theme, time() + (86400 * 180));
|
||||
context.cookies["app_theme"] = requested_theme;
|
||||
}
|
||||
|
||||
session_start();
|
||||
context.call["app"]["page_title"] = first(config["menu"][context.call["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
|
||||
AppUser(context).is_signed_in();
|
||||
|
||||
app_register_css(context.cfg.get_by_path("theme/path").to_string() + "css/style.css", context);
|
||||
app_register_css("themes/common/fontawesome/css/all.min.css", context);
|
||||
app_register_js("js/u-query.js", context);
|
||||
app_register_js("js/morphdom.js", context);
|
||||
app_register_js("js/site.js", context);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
print(starter_page_main_html(context));
|
||||
print(context.call["fragments"]["main"].to_string());
|
||||
}
|
||||
|
||||
@@ -2,13 +2,7 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.header["Content-Type"] = "application/json";
|
||||
context.header["Cache-Control"] = "no-cache, no-store, must-revalidate";
|
||||
if(context.props["json"].get_type_name() == "array")
|
||||
print(json_encode(context.props["json"]));
|
||||
else if(context.call["starter"]["json"].get_type_name() == "array")
|
||||
print(json_encode(context.call["starter"]["json"]));
|
||||
else
|
||||
print(starter_page_main_html(context));
|
||||
print(context.call["fragments"]["main"].to_string());
|
||||
}
|
||||
|
||||
@@ -2,26 +2,23 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
bool embed_mode = starter_page_embed_mode(context);
|
||||
String main_html = starter_page_main_html(context);
|
||||
String main_html = context.call["fragments"]["main"].to_string();
|
||||
|
||||
DTree nav_props;
|
||||
nav_props["account_wrapper_class"] = "nav-account nav-menu";
|
||||
|
||||
DTree footer_props;
|
||||
footer_props["embed_mode"].set_bool(embed_mode);
|
||||
|
||||
<>
|
||||
<!doctype html>
|
||||
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||
<head>
|
||||
<?: component("../../components/theme/head", context) ?>
|
||||
</head>
|
||||
<body<?: embed_mode ? " class=\"embed-mode\"" : "" ?>>
|
||||
<body>
|
||||
<?: component("../../components/theme/global_controls", context) ?>
|
||||
<?: component("../../components/theme/standard_nav", nav_props, context) ?>
|
||||
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
|
||||
<div id="content">
|
||||
<?: main_html ?>
|
||||
</div>
|
||||
<?: component("../../components/theme/footer", footer_props, context) ?>
|
||||
|
||||
@@ -2,23 +2,20 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
bool embed_mode = starter_page_embed_mode(context);
|
||||
String main_html = starter_page_main_html(context);
|
||||
String main_html = context.call["fragments"]["main"].to_string();
|
||||
|
||||
DTree footer_props;
|
||||
footer_props["embed_mode"].set_bool(embed_mode);
|
||||
|
||||
<>
|
||||
<!doctype html>
|
||||
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||
<head>
|
||||
<?: component("../../components/theme/head", context) ?>
|
||||
</head>
|
||||
<body<?: embed_mode ? " class=\"embed-mode\"" : "" ?>>
|
||||
<body>
|
||||
<?: component("../../components/theme/global_controls", context) ?>
|
||||
<?: component("../../components/theme/standard_nav", context) ?>
|
||||
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
|
||||
<div id="content">
|
||||
<?: main_html ?>
|
||||
</div>
|
||||
<?: component("../../components/theme/footer", footer_props, context) ?>
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
bool embed_mode = starter_page_embed_mode(context);
|
||||
String main_html = starter_page_main_html(context);
|
||||
String current_path = context.call["starter"]["route"]["l_path"].to_string();
|
||||
String main_html = context.call["fragments"]["main"].to_string();
|
||||
String current_path = context.call["route"]["l_path"].to_string();
|
||||
|
||||
DTree global_props;
|
||||
global_props["cookie_consent"].set_bool(false);
|
||||
@@ -17,32 +15,31 @@ COMPONENT(Request& context)
|
||||
|
||||
<>
|
||||
<!doctype html>
|
||||
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||
<head>
|
||||
<?: component("../../components/theme/head", context) ?>
|
||||
</head>
|
||||
<body class="admin-page localfirst-theme dark-theme<?= embed_mode ? " embed-mode" : "" ?>">
|
||||
<body class="admin-page localfirst-theme dark-theme">
|
||||
<?: component("../../components/theme/global_controls", global_props, context) ?>
|
||||
<div class="admin-shell">
|
||||
<nav class="admin-nav">
|
||||
<div class="admin-nav-header">
|
||||
<a class="admin-nav-title" href="<?= starter_link("", context) ?>">
|
||||
<img class="admin-nav-logo-img" src="<?= starter_asset_url("themes/localfirst/img/local_first_logo.png", context) ?>" alt="<?= context.cfg.get_by_path("site/name").to_string() ?>" />
|
||||
<a class="admin-nav-title" href="<?= app_link("", context) ?>">
|
||||
<img class="admin-nav-logo-img" src="<?= app_asset_url("themes/localfirst/img/local_first_logo.png", context) ?>" alt="<?= context.cfg.get_by_path("site/name").to_string() ?>" />
|
||||
</a>
|
||||
</div>
|
||||
<? context.cfg.get_by_path("menu").each([&](DTree menu_item, String menu_key) {
|
||||
if(menu_item["hidden"].to_string() == "1")
|
||||
return;
|
||||
bool active = menu_key != "" && (current_path == menu_key || current_path.rfind(menu_key + "/", 0) == 0);
|
||||
?><a class="admin-nav-item<?= active ? " active" : "" ?>" href="<?= starter_menu_href(menu_key, menu_item, context) ?>"><?= menu_item["title"].to_string() ?></a><?
|
||||
String href = menu_item["external"].to_string() != "" ? "/" + menu_key : app_link(menu_key, context);
|
||||
?><a class="admin-nav-item<?= active ? " active" : "" ?>" href="<?= href ?>"><?= menu_item["title"].to_string() ?></a><?
|
||||
}); ?>
|
||||
</nav>
|
||||
<div class="admin-main">
|
||||
<? if(!embed_mode) { ?>
|
||||
<div class="admin-toolbar">
|
||||
<?: component("../../components/theme/account_links", account_props, context) ?>
|
||||
</div>
|
||||
<? } ?>
|
||||
<div class="admin-toolbar">
|
||||
<?: component("../../components/theme/account_links", account_props, context) ?>
|
||||
</div>
|
||||
<div id="content" class="admin-content">
|
||||
<?: main_html ?>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#load "../lib/app.uce"
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
if(context.flags.status >= 300 && context.flags.status < 400)
|
||||
return;
|
||||
|
||||
String page_type = first(context.call["app"]["page_type"].to_string(), "html");
|
||||
String theme_path = "../" + context.cfg.get_by_path("theme/path").to_string();
|
||||
String page_component = theme_path + "page." + page_type + ".uce";
|
||||
if(!file_exists(page_component))
|
||||
page_component = "../themes/common/page." + page_type + ".uce";
|
||||
|
||||
if(file_exists(page_component))
|
||||
print(component(page_component, context));
|
||||
else
|
||||
{
|
||||
context.set_status(500, "Internal Server Error");
|
||||
context.header["Content-Type"] = "text/plain; charset=utf-8";
|
||||
print("UCE starter is missing a page template for page type: " + page_type);
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,23 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
bool embed_mode = starter_page_embed_mode(context);
|
||||
String main_html = starter_page_main_html(context);
|
||||
String main_html = context.call["fragments"]["main"].to_string();
|
||||
|
||||
DTree nav_props;
|
||||
nav_props["account_wrapper_class"] = "nav-account nav-menu";
|
||||
|
||||
DTree footer_props;
|
||||
footer_props["embed_mode"].set_bool(embed_mode);
|
||||
|
||||
<>
|
||||
<!doctype html>
|
||||
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||
<head>
|
||||
<?: component("../../components/theme/head", context) ?>
|
||||
</head>
|
||||
<body class="portal-theme portal-dark-theme dark-theme<?= embed_mode ? " embed-mode" : "" ?>">
|
||||
<body class="portal-theme portal-dark-theme dark-theme">
|
||||
<?: component("../../components/theme/global_controls", context) ?>
|
||||
<?: component("../../components/theme/standard_nav", nav_props, context) ?>
|
||||
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
|
||||
<div id="content">
|
||||
<?: main_html ?>
|
||||
</div>
|
||||
<?: component("../../components/theme/footer", footer_props, context) ?>
|
||||
|
||||
@@ -2,24 +2,21 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
bool embed_mode = starter_page_embed_mode(context);
|
||||
String main_html = starter_page_main_html(context);
|
||||
String main_html = context.call["fragments"]["main"].to_string();
|
||||
|
||||
DTree footer_props;
|
||||
footer_props["embed_mode"].set_bool(embed_mode);
|
||||
footer_props["inner_class"] = "footer-inner";
|
||||
|
||||
<>
|
||||
<!doctype html>
|
||||
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||
<head>
|
||||
<?: component("../../components/theme/head", context) ?>
|
||||
</head>
|
||||
<body class="portal-theme portal-light-theme<?= embed_mode ? " embed-mode" : "" ?>">
|
||||
<body class="portal-theme portal-light-theme">
|
||||
<?: component("../../components/theme/global_controls", context) ?>
|
||||
<?: component("../../components/theme/standard_nav", context) ?>
|
||||
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
|
||||
<div id="content">
|
||||
<?: main_html ?>
|
||||
</div>
|
||||
<?: component("../../components/theme/footer", footer_props, context) ?>
|
||||
|
||||
@@ -2,27 +2,24 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
bool embed_mode = starter_page_embed_mode(context);
|
||||
String main_html = starter_page_main_html(context);
|
||||
String main_html = context.call["fragments"]["main"].to_string();
|
||||
|
||||
DTree nav_props;
|
||||
nav_props["account_wrapper_class"] = "nav-account nav-menu";
|
||||
|
||||
DTree footer_props;
|
||||
footer_props["embed_mode"].set_bool(embed_mode);
|
||||
|
||||
<>
|
||||
<!doctype html>
|
||||
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||
<head>
|
||||
<?: component("../../components/theme/head", context) ?>
|
||||
</head>
|
||||
<body<?: embed_mode ? " class=\"embed-mode\"" : "" ?>>
|
||||
<body>
|
||||
<div class="retro-stars"></div>
|
||||
<?: component("../../components/theme/global_controls", context) ?>
|
||||
<?: component("../../components/theme/standard_nav", nav_props, context) ?>
|
||||
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
|
||||
<div id="content">
|
||||
<?: main_html ?>
|
||||
</div>
|
||||
<?: component("../../components/theme/footer", footer_props, context) ?>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#load "../../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "Login";
|
||||
context.call["app"]["page_title"] = "Login";
|
||||
StarterUser users(context);
|
||||
DTree result;
|
||||
if(context.params["REQUEST_METHOD"] == "POST")
|
||||
@@ -11,7 +10,7 @@ RENDER(Request& context)
|
||||
result = users.sign_in(context.post["email"], context.post["password"]);
|
||||
if(result["result"].to_string() != "")
|
||||
{
|
||||
starter_redirect("account/profile", context);
|
||||
redirect(app_link("account/profile", context));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +22,6 @@ RENDER(Request& context)
|
||||
<label>Password: <input type="password" name="password" required></label><br>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<p><a href="<?= starter_link("account/register", context) ?>">Register</a></p>
|
||||
<p><a href="<?= app_link("account/register", context) ?>">Register</a></p>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#load "../../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
StarterUser(context).logout();
|
||||
starter_redirect("account/login", context);
|
||||
redirect(app_link("account/login", context));
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
#load "../../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
StarterUser users(context);
|
||||
if(!users.is_signed_in())
|
||||
{
|
||||
starter_redirect("account/login", context);
|
||||
redirect(app_link("account/login", context));
|
||||
return;
|
||||
}
|
||||
context.call["starter"]["page_title"] = "Profile";
|
||||
context.call["app"]["page_title"] = "Profile";
|
||||
DTree user = users.current();
|
||||
String roles = "";
|
||||
user["roles"].each([&](DTree role, String key) {
|
||||
@@ -22,6 +21,6 @@ RENDER(Request& context)
|
||||
<p>Email: <?= user["email"].to_string() ?></p>
|
||||
<p>Roles: <?= roles ?></p>
|
||||
<p>Created: <?= time_format_local("%Y-%m-%dT%H:%M:%S", int_val(user["created"].to_string())) ?></p>
|
||||
<p><a href="<?= starter_link("account/logout", context) ?>">Logout</a></p>
|
||||
<p><a href="<?= app_link("account/logout", context) ?>">Logout</a></p>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#load "../../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "Register";
|
||||
context.call["app"]["page_title"] = "Register";
|
||||
StarterUser users(context);
|
||||
DTree result;
|
||||
if(context.params["REQUEST_METHOD"] == "POST")
|
||||
@@ -11,7 +10,7 @@ RENDER(Request& context)
|
||||
|
||||
<>
|
||||
<h1>Register</h1>
|
||||
<? if(result["result"].to_string() != "") { ?><div class="banner success">Registration successful. <a href="<?= starter_link("account/login", context) ?>">Log in</a></div><? } ?>
|
||||
<? if(result["result"].to_string() != "") { ?><div class="banner success">Registration successful. <a href="<?= app_link("account/login", context) ?>">Log in</a></div><? } ?>
|
||||
<? if(result["message"].to_string() != "" && result["result"].to_string() == "") { ?><div class="banner error"><?= result["message"].to_string() ?></div><? } ?>
|
||||
<form method="post">
|
||||
<label>Email: <input type="email" name="email" required></label><br>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#load "../../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "OAuth Callback";
|
||||
context.call["app"]["page_title"] = "OAuth Callback";
|
||||
String code = context.get["code"];
|
||||
String state = context.get["state"];
|
||||
String error = context.get["error"];
|
||||
@@ -18,8 +17,8 @@ RENDER(Request& context)
|
||||
<h2>Authentication Failed</h2>
|
||||
<p><?= first(error_description, error) ?></p>
|
||||
<div class="callback-actions">
|
||||
<a href="<?= starter_link("auth/demo", context) ?>" class="btn btn-primary">Try Again</a>
|
||||
<a href="<?= starter_link("", context) ?>" class="btn btn-outline">Go Home</a>
|
||||
<a href="<?= app_link("auth/demo", context) ?>" class="btn btn-primary">Try Again</a>
|
||||
<a href="<?= app_link("", context) ?>" class="btn btn-outline">Go Home</a>
|
||||
</div>
|
||||
</div>
|
||||
<? } else if(code != "" && state != "") { ?>
|
||||
@@ -33,8 +32,8 @@ RENDER(Request& context)
|
||||
<p><strong>State:</strong> <?= state ?></p>
|
||||
</div>
|
||||
<div class="callback-actions">
|
||||
<a href="<?= starter_link("auth/demo", context) ?>" class="btn btn-primary">Back to Auth Demo</a>
|
||||
<a href="<?= starter_link("", context) ?>" class="btn btn-outline">Go Home</a>
|
||||
<a href="<?= app_link("auth/demo", context) ?>" class="btn btn-primary">Back to Auth Demo</a>
|
||||
<a href="<?= app_link("", context) ?>" class="btn btn-outline">Go Home</a>
|
||||
</div>
|
||||
</div>
|
||||
<? } else { ?>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#load "../../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "Auth";
|
||||
starter_register_css("views/marketing.css", context);
|
||||
DTree asset_props;
|
||||
asset_props["css"]["0"] = "views/marketing.css";
|
||||
print(component("../../components/theme/assets", asset_props, context));
|
||||
context.call["app"]["page_title"] = "Auth";
|
||||
|
||||
DTree props;
|
||||
props["title"] = "Sign In to Your Account";
|
||||
@@ -13,7 +14,7 @@ RENDER(Request& context)
|
||||
props["github_client_id"] = "YOUR_GITHUB_CLIENT_ID";
|
||||
props["discord_client_id"] = "YOUR_DISCORD_CLIENT_ID";
|
||||
props["twitch_client_id"] = "YOUR_TWITCH_CLIENT_ID";
|
||||
props["callback_url"] = starter_link("auth/callback", context);
|
||||
props["callback_url"] = app_link("auth/callback", context);
|
||||
|
||||
<>
|
||||
<h1>Authentication Demo</h1>
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
#load "../../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_type"] = "json";
|
||||
context.call["app"]["page_type"] = "json";
|
||||
|
||||
DTree response;
|
||||
DTree body = json_decode(context.in);
|
||||
if(context.params["REQUEST_METHOD"] == "POST" && body["oauth_service"].to_string() != "" && body["oauth_state"].to_string() != "")
|
||||
{
|
||||
context.session["oauth_service"] = body["oauth_service"].to_string();
|
||||
context.session["oauth_state"] = body["oauth_state"].to_string();
|
||||
context.call["starter"]["json"]["status"] = "success";
|
||||
response["status"] = "success";
|
||||
}
|
||||
else
|
||||
{
|
||||
context.set_status(400, "Bad Request");
|
||||
context.call["starter"]["json"]["status"] = "error";
|
||||
context.call["starter"]["json"]["message"] = "Invalid input";
|
||||
response["status"] = "error";
|
||||
response["message"] = "Invalid input";
|
||||
}
|
||||
print(json_encode(response));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#load "../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
ONCE(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "Dashboard";
|
||||
starter_register_css("views/dashboard.css", context);
|
||||
?><link rel="stylesheet" href="<?= app_asset_url("views/dashboard.css", context) ?>" /><?
|
||||
}
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
context.call["app"]["page_title"] = "Dashboard";
|
||||
|
||||
DTree props;
|
||||
DTree item;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#load "../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "Features";
|
||||
starter_register_css("views/marketing.css", context);
|
||||
DTree asset_props;
|
||||
asset_props["css"]["0"] = "views/marketing.css";
|
||||
print(component("../components/theme/assets", asset_props, context));
|
||||
context.call["app"]["page_title"] = "Features";
|
||||
<>
|
||||
<h1>Features</h1>
|
||||
<p class="card">This page exists in the UCE port so the starter menu resolves cleanly as a full route, while still reusing the same marketing components as the home page.</p>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#load "../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "Gauges";
|
||||
starter_register_css("themes/common/css/gauges.css", context);
|
||||
context.call["app"]["page_title"] = "Gauges";
|
||||
f64 pi = 3.14159265358979323846;
|
||||
|
||||
DTree props;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#load "../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "Home";
|
||||
starter_register_css("views/marketing.css", context);
|
||||
DTree asset_props;
|
||||
asset_props["css"]["0"] = "views/marketing.css";
|
||||
print(component("../components/theme/assets", asset_props, context));
|
||||
context.call["app"]["page_title"] = "Home";
|
||||
|
||||
DTree props;
|
||||
|
||||
@@ -79,13 +80,13 @@ RENDER(Request& context)
|
||||
?><div class="component-card">
|
||||
<h3><?= theme["label"].to_string() ?></h3>
|
||||
<p><?= theme["description"].to_string() ?></p>
|
||||
<a class="btn" href="<?= starter_link("themes", theme_params, context) ?>">Preview Theme</a>
|
||||
<a class="btn" href="<?= app_link("themes", theme_params, context) ?>">Preview Theme</a>
|
||||
</div><?
|
||||
}); ?>
|
||||
<div class="component-card">
|
||||
<h3>Starter Themes</h3>
|
||||
<p>The original light and dark starter skins remain available in the same gallery for side-by-side checks.</p>
|
||||
<a class="btn btn-secondary" href="<?= starter_link("themes", context) ?>">Open Gallery</a>
|
||||
<a class="btn btn-secondary" href="<?= app_link("themes", context) ?>">Open Gallery</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,9 +99,16 @@ RENDER(Request& context)
|
||||
props["secondary_text"] = "View GitHub";
|
||||
print(component("../components/example/marketing_blocks:CTA_SECTION", props, context));
|
||||
|
||||
DTree island_props;
|
||||
island_props["name"] = "StarterGuidelines";
|
||||
island_props["id"] = "starter-guidelines-island";
|
||||
island_props["props"]["route"] = context.call["route"]["l_path"].to_string();
|
||||
island_props["props"]["enhancement"] = "progressive";
|
||||
|
||||
<>
|
||||
<div class="card">
|
||||
<h2>Component Development Guidelines</h2>
|
||||
<?: component("../components/theme/web_affordances:island", island_props, context) ?>
|
||||
<div class="guidelines-grid">
|
||||
<div class="guideline-item" style="border-left-color: var(--primary);"><span class="guideline-icon">🏷️</span><span>Use semantic HTML5 elements</span></div>
|
||||
<div class="guideline-item" style="border-left-color: var(--secondary);"><span class="guideline-icon">🎨</span><span>Implement CSS custom properties for theming</span></div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#load "../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "Components";
|
||||
starter_register_css("views/marketing.css", context);
|
||||
DTree asset_props;
|
||||
asset_props["css"]["0"] = "views/marketing.css";
|
||||
print(component("../components/theme/assets", asset_props, context));
|
||||
context.call["app"]["page_title"] = "Components";
|
||||
|
||||
<>
|
||||
<h1>Component System</h1>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#load "../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_type"] = "blank";
|
||||
context.call["app"]["page_type"] = "blank";
|
||||
<>
|
||||
<?= std::to_string((u64)time()) ?> - Page 2 Section 1 loaded
|
||||
<pre>UCE starter AJAX fragment response</pre>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
#load "../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "Ajaxy";
|
||||
context.call["app"]["page_title"] = "Ajaxy";
|
||||
<>
|
||||
<h1>Ajax Demo</h1>
|
||||
<div id="page2-section1">
|
||||
<p>This is the content of Page 2. You can add more information here.</p>
|
||||
<p>Sed at dolor leo. Morbi a tellus sed nisl dictum ultricies sit amet at purus. Nam mattis metus sed nunc egestas convallis.</p>
|
||||
<br/>
|
||||
<button id="page2-load-button" data-load-url="<?= starter_link("page2-section1", context) ?>" type="button">Load new text</button>
|
||||
<button id="page2-load-button" data-load-url="<?= app_link("page2-section1", context) ?>" type="button">Load new text</button>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#load "../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "Theme Preview";
|
||||
starter_register_css("views/themes.css", context);
|
||||
DTree asset_props;
|
||||
asset_props["css"]["0"] = "views/themes.css";
|
||||
print(component("../components/theme/assets", asset_props, context));
|
||||
context.call["app"]["page_title"] = "Theme Preview";
|
||||
String current_theme_key = context.cfg.get_by_path("theme/key").to_string();
|
||||
String theme_label = first(context.cfg.get_by_path("theme/label").to_string(), current_theme_key);
|
||||
String theme_mode = context.cfg.get_by_path("theme/mode").to_string();
|
||||
@@ -18,8 +19,8 @@ RENDER(Request& context)
|
||||
<h1><?= theme_label ?></h1>
|
||||
<p>This route renders the same neutral content inside each theme so downstream projects can compare layout, typography, color tokens, and chrome.</p>
|
||||
<div class="theme-preview-actions">
|
||||
<a class="btn" href="<?= starter_link("themes", theme_params, context) ?>">Back to Gallery</a>
|
||||
<a class="btn btn-secondary" href="<?= starter_link("", theme_params, context) ?>">Open Home in This Theme</a>
|
||||
<a class="btn" href="<?= app_link("themes", theme_params, context) ?>">Back to Gallery</a>
|
||||
<a class="btn btn-secondary" href="<?= app_link("", theme_params, context) ?>">Open Home in This Theme</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="theme-preview-grid">
|
||||
@@ -59,7 +60,6 @@ RENDER(Request& context)
|
||||
<tbody>
|
||||
<tr><td>Navigation</td><td>Should remain readable when menus get long.</td><td>Verified</td></tr>
|
||||
<tr><td>Cards</td><td>Should keep spacing and hierarchy on both desktop and mobile.</td><td>Verified</td></tr>
|
||||
<tr><td>Embeds</td><td>Should render cleanly inside the gallery iframe.</td><td>Verified</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -82,21 +82,6 @@
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.theme-gallery-frame-wrap {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: calc(var(--radius-lg, var(--radius)) - 4px);
|
||||
overflow: hidden;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.theme-gallery-frame-wrap iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 430px;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.theme-preview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
@@ -150,20 +135,6 @@
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.embed-mode nav,
|
||||
.embed-mode footer,
|
||||
.embed-mode #theme-switcher,
|
||||
.embed-mode .admin-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.embed-mode #content,
|
||||
.embed-mode .admin-content {
|
||||
margin-top: 0 !important;
|
||||
min-height: 100vh !important;
|
||||
padding-top: 1rem !important;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.theme-gallery-grid,
|
||||
.theme-preview-grid {
|
||||
@@ -173,8 +144,4 @@
|
||||
.theme-gallery-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.theme-gallery-frame-wrap iframe {
|
||||
height: 360px;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,25 @@
|
||||
#load "../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "Themes";
|
||||
starter_register_css("views/themes.css", context);
|
||||
DTree asset_props;
|
||||
asset_props["css"]["0"] = "views/themes.css";
|
||||
print(component("../components/theme/assets", asset_props, context));
|
||||
context.call["app"]["page_title"] = "Themes";
|
||||
String current_theme = context.cfg.get_by_path("theme/key").to_string();
|
||||
|
||||
<>
|
||||
<div class="theme-gallery-shell">
|
||||
<section class="theme-gallery-hero card">
|
||||
<span class="theme-gallery-kicker">Starter Theme Gallery</span>
|
||||
<h1>Compare Themes Side by Side</h1>
|
||||
<p>The gallery renders the same preview content in each theme family. This makes it easier to judge shell fit, typography, token balance, and embed behavior.</p>
|
||||
<h1>Compare Themes</h1>
|
||||
<p>Open each theme preview directly to judge shell fit, typography, and token balance.</p>
|
||||
</section>
|
||||
<div class="theme-gallery-grid">
|
||||
<? context.cfg.get_by_path("theme/options").each([&](DTree theme_info, String theme_key) {
|
||||
StringMap params;
|
||||
params["theme"] = theme_key;
|
||||
String preview_href = starter_link("theme-preview", params, context);
|
||||
params["embed"] = "1";
|
||||
String iframe_href = starter_link("theme-preview", params, context);
|
||||
String preview_href = app_link("theme-preview", params, context);
|
||||
StringMap home_params;
|
||||
home_params["theme"] = theme_key;
|
||||
?><section class="theme-gallery-card<?= theme_key == current_theme ? " is-active" : "" ?>">
|
||||
@@ -33,10 +32,7 @@ RENDER(Request& context)
|
||||
</div>
|
||||
<div class="theme-gallery-actions">
|
||||
<a class="btn" href="<?= preview_href ?>">Open Preview</a>
|
||||
<a class="btn btn-secondary" href="<?= starter_link("", home_params, context) ?>">Open Home</a>
|
||||
</div>
|
||||
<div class="theme-gallery-frame-wrap">
|
||||
<iframe title="<?= theme_info["label"].to_string() ?> preview" src="<?= iframe_href ?>"></iframe>
|
||||
<a class="btn btn-secondary" href="<?= app_link("", home_params, context) ?>">Open Home</a>
|
||||
</div>
|
||||
</section><?
|
||||
}); ?>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
#load "../../lib/app.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.call["starter"]["page_title"] = "Workspace";
|
||||
String section = first(context.call["starter"]["route"]["param"].to_string(), "overview");
|
||||
context.call["app"]["page_title"] = "Workspace";
|
||||
String section = first(context.call["route"]["param"].to_string(), "overview");
|
||||
if(section != "overview" && section != "projects" && section != "activity")
|
||||
section = "overview";
|
||||
|
||||
@@ -19,9 +18,9 @@ RENDER(Request& context)
|
||||
<>
|
||||
<div class="ws-nav-group-label">Workspace areas</div>
|
||||
<div class="ws-nav-list">
|
||||
<a class="ws-nav-item<?= section == "overview" ? " is-active" : "" ?>" href="<?= starter_link("workspace/overview", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-compass"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Overview</span><span class="ws-nav-meta">Shell</span></span></span></a>
|
||||
<a class="ws-nav-item<?= section == "projects" ? " is-active" : "" ?>" href="<?= starter_link("workspace/projects", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-folder-tree"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Projects</span><span class="ws-nav-meta">Routes</span></span></span></a>
|
||||
<a class="ws-nav-item<?= section == "activity" ? " is-active" : "" ?>" href="<?= starter_link("workspace/activity", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-wave-square"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Activity</span><span class="ws-nav-meta">State</span></span></span></a>
|
||||
<a class="ws-nav-item<?= section == "overview" ? " is-active" : "" ?>" href="<?= app_link("workspace/overview", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-compass"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Overview</span><span class="ws-nav-meta">Shell</span></span></span></a>
|
||||
<a class="ws-nav-item<?= section == "projects" ? " is-active" : "" ?>" href="<?= app_link("workspace/projects", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-folder-tree"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Projects</span><span class="ws-nav-meta">Routes</span></span></span></a>
|
||||
<a class="ws-nav-item<?= section == "activity" ? " is-active" : "" ?>" href="<?= app_link("workspace/activity", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-wave-square"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Activity</span><span class="ws-nav-meta">State</span></span></span></a>
|
||||
</div>
|
||||
<div class="ws-demo-sidebar-copy"><p>This sidebar and mobile shell pattern was extracted from the AI portal app and normalized against starter theme tokens.</p></div>
|
||||
</>
|
||||
@@ -32,7 +31,7 @@ RENDER(Request& context)
|
||||
sidebar_body += component("../../components/workspace/primitives:LIST_STATE", sidebar_note, context);
|
||||
|
||||
DTree toolbar_props;
|
||||
toolbar_props["action_html"] = "<a class=\"ws-sidebar-action-btn\" href=\"" + starter_link("workspace/overview", context) + "\"><i class=\"fas fa-grid-2\"></i><span>Open shell</span></a>";
|
||||
toolbar_props["action_html"] = "<a class=\"ws-sidebar-action-btn\" href=\"" + app_link("workspace/overview", context) + "\"><i class=\"fas fa-grid-2\"></i><span>Open shell</span></a>";
|
||||
toolbar_props["search_input_id"] = "workspace-demo-search";
|
||||
toolbar_props["search_input_name"] = "workspace_demo_search";
|
||||
toolbar_props["search_placeholder"] = "Search workspace sections";
|
||||
@@ -95,7 +94,7 @@ RENDER(Request& context)
|
||||
String demo_head = component("../../components/workspace/primitives:SECTION_HEAD", section_head, context);
|
||||
section_props.clear();
|
||||
section_props["header_html"] = demo_head;
|
||||
section_props["body_html"] = detail_html + "<p class=\"ws-inline-note\">Try visiting <strong>" + html_escape(starter_link("workspace/projects", context)) + "</strong> or <strong>" + html_escape(starter_link("workspace/activity", context)) + "</strong> to see the nested route fallback in action.</p>";
|
||||
section_props["body_html"] = detail_html + "<p class=\"ws-inline-note\">Try visiting <strong>" + html_escape(app_link("workspace/projects", context)) + "</strong> or <strong>" + html_escape(app_link("workspace/activity", context)) + "</strong> to see the nested route fallback in action.</p>";
|
||||
main_body += component("../../components/workspace/primitives:SECTION", section_props, context);
|
||||
|
||||
if(section == "activity")
|
||||
@@ -104,7 +103,7 @@ RENDER(Request& context)
|
||||
empty_props["icon_class"] = "fas fa-clock-rotate-left";
|
||||
empty_props["title"] = "No live stream wired yet";
|
||||
empty_props["text"] = "The shell is generic. Add your own websocket, polling, or event-driven runtime behind it when a real product needs one.";
|
||||
empty_props["action_html"] = "<a class=\"ws-primary-btn\" href=\"" + starter_link("dashboard", context) + "\">Open dashboard demo</a>";
|
||||
empty_props["action_html"] = "<a class=\"ws-primary-btn\" href=\"" + app_link("dashboard", context) + "\">Open dashboard demo</a>";
|
||||
main_body += component("../../components/workspace/primitives:EMPTY_STATE", empty_props, context);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
context.set_status(302, "Found");
|
||||
context.header["Location"] = "/info/";
|
||||
}
|
||||
}
|
||||
|
||||
+43
-2
@@ -18,13 +18,19 @@ RENDER(Request& context)
|
||||
site_tests_page_start("Core APIs", "Pure helper coverage for strings, UTF-8 splitting, DTree, and JSON encoding/decoding.");
|
||||
|
||||
auto comma_parts = split("alpha,beta,gamma", ",");
|
||||
check("split()", comma_parts.size() == 3 && comma_parts[1] == "beta", join(comma_parts, " | "));
|
||||
auto empty_delim_parts = split("alpha", "");
|
||||
check("split()", comma_parts.size() == 3 && comma_parts[1] == "beta" && empty_delim_parts.size() == 1 && empty_delim_parts[0] == "alpha", join(comma_parts, " | "));
|
||||
|
||||
auto spaced_parts = split_space(" one two three ");
|
||||
check("split_space()", spaced_parts.size() == 3 && spaced_parts[2] == "three", join(spaced_parts, " / "));
|
||||
|
||||
check("trim()", trim(" padded value ") == "padded value", trim(" padded value "));
|
||||
StringMap kv = split_kv("\n#comment\nalpha = one\nempty=\n", '=', true, false);
|
||||
StringMap http_headers = split_http_headers("GET /demo.uce?x=1 HTTP/1.1\r\nHost: example.test\r\nX-Empty:\r\n");
|
||||
StringMap leading_crlf_headers = split_http_headers("\r\nGET /lead.uce HTTP/1.1\r\nHost: lead.example\r\n");
|
||||
StringMap header_only = split_http_headers("Host: example.test\nX-Token: abc\n");
|
||||
check("trim() / split_kv() / split_http_headers()", trim(" padded value ") == "padded value" && kv["alpha"] == "one" && kv["empty"] == "" && http_headers["REQUEST_METHOD"] == "GET" && http_headers["DOCUMENT_URI"] == "/demo.uce" && http_headers["QUERY_STRING"] == "x=1" && http_headers["HTTP_X_EMPTY"] == "" && leading_crlf_headers["REQUEST_METHOD"] == "GET" && leading_crlf_headers["DOCUMENT_URI"] == "/lead.uce" && leading_crlf_headers["HTTP_HOST"] == "lead.example" && header_only["REQUEST_METHOD"] == "" && header_only["HTTP_HOST"] == "example.test" && header_only["HTTP_X_TOKEN"] == "abc", trim(" padded value ") + " / " + var_dump(kv) + " / " + var_dump(http_headers) + " / " + var_dump(leading_crlf_headers) + " / " + var_dump(header_only));
|
||||
check("replace()", replace("alpha-beta-beta", "beta", "done") == "alpha-done-done", replace("alpha-beta-beta", "beta", "done"));
|
||||
check("html_escape() attribute-safe quotes", html_escape("<&>\"Don't") == "<&>"Don't", html_escape("<&>\"Don't"));
|
||||
check("regex_match()", regex_match("[A-Z][a-z]+", "Alice") && !regex_match("[A-Z][a-z]+", "Alice!"), "full-string validation");
|
||||
|
||||
DTree regex_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", "Contact ops@example.test");
|
||||
@@ -42,6 +48,41 @@ RENDER(Request& context)
|
||||
check("str_starts_with()", str_starts_with("websocket-suite", "websocket"), "websocket-suite starts with websocket");
|
||||
check("str_ends_with()", str_ends_with("component.uce", ".uce"), "component.uce ends with .uce");
|
||||
check("to_lower() / to_upper()", to_lower("MiXeD") == "mixed" && to_upper("MiXeD") == "MIXED", to_lower("MiXeD") + " / " + to_upper("MiXeD"));
|
||||
check("request context params", context.params["SCRIPT_URL"] != "" && context.params["BASE_URL"] != "" && context.params["ROUTE_PATH"] != "" && context.params["ROUTE_PAGE"] != "" && context.params["ROUTE_VALID"] == "1", "script=" + context.params["SCRIPT_URL"] + " base=" + context.params["BASE_URL"] + " route=" + context.params["ROUTE_PATH"] + " page=" + context.params["ROUTE_PAGE"] + " valid=" + context.params["ROUTE_VALID"]);
|
||||
String saved_query_string = context.params["QUERY_STRING"];
|
||||
context.params["QUERY_STRING"] = "workspace/projects&theme=dark";
|
||||
check("request_query_path() delegates to request_query_route()", request_query_path(context) == request_query_route(context)["l_path"].to_string() && request_query_path(context) == "workspace/projects", request_query_path(context) + " / " + json_encode(request_query_route(context)));
|
||||
context.params["QUERY_STRING"] = saved_query_string;
|
||||
check("route path sanitizers", route_path_normalize("/workspace/projects/") == "workspace/projects" && route_path_is_safe("workspace/projects") && !route_path_is_safe("../demo") && !route_path_is_safe("workspace/../demo") && !route_path_is_safe("workspace/file.uce") && route_path_sanitize("../demo") == "" && route_path_sanitize("") == "index", route_path_sanitize("/workspace/projects/"));
|
||||
|
||||
StringList route_parts = {"dashboard", "index", "dashboard", "themes"};
|
||||
auto unique_routes = list_unique(route_parts);
|
||||
auto sorted_routes = list_sort(unique_routes);
|
||||
auto upper_routes = map(sorted_routes, [](String item) { return(to_upper(item)); });
|
||||
auto dashboard_routes = filter(route_parts, [](String item) { return(item == "dashboard"); });
|
||||
check("map/filter/list_unique/sort/find/some/every", unique_routes.size() == 3 && sorted_routes[0] == "dashboard" && upper_routes[2] == "THEMES" && dashboard_routes.size() == 2 && list_find(route_parts, [](String item) { return(str_starts_with(item, "them")); }, "missing") == "themes" && list_some(route_parts, [](String item) { return(item == "index"); }) && list_every(unique_routes, [](String item) { return(item != ""); }), join(upper_routes, ","));
|
||||
|
||||
DTree nav;
|
||||
DTree nav_home;
|
||||
nav_home["title"] = "Home";
|
||||
nav_home["section"] = "main";
|
||||
nav.push(nav_home);
|
||||
DTree nav_dash;
|
||||
nav_dash["title"] = "Dashboard";
|
||||
nav_dash["section"] = "app";
|
||||
nav.push(nav_dash);
|
||||
DTree nav_themes;
|
||||
nav_themes["title"] = "Themes";
|
||||
nav_themes["section"] = "app";
|
||||
nav.push(nav_themes);
|
||||
DTree empty_tree;
|
||||
DTree empty_pop = empty_tree.pop();
|
||||
DTree app_nav = dtree_filter(nav, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
|
||||
DTree nav_titles = dtree_map(app_nav, [](DTree item, String key) { DTree title; title = item["title"].to_string(); return(title); });
|
||||
DTree grouped_nav = dtree_group_by(nav, [](DTree item, String key) { return(item["section"].to_string()); });
|
||||
DTree nav_dash_summary = dtree_pick(nav_dash, {"title"});
|
||||
DTree nav_dash_public = dtree_omit(nav_dash, {"section"});
|
||||
check("DTree collection helpers", empty_pop.to_string() == "" && app_nav.is_list() && nav_titles["0"].to_string() == "Dashboard" && grouped_nav["app"]["1"]["title"].to_string() == "Themes" && join(dtree_keys(nav_dash_summary), ",") == "title" && dtree_values(nav_dash_public)["0"].to_string() == "Dashboard", json_encode(grouped_nav));
|
||||
|
||||
String binary_payload = "core";
|
||||
binary_payload.push_back((char)0x00);
|
||||
|
||||
+2
-2
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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.");
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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\">");
|
||||
|
||||
Reference in New Issue
Block a user