diff --git a/README.md b/README.md index b2ac4f0..06bd3c2 100644 --- a/README.md +++ b/README.md @@ -60,14 +60,23 @@ UCE pages now use explicit request handlers instead of implicit globals: Useful related runtime patterns: - `render_file(String file_name)` or `render_file(String file_name, Request& context)` to invoke another page +- `context.cfg` for request-local structured configuration - `context.call` for invocation or message-local structured input - `context.connection` for broker-owned per-WebSocket-connection state shared across `WS(Request& context)` calls - `context.params`, `context.get`, `context.post`, `context.cookies`, `context.session`, and `context.header` for request/response state +- `context.set_status(code[, reason])` to set the HTTP response status -Named component-style render handlers are also supported: +Useful helpers for that data model now include: + +- `DTree::get_by_path("a/b/c")` for path-style config traversal without creating missing keys +- `json_encode(String)` for emitting JavaScript-safe string literals directly +- `ascii_safe_name(String)` for conservative ASCII identifier normalization +- `path_join(base, child)` for filesystem-style path assembly + +Named component handlers are also supported: ```cpp -RENDER:BODY(Request& context) +COMPONENT:BODY(Request& context) { <>

@@ -114,7 +123,9 @@ When you want returned component markup inside a literal block, prefer: because `` HTML-escapes the returned markup. For direct output from C++ code, use `render_component(...)`. -Components may expose additional named handlers with `RENDER:NAME(Request& context)`. The default handler remains the ordinary `RENDER(Request& context)`. +Components expose `COMPONENT(Request& context)` as their default entrypoint and may expose additional named handlers with `COMPONENT:NAME(Request& context)`. + +The component helpers call only `COMPONENT...` handlers. A file meant purely for component use can define `COMPONENT()` without defining `RENDER()`, which keeps direct page entry and component entry cleanly separated. Inside a component file, `component(":NAME", props, context)` and `render_component(":NAME", props, context)` target another named component handler in that same file. ## WebSockets diff --git a/site/doc/areas/string.txt b/site/doc/areas/string.txt index 914c1b4..509f44b 100644 --- a/site/doc/areas/string.txt +++ b/site/doc/areas/string.txt @@ -1,9 +1,11 @@ String Functions +ascii_safe_name concat filter first join +json_encode nibble print split diff --git a/site/doc/areas/sys.txt b/site/doc/areas/sys.txt index 8d5c66b..0313987 100644 --- a/site/doc/areas/sys.txt +++ b/site/doc/areas/sys.txt @@ -11,6 +11,7 @@ file_put_contents get_cwd ls mkdir +path_join set_cwd shell_escape shell_exec diff --git a/site/doc/areas/types.txt b/site/doc/areas/types.txt index 854b9c7..c90239e 100644 --- a/site/doc/areas/types.txt +++ b/site/doc/areas/types.txt @@ -1,5 +1,7 @@ Types DTree +get_by_path +set_status String StringMap diff --git a/site/doc/pages/0_context.txt b/site/doc/pages/0_context.txt index da04738..d10b143 100644 --- a/site/doc/pages/0_context.txt +++ b/site/doc/pages/0_context.txt @@ -28,6 +28,9 @@ Name of the session cookie :DTree var Variable user-defined data +:DTree cfg +Request-local configuration tree. Apps can keep structured configuration here and traverse it with `context.cfg.get_by_path("path/to/value")`. + :DTree call Invocation or message-local structured data @@ -43,6 +46,9 @@ Headers to be sent back to the browser :StringList set_cookies; Cookies that should be sent back to the browser +: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. + :u64 random_seed The current request's "random" noise generator seed @@ -64,4 +70,3 @@ Invokes another UCE file using the current or supplied request context :see >types - diff --git a/site/doc/pages/1_COMPONENT.txt b/site/doc/pages/1_COMPONENT.txt new file mode 100644 index 0000000..39b3832 --- /dev/null +++ b/site/doc/pages/1_COMPONENT.txt @@ -0,0 +1,36 @@ +:sig +COMPONENT(Request& context) + +:desc +Defines the default component entrypoint for the current `.uce` file. + +`component()` and `render_component()` invoke `COMPONENT(Request& context)` by default. Named component entrypoints use `COMPONENT:NAME(Request& context)`. + +This keeps page rendering and component rendering separate: + +- direct HTTP requests call `RENDER(Request& context)` +- WebSocket messages call `WS(Request& context)` +- component helpers call `COMPONENT(Request& context)` or `COMPONENT:NAME(Request& context)` + +A file intended only for component reuse can define `COMPONENT()` without defining `RENDER()`. + +Inside component handlers, props arrive through `context.call`. + +When you call `component(":NAME", props, context)` or `render_component(":NAME", props, context)`, the runtime resolves `:NAME` against the current `.uce` file instead of requiring the file name again. + +Examples: +`COMPONENT(Request& context)` +`{` +` <><section><?: component(":BODY", context.call, context) ?></section></>` +`}` + +`COMPONENT:BODY(Request& context)` +`{` +` <><p><?= context.call["body"] ?></p></>` +`}` + +:see +>component +>render_component +>1_RENDER +>1_WS diff --git a/site/doc/pages/1_RENDER.txt b/site/doc/pages/1_RENDER.txt index 562c296..1628652 100644 --- a/site/doc/pages/1_RENDER.txt +++ b/site/doc/pages/1_RENDER.txt @@ -6,19 +6,17 @@ Defines the main HTTP render handler for the current `.uce` page. When a page is requested over HTTP, the runtime loads the target file and calls its `RENDER(Request& context)` function. -Pages may also export additional named render handlers with `RENDER:NAME(Request& context)`. - -Named render handlers are not used for the page's direct HTTP entrypoint. They are intended for component-style sub-rendering through helpers such as `component("components/card:BODY", props, context)` or `render_component("components/card:BODY", props, context)`. - The default page entrypoint is always the plain `RENDER(Request& context)` handler. +Reusable component handlers now live on `COMPONENT(Request& context)` and `COMPONENT:NAME(Request& context)`. The component helpers call those handlers, not `RENDER()`. + The request environment is passed explicitly through `context`, including params, cookies, post data, session state, headers, uploaded files, and the current `context.call` tree. For a normal direct page request, `context.call` starts empty. If the page is invoked from another UCE file via `render_file(file_name, context)`, the callee receives that same `context`. -Pages intended to serve WebSocket traffic may expose both `RENDER(Request& context)` and `WS(Request& context)`. In that case `RENDER(Request& context)` serves the initial HTTP response and `WS(Request& context)` handles subsequent WebSocket messages. +Pages intended to serve WebSocket traffic may expose both `RENDER(Request& context)` and `WS(Request& context)`. Files may also define `COMPONENT()` handlers when they intentionally need both page and component behavior in one unit. In that case `RENDER(Request& context)` serves the direct HTTP response, `WS(Request& context)` handles subsequent WebSocket messages, and `COMPONENT()` remains available only through the component helpers. :see >ob diff --git a/site/doc/pages/1_preprocessor.txt b/site/doc/pages/1_preprocessor.txt index b6ee0be..3116e44 100644 --- a/site/doc/pages/1_preprocessor.txt +++ b/site/doc/pages/1_preprocessor.txt @@ -12,7 +12,8 @@ The implementation lives in `src/lib/compiler.cpp`. It does not try to parse all - Inside a literal block, `` emits `print(html_escape(expression));`. - Inside a literal block, `` emits `print(expression);` without HTML escaping. - `#load "other.uce"` injects another UCE unit at compile time. -- `RENDER(Request& context)` and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`. +- `RENDER(Request& context)`, `COMPONENT(Request& context)`, and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`. +- `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. :Pipeline @@ -25,6 +26,7 @@ The implementation lives in `src/lib/compiler.cpp`. It does not try to parse all - `` 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. +- Lines beginning with `COMPONENT:NAME(...)` are rewritten into exported `component_render_NAME(...)` functions for the component helpers. - The final generated source is written to `BIN_DIRECTORY + src_path + "/" + source_file + ".cpp"`. - `scripts/compile` then compiles that generated `.cpp` into `source_file + ".so"` with `clang++ -shared -std=c++20 ...`. @@ -93,3 +95,4 @@ load render_file call_file 0_context +1_COMPONENT diff --git a/site/doc/pages/DTree.txt b/site/doc/pages/DTree.txt index 84cb1bc..985d286 100644 --- a/site/doc/pages/DTree.txt +++ b/site/doc/pages/DTree.txt @@ -18,6 +18,7 @@ Useful methods include: `to_string()` `to_json()` `get_type_name()` +`get_by_path()` `set_bool()` `remove()` `clear()` diff --git a/site/doc/pages/ascii_safe_name.txt b/site/doc/pages/ascii_safe_name.txt new file mode 100644 index 0000000..85bfb97 --- /dev/null +++ b/site/doc/pages/ascii_safe_name.txt @@ -0,0 +1,14 @@ +:sig +String ascii_safe_name(String raw) + +:params +raw : input string to normalize +return value : ASCII-safe identifier made from letters, digits, and underscores + +:desc +Builds a conservative identifier by keeping ASCII letters, digits, and underscores and dropping other characters. + +This is useful when turning user- or config-provided names into handler suffixes, DOM-safe variable stems, or CSS/JS hook names. + +:see +>string diff --git a/site/doc/pages/component.txt b/site/doc/pages/component.txt index ff89191..f0f1199 100644 --- a/site/doc/pages/component.txt +++ b/site/doc/pages/component.txt @@ -10,9 +10,11 @@ Component props are passed in `context.call`. Because `` HTML-escapes its value, embed component markup with ``, `print(component(...))`, or use `render_component(...)` for direct output. -When `name` contains a colon, such as `components/card:BODY`, the part after the colon selects a named render handler exported from the component file through `RENDER:BODY(Request& context)`. +When `name` contains a colon, such as `components/card:BODY`, the part after the colon selects a named component handler exported from the component file through `COMPONENT:BODY(Request& context)`. -The default handler is `RENDER(Request& context)`. +The default handler is `COMPONENT(Request& context)`. + +When `name` starts with a colon, such as `:BODY`, the target resolves against the current `.uce` file so component files can call their own named handlers without repeating the file name. Resolution order is: exact file name diff --git a/site/doc/pages/get_by_path.txt b/site/doc/pages/get_by_path.txt new file mode 100644 index 0000000..89a91f1 --- /dev/null +++ b/site/doc/pages/get_by_path.txt @@ -0,0 +1,20 @@ +:sig +DTree DTree::get_by_path(String path, String delim = "/") + +:params +path : slash-delimited path to traverse +delim : optional path separator +return value : the resolved child node, or an empty `DTree` when the path cannot be followed + +:desc +Traverses a nested `DTree` without creating missing keys. + +Empty path segments are ignored, so leading and trailing `/` characters are harmless. If any intermediate node is not a map or a segment is missing, `get_by_path()` returns an empty `DTree`. + +Typical usage: +`context.cfg.get_by_path("theme/options/portal-dark/label").to_string()` + +:see +DTree +0_context +>types diff --git a/site/doc/pages/json_encode.txt b/site/doc/pages/json_encode.txt index 4d5e8b9..7d6f2c9 100644 --- a/site/doc/pages/json_encode.txt +++ b/site/doc/pages/json_encode.txt @@ -1,10 +1,15 @@ :sig +String json_encode(String s) String json_encode(DTree t) :params +s : string to encode as a JSON string literal t : DTree object to be serialized return value : string containing the JSON result :desc -Serializes a DTree structure 't' into a String in JSON notation. +Serializes either a `String` or a `DTree` into JSON notation. +When passed a `String`, `json_encode()` returns a quoted and escaped JSON string literal. + +When passed a `DTree`, scalar values are serialized directly and nested map values are emitted as JSON objects. diff --git a/site/doc/pages/path_join.txt b/site/doc/pages/path_join.txt new file mode 100644 index 0000000..8a1a59a --- /dev/null +++ b/site/doc/pages/path_join.txt @@ -0,0 +1,15 @@ +:sig +String path_join(String base, String child) + +:params +base : parent path +child : child path or absolute override +return value : combined path + +:desc +Joins two filesystem-style path fragments with a single `/` when needed. + +If `child` is empty, `base` is returned. If `child` already starts with `/`, it is returned unchanged. This makes `path_join()` a better fit for app-level path assembly than open-coded string concatenation. + +:see +>sys diff --git a/site/doc/pages/render_component.txt b/site/doc/pages/render_component.txt index 34fde7d..9c6fb8d 100644 --- a/site/doc/pages/render_component.txt +++ b/site/doc/pages/render_component.txt @@ -6,7 +6,9 @@ Renders another `.uce` file as a component and writes the result directly to the This is the direct-output counterpart to `component()`. -Component props are passed through `context.call`, and `name:RENDERFUNC` may be used to select a named handler exported by `RENDER:RENDERFUNC(Request& context)`. +Component props are passed through `context.call`, and `name:COMPONENTFUNC` may be used to select a named handler exported by `COMPONENT:COMPONENTFUNC(Request& context)`. + +When `name` starts with `:`, the runtime resolves that named handler against the current `.uce` file. Use `render_component()` when you want to write component output directly from C++ code instead of capturing it as a `String`. diff --git a/site/doc/pages/set_status.txt b/site/doc/pages/set_status.txt new file mode 100644 index 0000000..9e6067f --- /dev/null +++ b/site/doc/pages/set_status.txt @@ -0,0 +1,15 @@ +:sig +void context.set_status(s32 code, String reason = "") + +:params +code : HTTP status code +reason : optional reason phrase override + +:desc +Sets the current request status line and mirrors the numeric status into `context.flags.status`. + +When `reason` is omitted, UCE fills in a standard reason phrase for common HTTP status codes such as `200`, `302`, `400`, `404`, and `500`. + +:see +0_context +>types diff --git a/site/examples/uce-starter/components/auth/oauth-client.uce b/site/examples/uce-starter/components/auth/oauth-client.uce index 2422967..7439a78 100644 --- a/site/examples/uce-starter/components/auth/oauth-client.uce +++ b/site/examples/uce-starter/components/auth/oauth-client.uce @@ -1,6 +1,6 @@ #load "../../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { String title = first(context.call["title"].to_string(), "Sign in with OAuth"); String subtitle = first(context.call["subtitle"].to_string(), "Choose your preferred authentication method"); @@ -42,7 +42,7 @@ RENDER(Request& context) ; + const clientId = ; if (!clientId || clientId.indexOf('YOUR_') === 0) { alert(' OAuth is not configured yet.'); if (button) button.style.display = ''; @@ -74,21 +74,21 @@ RENDER(Request& context) const state = btoa(Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2)).replace(/[^a-zA-Z0-9]/g, ''); const params = new URLSearchParams({ client_id: clientId, - redirect_uri: , - scope: , + redirect_uri: , + scope: , response_type: 'code', state: state }); sessionStorage.setItem('oauth_state', state); - sessionStorage.setItem('oauth_service', ); + sessionStorage.setItem('oauth_service', ); - fetch(, { + fetch(, { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({oauth_service: , oauth_state: state}) + body: JSON.stringify({oauth_service: , oauth_state: state}) }).catch(console.error).finally(() => { - window.location.href = + '?' + params.toString(); + window.location.href = + '?' + params.toString(); }); }; diff --git a/site/examples/uce-starter/components/basic/cookie-consent.uce b/site/examples/uce-starter/components/basic/cookie-consent.uce index 174f0ac..05bfa76 100644 --- a/site/examples/uce-starter/components/basic/cookie-consent.uce +++ b/site/examples/uce-starter/components/basic/cookie-consent.uce @@ -1,6 +1,6 @@ #load "../../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { starter_boot(context); diff --git a/site/examples/uce-starter/components/data/widgets.uce b/site/examples/uce-starter/components/data/widgets.uce index ee07030..a14928d 100644 --- a/site/examples/uce-starter/components/data/widgets.uce +++ b/site/examples/uce-starter/components/data/widgets.uce @@ -74,7 +74,7 @@ DTree data_format_value(DTree value, DTree row, DTree column) return(result); } -RENDER:SUMMARY_METRICS(Request& context) +COMPONENT:SUMMARY_METRICS(Request& context) { String title = context.call["title"].to_string(); String subtitle = context.call["subtitle"].to_string(); @@ -103,7 +103,7 @@ RENDER:SUMMARY_METRICS(Request& context) } -RENDER:TIMESERIES_CHART(Request& context) +COMPONENT:TIMESERIES_CHART(Request& context) { starter_register_js("js/u-format.js", context); starter_register_js("js/u-timeseries-chart.js", context); @@ -132,21 +132,21 @@ RENDER:TIMESERIES_CHART(Request& context) } -RENDER:SORTABLE_TABLE(Request& context) +COMPONENT:SORTABLE_TABLE(Request& context) { starter_register_js("js/u-format.js", context); starter_register_js("js/u-sortable-table.js", context); @@ -205,13 +205,13 @@ RENDER:SORTABLE_TABLE(Request& context) } -RENDER:DATA_TABLE(Request& context) +COMPONENT:DATA_TABLE(Request& context) { String table_id = first(context.call["id"].to_string(), "data-grid-" + std::to_string((u64)time())); starter_register_js("js/ag-grid/ag-grid-community.min.js", context); @@ -259,15 +259,15 @@ RENDER:DATA_TABLE(Request& context) (function () { if (typeof agGrid === 'undefined') return; const gridOptions = ; - new agGrid.Grid(document.getElementById(), gridOptions); - document.getElementById()?.addEventListener('input', function () { + new agGrid.Grid(document.getElementById(), gridOptions); + document.getElementById()?.addEventListener('input', function () { gridOptions.api.setQuickFilter(this.value); }); - document.getElementById()?.addEventListener('click', function () { + document.getElementById()?.addEventListener('click', function () { gridOptions.api.setFilterModel(null); gridOptions.api.setQuickFilter(''); }); - document.getElementById()?.addEventListener('click', function () { + document.getElementById()?.addEventListener('click', function () { gridOptions.api.exportDataAsCsv(); }); }()); diff --git a/site/examples/uce-starter/components/example/marketing_blocks.uce b/site/examples/uce-starter/components/example/marketing_blocks.uce index 574e2b4..5027036 100644 --- a/site/examples/uce-starter/components/example/marketing_blocks.uce +++ b/site/examples/uce-starter/components/example/marketing_blocks.uce @@ -42,7 +42,7 @@ void marketing_default_features(DTree& features) features.push(item); } -RENDER:HERO_SECTION(Request& context) +COMPONENT:HERO_SECTION(Request& context) { String title = first(context.call["title"].to_string(), "Welcome to the Present"); String subtitle = first(context.call["subtitle"].to_string(), "Experience no-quite-modern web development with our cutting-edge framework"); @@ -192,7 +192,7 @@ RENDER:HERO_SECTION(Request& context) } -RENDER:FEATURES_GRID(Request& context) +COMPONENT:FEATURES_GRID(Request& context) { DTree features = context.call["features"]; marketing_default_features(features); @@ -286,7 +286,7 @@ RENDER:FEATURES_GRID(Request& context) } -RENDER:STATS_SECTION(Request& context) +COMPONENT:STATS_SECTION(Request& context) { DTree stats = context.call["stats"]; if(stats.get_type_name() != "array" || stats["0"]["label"].to_string() == "") @@ -362,7 +362,7 @@ RENDER:STATS_SECTION(Request& context) } -RENDER:BRANDS_SHOWCASE(Request& context) +COMPONENT:BRANDS_SHOWCASE(Request& context) { DTree logos = context.call["logos"]; if(logos.get_type_name() != "array" || logos["0"]["name"].to_string() == "") @@ -408,7 +408,7 @@ RENDER:BRANDS_SHOWCASE(Request& context) } -RENDER:TESTIMONIALS(Request& context) +COMPONENT:TESTIMONIALS(Request& context) { DTree testimonials = context.call["testimonials"]; if(testimonials.get_type_name() != "array" || testimonials["0"]["name"].to_string() == "") @@ -471,7 +471,7 @@ RENDER:TESTIMONIALS(Request& context) } -RENDER:PRICING_TABLE(Request& context) +COMPONENT:PRICING_TABLE(Request& context) { DTree plans = context.call["plans"]; if(plans.get_type_name() != "array" || plans["0"]["name"].to_string() == "") @@ -544,7 +544,7 @@ RENDER:PRICING_TABLE(Request& context) } -RENDER:CTA_SECTION(Request& context) +COMPONENT:CTA_SECTION(Request& context) { String title = first(context.call["title"].to_string(), "Ready to Get Started?"); String subtitle = first(context.call["subtitle"].to_string(), "Join thousands of developers building amazing applications with our framework."); diff --git a/site/examples/uce-starter/components/example/theme-switcher.uce b/site/examples/uce-starter/components/example/theme-switcher.uce index 75c3add..28f7de7 100644 --- a/site/examples/uce-starter/components/example/theme-switcher.uce +++ b/site/examples/uce-starter/components/example/theme-switcher.uce @@ -1,10 +1,10 @@ #load "../../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { starter_boot(context); - String current_theme = starter_theme_value("key", context); - String current_label = first(starter_theme_value("label", context), current_theme); + 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.var["starter"]["route"]["l_path"].to_string(); <> @@ -108,7 +108,7 @@ RENDER(Request& context)