working on documentation and more API functions

This commit is contained in:
Udo 2026-04-29 12:09:37 +00:00
parent cd445f3c9b
commit 9f7625c7fd
94 changed files with 1896 additions and 458 deletions

View File

@ -13,7 +13,7 @@ UCE is a PHP-inspired server-side runtime that lets you build web pages and hand
- WebSocket pages can additionally expose `WS(Request& context)` - WebSocket pages can additionally expose `WS(Request& context)`
- sub-rendering and components pass structured data through `context.props` - sub-rendering and components pass structured data through `context.props`
- nginx can forward normal `.uce` requests and ordinary `.ws.uce` page loads to the FastCGI socket, while real WebSocket upgrade requests for `.ws.uce` endpoints go to the built-in HTTP/WebSocket listener - nginx can forward normal `.uce` requests and ordinary `.ws.uce` page loads to the FastCGI socket, while real WebSocket upgrade requests for `.ws.uce` endpoints go to the built-in HTTP/WebSocket listener
- the nginx-published application tree now lives under `site/` - the nginx-published application tree lives under `site/`
- you can include C++ code as much as you want, but only .uce files called via API functions and entry points will be pre-processed - you can include C++ code as much as you want, but only .uce files called via API functions and entry points will be pre-processed
- the preprocessor has two jobs: - the preprocessor has two jobs:
- allow for inline HTML within C++ and the use of templating tags inside of that HTML - allow for inline HTML within C++ and the use of templating tags inside of that HTML
@ -42,6 +42,7 @@ The current build expects:
- `clang++` - `clang++`
- `mysql_config` - `mysql_config`
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
- standard Linux development headers for `dl`, `pthread`, sockets, and backtrace support - standard Linux development headers for `dl`, `pthread`, sockets, and backtrace support
The binary is written to: The binary is written to:
@ -52,7 +53,7 @@ bin/uce_fastcgi.linux.bin
## Runtime Model ## Runtime Model
UCE pages now use explicit request handlers instead of implicit globals: UCE pages use explicit request handlers instead of implicit globals:
- `RENDER(Request& context)` for normal HTTP rendering - `RENDER(Request& context)` for normal HTTP rendering
- `WS(Request& context)` for inbound WebSocket messages - `WS(Request& context)` for inbound WebSocket messages
@ -68,7 +69,7 @@ Useful related runtime patterns:
- `context.params`, `context.get`, `context.post`, `context.cookies`, `context.session`, and `context.header` for request/response state - `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 - `context.set_status(code[, reason])` to set the HTTP response status
Useful helpers for that data model now include: Useful helpers for that data model include:
- `DTree::get_by_path("a/b/c")` for path-style config traversal without creating missing keys - `DTree::get_by_path("a/b/c")` for path-style config traversal without creating missing keys
- `DTree::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended - `DTree::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended
@ -90,9 +91,14 @@ COMPONENT:BODY(Request& context)
Those are intended for sub-rendering through helpers such as `component("components/card:BODY", props, context)` rather than direct page entry. Those are intended for sub-rendering through helpers such as `component("components/card:BODY", props, context)` rather than direct page entry.
Additional lifecycle hooks are also available on ordinary `.uce` units:
- `INIT(Request& context)` runs once when a worker loads that unit's shared object into memory
- `ONCE(Request& context)` runs once per request before the first `RENDER()` or `COMPONENT...` entrypoint from that file
## Template Output ## Template Output
UCE now treats template parsing as one shared code-vs-literal state machine. UCE treats template parsing as one shared code-vs-literal state machine.
- `<>` and `?>` both enter literal output mode - `<>` and `?>` both enter literal output mode
- `</>` and `<?` both return to code mode - `</>` and `<?` both return to code mode
@ -106,9 +112,9 @@ Inside literal output, UCE supports three inline forms:
Use `<?= ... ?>` by default for user-visible text. Use `<?: ... ?>` only for trusted markup or content that has already been escaped. Use `<?= ... ?>` by default for user-visible text. Use `<?: ... ?>` only for trusted markup or content that has already been escaped.
The parser now treats C++ `//` and `/* ... */` comments as comments in both normal code and `<? ... ?>` islands, so quotes or delimiter markers inside comments do not confuse template parsing. The parser treats C++ `//` and `/* ... */` comments as comments in both normal code and `<? ... ?>` islands, so quotes or delimiter markers inside comments do not confuse template parsing.
The preprocessing implementation is now split between `src/lib/compiler.cpp` and `src/lib/compiler-parser.cpp`. `compiler.cpp` owns unit compilation and cache orchestration, while `compiler-parser.cpp` owns source rewriting and template parsing. The preprocessing implementation is split between `src/lib/compiler.cpp` and `src/lib/compiler-parser.cpp`. `compiler.cpp` owns unit compilation and cache orchestration, while `compiler-parser.cpp` owns source rewriting and template parsing.
## Components ## Components
@ -141,6 +147,8 @@ Components expose `COMPONENT(Request& context)` as their default entrypoint and
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 `component_render(":NAME", props, context)` target another named component handler in that same file. 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 `component_render(":NAME", props, context)` target another named component handler in that same file.
If the component file also defines `ONCE(Request& context)`, that hook runs once per request before the file's first component/render entrypoint. If it defines `INIT(Request& context)`, that hook runs once when the worker loads the unit.
## WebSockets ## WebSockets
The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate API to page code: The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate API to page code:
@ -158,7 +166,7 @@ The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate
By default, the WebSocket scope is the current page file, so `ws_send()` queues a message for clients connected to that same `.ws.uce` endpoint. By default, the WebSocket scope is the current page file, so `ws_send()` queues a message for clients connected to that same `.ws.uce` endpoint.
Each live WebSocket connection now owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client. Each live WebSocket connection owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client.
The current inbound payload is available directly as `context.in`, and the runtime mirrors message metadata into `context.params` using keys such as `WS_CONNECTION_ID`, `WS_SCOPE`, `WS_CONNECTION_COUNT`, `WS_OPCODE`, `WS_MESSAGE_TYPE`, and `WS_DOCUMENT_URI`. The current inbound payload is available directly as `context.in`, and the runtime mirrors message metadata into `context.params` using keys such as `WS_CONNECTION_ID`, `WS_SCOPE`, `WS_CONNECTION_COUNT`, `WS_OPCODE`, `WS_MESSAGE_TYPE`, and `WS_DOCUMENT_URI`.
@ -166,11 +174,11 @@ The current inbound payload is available directly as `context.in`, and the runti
Set `binary = true` on `ws_send()` or `ws_send_to()` to queue a binary frame instead of a text frame. Set `binary = true` on `ws_send()` or `ws_send_to()` to queue a binary frame instead of a text frame.
The runtime now accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS(Request& context)`. The runtime accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS(Request& context)`.
## Error Reporting ## Error Reporting
Unhandled exceptions and recovered fatal request signals now return a `500 Internal Server Error` response with a plain-text trace instead of simply dropping the upstream connection and leaving nginx to show a generic `502`. Unhandled exceptions and recovered fatal request signals return a `500 Internal Server Error` response with a plain-text trace instead of simply dropping the upstream connection and leaving nginx to show a generic `502`.
The demo page `site/test/error-reporting.uce` can be used to exercise: The demo page `site/test/error-reporting.uce` can be used to exercise:
@ -232,7 +240,7 @@ On a Debian or Ubuntu host, start with the packages needed to build and run UCE
```bash ```bash
apt update apt update
apt install -y nginx clang mariadb-client libmariadb-dev build-essential apt install -y nginx clang mariadb-client libmariadb-dev libpcre2-dev build-essential
``` ```
The exact package names may vary by distro. The important requirements are: The exact package names may vary by distro. The important requirements are:
@ -240,6 +248,7 @@ The exact package names may vary by distro. The important requirements are:
- `nginx` - `nginx`
- `clang++` - `clang++`
- `mysql_config` - `mysql_config`
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
- normal Linux development headers for threads, sockets, `dl`, and backtrace support - normal Linux development headers for threads, sockets, `dl`, and backtrace support
### 2. Put the repo on the server ### 2. Put the repo on the server
@ -484,7 +493,7 @@ Common failure modes:
- WebSocket upgrade fails - WebSocket upgrade fails
Check that nginx is routing `.ws.uce` to `proxy_pass`, not `fastcgi_pass`, and that `HTTP_PORT` is reachable on localhost. Check that nginx is routing `.ws.uce` to `proxy_pass`, not `fastcgi_pass`, and that `HTTP_PORT` is reachable on localhost.
- Requests compile but immediately crash - Requests compile but immediately crash
Check `journalctl -u uce.service`. Generated units now carry an ABI metadata sidecar and should be recompiled automatically after runtime ABI changes, but clearing stale artifacts under `BIN_DIRECTORY` is still a useful last-resort recovery step if the cache has been damaged manually. Check `journalctl -u uce.service`. Generated units carry an ABI metadata sidecar and should be recompiled automatically after runtime ABI changes, but clearing stale artifacts under `BIN_DIRECTORY` is still a useful last-resort recovery step if the cache has been damaged manually.
- nginx serves raw source or internal files - nginx serves raw source or internal files
Tighten the server root and add explicit deny rules for non-public directories. Tighten the server root and add explicit deny rules for non-public directories.

View File

@ -20,8 +20,8 @@ JIT_COMPILE_ON_REQUEST=1
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP # ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
PROACTIVE_COMPILE_ENABLED=1 PROACTIVE_COMPILE_ENABLED=1
# AFTER A FAILED COMPILE, SERVE THE LAST COMPILER OUTPUT AND WAIT THIS MANY SECONDS # AFTER A FAILED COMPILE, UCE NOW REUSES THE PERSISTED COMPILER OUTPUT UNTIL
# BEFORE TRYING AGAIN IF THE SOURCE FILE HAS NOT CHANGED # THE SOURCE OR COMPILER INPUTS CHANGE. THIS SETTING IS KEPT FOR COMPATIBILITY.
COMPILE_FAILURE_RETRY_SECONDS=10 COMPILE_FAILURE_RETRY_SECONDS=10
# PERIODIC KNOWN-.uce RECHECK INTERVAL IN SECONDS # PERIODIC KNOWN-.uce RECHECK INTERVAL IN SECONDS

View File

@ -14,7 +14,7 @@ mkdir work > /dev/null 2>&1
COMPILER="clang++" COMPILER="clang++"
FLAGS="-g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math" FLAGS="-g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math"
LIBS="-ldl -lm -lpthread `mysql_config --cflags --libs`" LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs`"
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\"" SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\""
echo "Compliling executable..." echo "Compliling executable..."

View File

@ -38,6 +38,7 @@ RENDER(Request& context)
<? render_card("dtree.uce", "DTree", "Dynamic hierarchical data tree"); ?> <? render_card("dtree.uce", "DTree", "Dynamic hierarchical data tree"); ?>
<? render_card("json.uce", "JSON", "Parse and encode JSON data"); ?> <? render_card("json.uce", "JSON", "Parse and encode JSON data"); ?>
<? render_card("preprocessor-comments.uce", "Preprocessor Comments", "Regression coverage for comment parsing in templates"); ?> <? 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("string.uce", "String", "String operations"); ?>
<? render_card("str_replace.uce", "String Replace", "Search and replace in strings"); ?> <? render_card("str_replace.uce", "String Replace", "Search and replace in strings"); ?>
<? render_card("utf8.uce", "UTF-8", "Unicode string handling"); ?> <? render_card("utf8.uce", "UTF-8", "Unicode string handling"); ?>
@ -61,6 +62,7 @@ RENDER(Request& context)
<div class="grid-heading">Advanced</div> <div class="grid-heading">Advanced</div>
<? render_card("call_file.uce", "unit_call()", "Dynamic file inclusion"); ?> <? render_card("call_file.uce", "unit_call()", "Dynamic file inclusion"); ?>
<? render_card("components.uce", "Components", "Reusable component system"); ?> <? render_card("components.uce", "Components", "Reusable component system"); ?>
<? render_card("once-init.uce", "ONCE / INIT", "Unit lifecycle hooks for worker load and request entry"); ?>
<? render_card("markdown.uce", "Markdown", "Markdown parsing with components"); ?> <? render_card("markdown.uce", "Markdown", "Markdown parsing with components"); ?>
<? render_card("script.uce", "Script", "UCE script integration"); ?> <? render_card("script.uce", "Script", "UCE script integration"); ?>
<? render_card("websockets.ws.uce", "WebSockets", "Real-time WebSocket chat"); ?> <? render_card("websockets.ws.uce", "WebSockets", "Real-time WebSocket chat"); ?>

53
site/demo/once-init.uce Normal file
View File

@ -0,0 +1,53 @@
static s64 demo_worker_init_count = 0;
static s64 demo_component_hits = 0;
INIT(Request& context)
{
(void)context;
demo_worker_init_count += 1;
}
ONCE(Request& context)
{
context.call["once_hits"] = context.call["once_hits"].to_s64() + 1;
}
COMPONENT:PROBE(Request& context)
{
demo_component_hits += 1;
<>
<div class="banner">
<strong><?= context.props["label"].to_string() ?></strong>
<div>worker INIT count for this loaded unit: <?= (u64)demo_worker_init_count ?></div>
<div>request ONCE count for this request: <?= context.call["once_hits"].to_u64() ?></div>
<div>component handler calls served by this worker copy: <?= (u64)demo_component_hits ?></div>
</div>
</>
}
RENDER(Request& context)
{
DTree first;
first["label"] = "First component call";
DTree second;
second["label"] = "Second component call in the same request";
DTree third;
third["label"] = "Third component call through unit_call(\"COMPONENT:PROBE\")";
<><html>
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
<h1>
<a href="index.uce">UCE Test</a>:
ONCE() and INIT()
</h1>
<p>
This page calls the same named component twice. `ONCE()` should only run once for the request, while `INIT()` should stay stable for the currently loaded worker copy.
</p>
<?: component(":PROBE", first, context) ?>
<?: component(":PROBE", second, context) ?>
<? unit_call("once-init.uce", "COMPONENT:PROBE", &third); ?>
</html></>
}

View File

@ -11,6 +11,7 @@ RENDER(Request& context)
</h1> </h1>
<p>This page exists to prove the template parser ignores quotes and template markers that appear inside C++ comments.</p> <p>This page exists to prove the template parser ignores quotes and template markers that appear inside C++ comments.</p>
<p>It also renders a literal raw-string terminator sequence safely: <code>)"</code>.</p>
<? <?
// Regression: this comment's apostrophe must not swallow the later ?> marker. // Regression: this comment's apostrophe must not swallow the later ?> marker.
@ -36,4 +37,4 @@ RENDER(Request& context)
<pre><?= var_dump(context.params) ?></pre> <pre><?= var_dump(context.params) ?></pre>
</details> </details>
</> </>
} }

74
site/demo/regex.uce Normal file
View File

@ -0,0 +1,74 @@
#include "demo_guard.h"
void regex_row(String label, String result, String expect)
{
bool pass = (result == expect);
<><tr>
<td class="test-label"><?= label ?></td>
<td class="test-result"><code><?= result ?></code></td>
<td class="test-status <?= pass ? "pass" : "fail" ?>"><?= pass ? "pass" : "expected: " + expect ?></td>
</tr></>
}
void regex_bool_row(String label, bool result, bool expect)
{
regex_row(label, result ? "true" : "false", expect ? "true" : "false");
}
RENDER(Request& context)
{
String text = "Contact ops@example.test or tag #uce, #docs, and café.";
DTree first_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", text);
DTree tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", text);
StringList pieces = regex_split("\\s*,\\s*", "uce, components, markdown");
<>
<link rel="stylesheet" href='style.css'></link>
<style>
table.tests { width: 100%; border-collapse: collapse; margin-bottom: 8px; }
table.tests td { padding: 6px 12px; border-bottom: 1px solid rgba(255,255,255,0.06); font-family: var(--font-mono); font-size: 0.88rem; }
.test-label { color: var(--text-dim); white-space: nowrap; width: 1%; }
.test-result code { background: var(--bg-code); padding: 2px 8px; border-radius: 4px; }
.test-status.pass { color: #6f6; width: 1%; }
.test-status.fail { color: #f66; }
</style>
<h1>
<a href="index.uce">UCE Test</a>:
Regular Expressions
</h1>
<p>UCE regex functions use PCRE2 and return ordinary UCE strings, lists, and DTree values.</p>
<p>sample = <code><?= text ?></code></p>
<h2>Validation And Search</h2>
<table class="tests"><?
regex_bool_row("regex_match(\"[A-Z][a-z]+\", \"Alice\")", regex_match("[A-Z][a-z]+", "Alice"), true);
regex_bool_row("regex_match(\"[A-Z][a-z]+\", \"Alice!\")", regex_match("[A-Z][a-z]+", "Alice!"), false);
regex_bool_row("regex_search(\"example\", text)[\"matched\"]", first_email["matched"].to_bool(), true);
regex_row("first_email[\"match\"]", first_email["match"].to_string(), "ops@example.test");
regex_row("first_email[\"named\"][\"user\"]", first_email["named"]["user"].to_string(), "ops");
regex_row("first_email[\"named\"][\"host\"]", first_email["named"]["host"].to_string(), "example.test");
regex_bool_row("regex_match(\"\\\\p{L}+\", \"café\")", regex_match("\\p{L}+", "café"), true);
?></table>
<h2>All Matches</h2>
<table class="tests"><?
regex_row("regex_search_all hashtags count", tags["count"].to_string(), "2.000000");
regex_row("first hashtag", tags["matches"]["0"]["named"]["tag"].to_string(), "uce");
regex_row("second hashtag", tags["matches"]["1"]["named"]["tag"].to_string(), "docs");
?></table>
<h2>Replace And Split</h2>
<table class="tests"><?
regex_row("regex_replace(\"#([A-Za-z0-9_]+)\", \"<tag>$1</tag>\", \"#uce\")", regex_replace("#([A-Za-z0-9_]+)", "<tag>$1</tag>", "#uce"), "<tag>uce</tag>");
regex_row("join(regex_split(\"\\\\s*,\\\\s*\", ...), \"|\")", join(pieces, "|"), "uce|components|markdown");
regex_row("case-insensitive flag", regex_search("uce", "Hello UCE", "i")["match"].to_string(), "UCE");
?></table>
<h2>Structured Result</h2>
<pre><?= json_encode(first_email) ?></pre>
<p><a href="../doc/index.uce?p=regex_search">Read the regex API documentation</a></p>
</>
}

7
site/doc/areas/regex.txt Normal file
View File

@ -0,0 +1,7 @@
Regular Expressions
regex_match
regex_search
regex_search_all
regex_replace
regex_split

View File

@ -17,6 +17,11 @@ split
split_space split_space
split_utf8 split_utf8
replace replace
regex_match
regex_search
regex_search_all
regex_replace
regex_split
to_lower to_lower
to_upper to_upper
trim trim

View File

@ -1,6 +1,6 @@
Task API Task API
kill
task task
task_repeat task_repeat
task_pid task_pid
task_kill

View File

@ -1,8 +1,13 @@
Types Types
0_Request
array_merge array_merge
DTree 0_DTree
get_by_path get_by_path
set_status set_status
String String
StringList
StringMap StringMap
to_bool
to_f64
to_u64

View File

@ -1,104 +1,25 @@
struct DocPage { #include "lib/doc_page.h"
String title;
String content;
StringList sig_lines;
StringList param_lines;
StringList see_lines;
};
String doc_default_title(String page) void render_doc_page_link(String page, String label = "", String badge = "")
{ {
String page_title = page; page = trim(page);
if(page_title.length() > 1 && page_title[1] == '_') if(page == "")
nibble(page_title, "_"); return;
return(page_title); if(label == "")
} label = doc_index_label(page);
if(badge == "")
badge = doc_page_kind_badge(doc_page_kind(page));
String doc_markdown_inline(String text) ?><a href="index.uce?p=<?= uri_encode(page) ?>"><?= label ?><?
{ if(badge != "")
text = trim(text);
if(text == "")
return("");
String html = markdown_to_html(text);
if(html.length() >= 7 && html.substr(0, 3) == "<p>" && html.substr(html.length() - 4) == "</p>")
return(html.substr(3, html.length() - 7));
return(html);
}
String doc_legacy_heading(String section)
{
if(section == "desc")
return("");
if(section == "related")
return("## PHP & JS Equivalents");
return("## " + section);
}
DocPage load_doc_page(String page)
{
DocPage result;
StringList lines = split(file_get_contents("pages/" + page + ".txt"), "\n");
String current_section = "";
bool content_mode = false;
StringList content_lines;
for(auto line : lines)
{ {
if(!content_mode && line != "" && line.substr(0, 1) == ":") ?><span class="badge"><?= badge ?></span><?
{
String section = trim(line.substr(1));
if(section == "title" || section == "sig" || section == "params" || section == "see")
{
current_section = section;
continue;
}
if(section == "content")
{
content_mode = true;
current_section = "content";
continue;
}
current_section = "legacy";
String heading = doc_legacy_heading(section);
if(heading != "")
{
if(content_lines.size() > 0 && content_lines.back() != "")
content_lines.push_back("");
content_lines.push_back(heading);
content_lines.push_back("");
}
continue;
}
if(current_section == "title")
{
if(result.title != "")
result.title += "\n";
result.title += line;
}
else if(current_section == "sig")
{
result.sig_lines.push_back(line);
}
else if(current_section == "params")
{
result.param_lines.push_back(line);
}
else if(current_section == "see")
{
if(trim(line) != "")
result.see_lines.push_back(trim(line));
}
else
{
content_lines.push_back(line);
}
} }
else
result.content = join(content_lines, "\n"); {
result.title = trim(result.title); ?><span class="dim">()</span><?
return(result); }
?></a><?
} }
void render_doc_params(StringList param_lines) void render_doc_params(StringList param_lines)
@ -130,13 +51,19 @@ void render_see_section(String name)
s32 idx = 0; s32 idx = 0;
for(auto line : lines) for(auto line : lines)
{ {
line = trim(line);
if(line == "")
{
idx += 1;
continue;
}
if(idx == 0) if(idx == 0)
{ {
<><div class="category"><h3><?= line ?></h3><ul></> <><div class="category"><h3><?= line ?></h3><ul></>
} }
else if(line != "") else
{ {
<><li><a href="index.uce?p=<?= uri_encode(line) ?>"><?= line ?><span class="dim">()</span></a></li></> ?><li><? render_doc_page_link(line); ?></li><?
} }
idx += 1; idx += 1;
} }
@ -154,11 +81,19 @@ void render_doc_see_links(StringList see_lines)
{ {
if(sl[0] == '>') if(sl[0] == '>')
{ {
render_see_section(sl.substr(1)); String target = trim(sl.substr(1));
if(doc_has_area(target))
{
render_see_section(target);
}
else if(doc_has_page(target))
{
?><div><? render_doc_page_link(target); ?></div><?
}
} }
else else
{ {
?><div><a href="index.uce?p=<?= trim(sl) ?>"><?= trim(sl) ?><span class="dim">()</span></a></div><? ?><div><? render_doc_page_link(trim(sl)); ?></div><?
} }
} }
?> ?>
@ -216,36 +151,9 @@ RENDER(Request& context)
for(auto file_name : ls("pages/")) for(auto file_name : ls("pages/"))
{ {
String ft = nibble(file_name, "."); String ft = nibble(file_name, ".");
if(ft.substr(0, 2) == "0_") String label = doc_index_label(ft);
{ String badge = doc_page_kind_badge(doc_page_kind(ft));
String fn = ft; ?><div class="func-item"><? render_doc_page_link(ft, label, badge); ?></div><?
String pre = nibble(fn, "_");
?>
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= fn ?><span class="badge">struct</span></a></div>
<?
}
else if(ft.substr(0, 2) == "1_")
{
String fn = ft;
String pre = nibble(fn, "_");
?>
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= fn ?><span class="badge">directive</span></a></div>
<?
}
else if(ft.substr(0, 2) == "3_")
{
String fn = ft;
String pre = nibble(fn, "_");
?>
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= fn ?><span class="badge">info</span></a></div>
<?
}
else
{
?>
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= ft ?><span class="dim">()</span></a></div>
<?
}
} }
?></div> ?></div>
</main> </main>

153
site/doc/lib/doc_page.h Normal file
View File

@ -0,0 +1,153 @@
#pragma once
struct DocPage {
String title;
String content;
StringList sig_lines;
StringList param_lines;
StringList see_lines;
};
enum class DocPageKind
{
function,
struct_page,
directive,
info
};
String doc_default_title(String page)
{
String page_title = page;
if(page_title.length() > 1 && page_title[1] == '_')
nibble(page_title, "_");
return(page_title);
}
String doc_markdown_inline(String text)
{
text = trim(text);
if(text == "")
return("");
String html = markdown_to_html(text);
if(html.length() >= 7 && html.substr(0, 3) == "<p>" && html.substr(html.length() - 4) == "</p>")
return(html.substr(3, html.length() - 7));
return(html);
}
String doc_legacy_heading(String section)
{
if(section == "desc")
return("");
if(section == "related")
return("## PHP & JS Equivalents");
return("## " + section);
}
bool doc_has_area(String name)
{
return(file_exists("areas/" + name + ".txt"));
}
bool doc_has_page(String name)
{
return(file_exists("pages/" + name + ".txt"));
}
DocPageKind doc_page_kind(String page)
{
if(page.substr(0, 2) == "0_")
return(DocPageKind::struct_page);
if(page.substr(0, 2) == "1_")
return(DocPageKind::directive);
if(page.substr(0, 2) == "3_")
return(DocPageKind::info);
return(DocPageKind::function);
}
String doc_page_kind_badge(DocPageKind kind)
{
if(kind == DocPageKind::struct_page)
return("struct");
if(kind == DocPageKind::directive)
return("directive");
if(kind == DocPageKind::info)
return("info");
return("");
}
String doc_index_label(String page)
{
String label = page;
auto kind = doc_page_kind(page);
if(kind == DocPageKind::struct_page || kind == DocPageKind::directive || kind == DocPageKind::info)
nibble(label, "_");
return(label);
}
DocPage load_doc_page(String page)
{
DocPage result;
StringList lines = split(file_get_contents("pages/" + page + ".txt"), "\n");
String current_section = "";
bool content_mode = false;
StringList content_lines;
for(auto line : lines)
{
if(!content_mode && line != "" && line.substr(0, 1) == ":")
{
String section = trim(line.substr(1));
if(section == "title" || section == "sig" || section == "params" || section == "see")
{
current_section = section;
continue;
}
if(section == "content")
{
content_mode = true;
current_section = "content";
continue;
}
current_section = "legacy";
String heading = doc_legacy_heading(section);
if(heading != "")
{
if(content_lines.size() > 0 && content_lines.back() != "")
content_lines.push_back("");
content_lines.push_back(heading);
content_lines.push_back("");
}
continue;
}
if(current_section == "title")
{
if(result.title != "")
result.title += "\n";
result.title += line;
}
else if(current_section == "sig")
{
result.sig_lines.push_back(line);
}
else if(current_section == "params")
{
result.param_lines.push_back(line);
}
else if(current_section == "see")
{
if(trim(line) != "")
result.see_lines.push_back(trim(line));
}
else
{
content_lines.push_back(line);
}
}
result.content = join(content_lines, "\n");
result.title = trim(result.title);
return(result);
}

View File

@ -6,6 +6,11 @@ Request& context;
:see :see
>types >types
set_status
component
unit_render
ws_message
session_start
:content :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 UCE handlers. It carries incoming request data, response state, runtime metadata, and helper trees such as `context.cfg`, `context.props`, and `context.connection`.

View File

@ -8,6 +8,8 @@ COMPONENT(Request& context)
>component >component
>component_render >component_render
>1_RENDER >1_RENDER
>1_INIT
>1_ONCE
>1_WS >1_WS
:content :content
@ -15,6 +17,10 @@ Defines the default component entrypoint for the current `.uce` file.
`component()` and `component_render()` call `COMPONENT(Request& context)` by default. Named component entrypoints use `COMPONENT:NAME(Request& context)`. `component()` and `component_render()` call `COMPONENT(Request& context)` by default. Named component entrypoints use `COMPONENT:NAME(Request& context)`.
If the same file defines `ONCE(Request& context)`, that hook runs once per request before the first `COMPONENT()` or `COMPONENT:NAME()` call for that unit.
If the file defines `INIT(Request& context)`, that hook runs once when the worker loads the compiled unit into memory.
## Why It Exists ## Why It Exists
This keeps page rendering and component rendering separate: This keeps page rendering and component rendering separate:

50
site/doc/pages/1_INIT.txt Normal file
View File

@ -0,0 +1,50 @@
:title
INIT
:sig
INIT(Request& context)
:see
>1_COMPONENT
>1_ONCE
>1_RENDER
>1_WS
>3_C++ Preprocessor
>unit_call
:content
Defines a worker-load hook for the current `.uce` unit.
When a worker loads the unit's compiled shared object into memory, the runtime checks whether the unit exposes `INIT(Request& context)`. If it does, the hook runs once for that load before the unit begins serving later requests from that in-memory copy.
Because UCE usually loads units on demand during a request, `INIT()` still receives a valid `Request& context`. Use it for worker-local initialization, not for request-local state that should reset each request.
## Typical Uses
- warm caches or parse static lookup data into globals
- initialize worker-local helper state for expensive component trees
- perform one-time registration work for that unit's in-memory copy
## Example
```cpp
std::map<String, String> cached_labels;
INIT(Request& context)
{
if(cached_labels.empty())
cached_labels["ready"] = "Ready";
}
COMPONENT(Request& context)
{
<>
<p><?= cached_labels["ready"] ?></p>
</>
}
```
## Related Concepts
- PHP: opcode-cache preload or one-time bootstrap work per worker process
- JavaScript / Node.js: module-load initialization or lazy singleton setup

49
site/doc/pages/1_ONCE.txt Normal file
View File

@ -0,0 +1,49 @@
:title
ONCE
:sig
ONCE(Request& context)
:see
>1_COMPONENT
>1_INIT
>1_RENDER
>1_WS
>3_C++ Preprocessor
>unit_call
:content
Defines a request-local one-time hook for the current `.uce` unit.
When a request first enters a given file through `RENDER(Request& context)`, `COMPONENT(Request& context)`, or any `COMPONENT:NAME(Request& context)` handler, the runtime checks whether that unit exposes `ONCE(Request& context)`. If it does, the hook runs before the selected render or component handler.
`ONCE()` is tracked per request and per resolved unit file, so repeated component calls to the same file inside one request do not rerun it.
## Typical Uses
- prepare request-local derived state on `context.call`
- load request-scoped config or data needed by multiple named component handlers
- normalize shared props before the unit's first render/component call
## Example
```cpp
ONCE(Request& context)
{
context.call["card_defaults"]["tone"] = "info";
}
COMPONENT(Request& context)
{
<>
<div class="card card-<?= context.call["card_defaults"]["tone"] ?>">
<?: component(":BODY", context.props, context) ?>
</div>
</>
}
```
## Related Concepts
- PHP: per-request bootstrap work before a template or partial first runs
- JavaScript / Node.js: request-scoped lazy initialization before a route or component render

View File

@ -6,6 +6,10 @@ RENDER(Request& context)
:see :see
>ob >ob
>1_COMPONENT
>1_INIT
>1_ONCE
>1_WS
:content :content
Defines the main HTTP render handler for the current `.uce` page. Defines the main HTTP render handler for the current `.uce` page.
@ -16,10 +20,14 @@ When a page is requested over HTTP, the runtime loads the target file and calls
The default page entrypoint is always the plain `RENDER(Request& context)` handler. 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()`. Reusable component handlers 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.props` tree. The request environment is passed explicitly through `context`, including params, cookies, post data, session state, headers, uploaded files, and the current `context.props` tree.
If the file defines `ONCE(Request& context)`, the runtime calls that hook once per request before the first `RENDER()` or `COMPONENT...` entrypoint from that unit runs.
If the file defines `INIT(Request& context)`, the runtime calls that hook once when the worker loads the compiled unit into memory.
For a normal direct page request, `context.props` starts empty. For a normal direct page request, `context.props` starts empty.
If the page is invoked from another UCE file via `unit_render(file_name, context)`, the callee receives that same `context`. If the page is invoked from another UCE file via `unit_render(file_name, context)`, the callee receives that same `context`.

View File

@ -6,6 +6,10 @@ WS(Request& context)
:see :see
>websocket >websocket
>1_COMPONENT
>1_INIT
>1_ONCE
>1_RENDER
:content :content
Defines the WebSocket message handler for the current `.ws.uce` page. Defines the WebSocket message handler for the current `.ws.uce` page.

View File

@ -8,7 +8,7 @@ UCE source preprocessing
load load
unit_render unit_render
unit_call unit_call
0_context 0_Request
1_COMPONENT 1_COMPONENT
:content :content
@ -25,7 +25,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- Inside a literal block, `<?= expression ?>` emits `print(html_escape(expression));`. - Inside a literal block, `<?= expression ?>` emits `print(html_escape(expression));`.
- Inside a literal block, `<?: expression ?>` emits `print(expression);` without HTML escaping. - Inside a literal block, `<?: expression ?>` emits `print(expression);` without HTML escaping.
- `#load "other.uce"` injects another UCE unit at compile time. - `#load "other.uce"` injects another UCE unit at compile time.
- `RENDER(Request& context)`, `COMPONENT(Request& context)`, and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`. - `RENDER(Request& context)`, `COMPONENT(Request& context)`, `ONCE(Request& context)`, `INIT(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. - `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. - `EXPORT` is also a normal C++ macro, but the custom pass additionally records exported declarations for metadata.
@ -34,7 +34,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- The generated file starts by including `COMPILER_SYS_PATH/src/lib/uce_lib.h`. - The generated file starts by including `COMPILER_SYS_PATH/src/lib/uce_lib.h`.
- It then inlines the configured setup template from `SETUP_TEMPLATE` (by default `scripts/setup.h.template`), which defines the internal hook `__uce_set_current_request(Request*)`. - It then inlines the configured setup template from `SETUP_TEMPLATE` (by default `scripts/setup.h.template`), which defines the internal hook `__uce_set_current_request(Request*)`.
- It inserts `#line 1` before page code so compiler diagnostics point back to the original `.uce` file. - It inserts `#line 1` before page code so compiler diagnostics point back to the original `.uce` file.
- Each literal region is rewritten into one or more `print(R"( ... )");` calls. - Each literal region is rewritten into one or more `print(R"...( ... )...");` calls using a safe raw-string delimiter selected for that literal content.
- `<>` and `?>` both switch from code mode into literal output. - `<>` and `?>` both switch from code mode into literal output.
- `</>` and `<?` both switch from literal output back into code mode. - `</>` and `<?` both switch from literal output back into code mode.
- `<? ... ?>` temporarily breaks out of literal printing, emits the enclosed C++ unchanged, then resumes literal output. - `<? ... ?>` temporarily breaks out of literal printing, emits the enclosed C++ unchanged, then resumes literal output.
@ -46,6 +46,8 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- Lines beginning with `COMPONENT:NAME(...)` are rewritten into exported `__uce_component_NAME(...)` functions for the component helpers. - 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"`. - 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 ...`. - `scripts/compile` then compiles that generated `.cpp` into `source_file + ".so"` with `clang++ -shared -std=c++20 ...`.
- When a worker loads the compiled unit into memory, the runtime checks for `INIT(Request& context)` and calls it once for that worker-side load.
- On each request, the first time a given unit is entered through `RENDER()` or any `COMPONENT...` handler, the runtime checks for `ONCE(Request& context)` and calls it before the render/component handler.
## Generated Files ## Generated Files
@ -113,15 +115,32 @@ RENDER(Request& context)
The loaded file is resolved relative to the current source file unless the path is already absolute. The loaded file is resolved relative to the current source file unless the path is already absolute.
One-time worker initialization plus request-local setup:
```cpp
INIT(Request& context)
{
// load worker-local data, warm caches, or initialize globals for this unit
}
ONCE(Request& context)
{
// prepare request-local state before the first render/component call
context.call["page_title"] = "Demo";
}
```
## Rules ## Rules
- Literal mode can start on either `<>` or `?>`. - Literal mode can start on either `<>` or `?>`.
- Literal mode can end on either `</>` or `<?`. - Literal mode can end on either `</>` or `<?`.
- Literal delimiters are interchangeable; the parser now treats them as one shared code-vs-literal state machine rather than as separate nested block types. - Literal delimiters are interchangeable; the parser treats them as one shared code-vs-literal state machine rather than as separate nested block types.
- `#load` is recognized only when the current line starts with `#load ` at column 1. - `#load` is recognized only when the current line starts with `#load ` at column 1.
- `EXPORT` harvesting only triggers when the current line starts with `EXPORT` at column 1 and is followed by whitespace. - `EXPORT` harvesting only triggers when the current line starts with `EXPORT` at column 1 and is followed by whitespace.
- Relative `#load` paths are expanded against the including unit's source directory. - Relative `#load` paths are expanded against the including unit's source directory.
- `unit_render()` and `unit_call()` are runtime APIs. `#load` is a compile-time composition feature. - `unit_render()` and `unit_call()` are runtime APIs. `#load` is a compile-time composition feature.
- `INIT()` runs when the shared object is loaded into a worker during a request-triggered load, so it still receives a valid `Request& context`.
- `ONCE()` is tracked per request and per resolved unit file. A file entered multiple times in one request only runs `ONCE()` once.
## Limitations ## Limitations
@ -129,7 +148,7 @@ The loaded file is resolved relative to the current source file unless the path
- Outside literal blocks it tracks C++ quotes and comments while deciding whether `<>` or `?>` should open literal mode. - Outside literal blocks it tracks C++ quotes and comments while deciding whether `<>` or `?>` should open literal mode.
- It does not understand comments, raw string literals, templates, or general C++ token structure. - It does not understand comments, raw string literals, templates, or general C++ token structure.
- Inside literal blocks it tracks quotes and comments while scanning `<? ... ?>`, `<?= ... ?>`, and `<?: ... ?>` islands so quoted `?>` text does not close those islands early. - Inside literal blocks it tracks quotes and comments while scanning `<? ... ?>`, `<?= ... ?>`, and `<?: ... ?>` islands so quoted `?>` text does not close those islands early.
- Because literal output is emitted as a C++ raw string literal `R"( ... )"`, literal content must not contain the exact terminator sequence `)"` or the generated C++ will break. - Literal output is emitted through C++ string literals generated by the preprocessor. The preprocessor chooses a raw-string delimiter that does not occur in the literal content, so literal text may safely contain the ordinary raw-string terminator sequence `)"`.
- `#load` depends on the target unit's generated `.cpp` existing and being compilable. If the target cannot be preprocessed or compiled correctly, the including file will fail to compile as well. - `#load` depends on the target unit's generated `.cpp` existing and being compilable. If the target cannot be preprocessed or compiled correctly, the including file will fail to compile as well.
## Debugging ## Debugging

View File

@ -3,6 +3,10 @@ String
:see :see
>types >types
>string
split_utf8
substr
html_escape
:content :content
Primary string type used throughout UCE. Primary string type used throughout UCE.

View File

@ -0,0 +1,25 @@
:sig
StringList
:see
>types
String
split
split_space
split_utf8
join
regex_split
:content
Sequential container of `String` values.
`StringList` is an alias for `std::vector<String>`.
It is returned by split-style helpers such as `split()`, `split_space()`, `split_utf8()`, and `regex_split()`.
Use `join()` when you want to turn a `StringList` back into a single `String`.
Related:
- PHP: indexed arrays of strings
- JavaScript / Node.js: arrays of strings

View File

@ -3,6 +3,10 @@ StringMap
:see :see
>types >types
0_Request
parse_query
encode_query
array_merge
:content :content
Associative container mapping `String` keys to `String` values. Associative container mapping `String` keys to `String` values.

View File

@ -9,6 +9,9 @@ return value : merged result
:see :see
>types >types
0_DTree
StringMap
json_decode
:content :content
Merges two maps or trees using PHP-like merge behavior. Merges two maps or trees using PHP-like merge behavior.

View File

@ -3,6 +3,9 @@ String component(String name, [DTree props], [Request& context])
:see :see
>ob >ob
>component_render
>1_COMPONENT
>1_RENDER
:content :content
Renders another `.uce` file as a component and returns the captured output as a `String`. Renders another `.uce` file as a component and returns the captured output as a `String`.
@ -21,13 +24,17 @@ 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. 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.
When a component unit defines `ONCE(Request& context)`, the runtime calls that hook once per request, per resolved component file, before the first `COMPONENT()` or `COMPONENT:NAME()` handler from that file runs.
## Resolution Order ## Resolution Order
- exact file name - exact file name
- exact file name with `.uce` - exact file name with `.uce`
- the same two forms under `components/` - the same two forms under `components/`
## Example ## Common Patterns
Default component handler:
```cpp ```cpp
DTree props; DTree props;
@ -36,6 +43,66 @@ props["title"] = "Status";
<><?: component("workspace/panel", props, context) ?></> <><?: component("workspace/panel", props, context) ?></>
``` ```
Named component handler:
```cpp
DTree props;
props["title"] = "System";
props["body"] = "Healthy";
print(component("components/card:BODY", props, context));
```
Self-targeted named handler from inside the same file:
```cpp
COMPONENT(Request& context)
{
<>
<section class="card">
<?: component(":BODY", context.props, context) ?>
</section>
</>
}
COMPONENT:BODY(Request& context)
{
<>
<p><?= context.props["body"] ?></p>
</>
}
```
Preparing props in C++ before rendering:
```cpp
DTree props;
props["items"][0] = "alpha";
props["items"][1] = "beta";
props["items"][2] = "gamma";
String html = component("components/list", props, context);
print(html);
```
Embedding returned component markup inside a literal block:
```cpp
<>
<div class="panel">
<?: component("components/card", props, context) ?>
</div>
</>
```
Because `<?= ... ?>` escapes HTML, use `<?: ... ?>` when inserting the returned markup from `component()`.
## Lifecycle Notes
- `INIT(Request& context)` runs once when the worker loads that unit into memory.
- `ONCE(Request& context)` runs once per request before the first component or render entrypoint from that file.
- `component()` then calls either `COMPONENT(Request& context)` or the selected `COMPONENT:NAME(Request& context)` handler.
## Related Concepts ## Related Concepts
- PHP: reusable template partials or helper-rendered view fragments returned as strings - PHP: reusable template partials or helper-rendered view fragments returned as strings

View File

@ -3,6 +3,10 @@ bool component_exists(String name)
:see :see
>ob >ob
component
component_render
component_resolve
1_COMPONENT
:content :content
Checks whether a component file can be resolved from the current page context. Checks whether a component file can be resolved from the current page context.

View File

@ -3,6 +3,10 @@ void component_render(String name, [DTree props], [Request& context])
:see :see
>ob >ob
component
component_exists
component_resolve
1_COMPONENT
:content :content
Renders another `.uce` file as a component and writes the result directly to the current output buffer. Renders another `.uce` file as a component and writes the result directly to the current output buffer.
@ -13,6 +17,8 @@ Component props are passed through `context.props`, and `name:COMPONENTFUNC` may
When `name` starts with `:`, the runtime resolves that named handler against the current `.uce` file. When `name` starts with `:`, the runtime resolves that named handler against the current `.uce` file.
If the target file defines `ONCE(Request& context)`, that hook runs once per request before the file's first component or render entrypoint.
Use `component_render()` when you want to write component output directly from C++ code instead of capturing it as a `String`. Use `component_render()` when you want to write component output directly from C++ code instead of capturing it as a `String`.
## Example ## Example

View File

@ -3,6 +3,10 @@ String component_resolve(String name)
:see :see
>ob >ob
component
component_exists
component_render
1_COMPONENT
:content :content
Resolves a component name to the concrete `.uce` file path that will be loaded. Resolves a component name to the concrete `.uce` file path that will be loaded.

View File

@ -5,6 +5,11 @@ f64 float_val(String s)
s : string to be converted s : string to be converted
return value : a f64 containing the number (0 if no number could be identified). return value : a f64 containing the number (0 if no number could be identified).
:see
>string
int_val
String
:content :content
Extracts a floating point number from a `String`. Extracts a floating point number from a `String`.

View File

@ -7,9 +7,10 @@ delim : optional path separator
return value : the resolved child node, or an empty `DTree` when the path cannot be followed return value : the resolved child node, or an empty `DTree` when the path cannot be followed
:see :see
DTree 0_DTree
0_context 0_Request
>types >types
json_decode
:content :content
Traverses a nested `DTree` without creating missing keys. Traverses a nested `DTree` without creating missing keys.

View File

@ -5,6 +5,12 @@ String html_escape(String s)
s : string to be escaped s : string to be escaped
return value : an HTML-safe escaped version of 's' return value : an HTML-safe escaped version of 's'
:see
>string
json_encode
print
component
:content :content
Returns a version of the input string where special HTML characters are replaced by entities: Returns a version of the input string where special HTML characters are replaced by entities:

View File

@ -6,6 +6,11 @@ s : string to be converted
base : number system base (default 10) base : number system base (default 10)
return value : a u64 containing the number (0 if no number could be identified). return value : a u64 containing the number (0 if no number could be identified).
:see
>string
float_val
String
:content :content
Extracts an integer value from `s`. Extracts an integer value from `s`.

View File

@ -6,7 +6,7 @@ s : string containing JSON data
return value : a DTree object containing the deserialized JSON data return value : a DTree object containing the deserialized JSON data
:see :see
DTree 0_DTree
json_encode json_encode
to_bool to_bool
to_f64 to_f64

View File

@ -7,6 +7,13 @@ s : string to encode as a JSON string literal
t : DTree object to be serialized t : DTree object to be serialized
return value : string containing the JSON result return value : string containing the JSON result
:see
>types
json_decode
0_DTree
String
html_escape
:content :content
Serializes either a `String` or a `DTree` into JSON notation. Serializes either a `String` or a `DTree` into JSON notation.

View File

@ -12,7 +12,7 @@ markdown_to_html
component component
component_render component_render
json_encode json_encode
DTree 0_DTree
:content :content
Parses Markdown source into a structured `DTree` document tree. Parses Markdown source into a structured `DTree` document tree.

View File

@ -7,7 +7,7 @@ code : optional HTTP redirect status, defaults to `302`
:see :see
set_status set_status
0_context 0_Request
:content :content
Sets the `Location` response header and updates the current HTTP status code. Sets the `Location` response header and updates the current HTTP status code.

View File

@ -0,0 +1,43 @@
:sig
bool regex_match(String pattern, String subject)
bool regex_match(String pattern, String subject, String flags)
:params
pattern : PCRE2 regular expression pattern
subject : string to test
flags : optional regex flags
return value : `true` when the entire subject matches the pattern
:see
>regex
regex_search
regex_search_all
regex_replace
regex_split
String
:content
Tests whether `subject` matches `pattern` from start to end.
This is a full-string match, not a substring search. Use `regex_search()` when you want to find the first occurrence anywhere in a string.
Examples:
```uce
regex_match("[A-Z][a-z]+", "Alice"); // true
regex_match("[A-Z][a-z]+", "Alice!"); // false
regex_match("\\d{4}-\\d{2}-\\d{2}", "2026-04-29");
```
Supported flags:
- `i` enables case-insensitive matching.
- `m` enables multiline `^` and `$`.
- `s` lets `.` match newlines.
- `x` enables extended / whitespace-insensitive pattern syntax.
- `u` explicitly enables UTF-8 and Unicode character properties.
- `a` disables UTF-8 / Unicode property mode for ASCII-oriented matching.
UCE uses PCRE2 in UTF-8 + Unicode-property mode by default, so patterns such as `\\p{L}+` work naturally on Unicode text.
Invalid patterns or invalid flags raise a request-visible runtime error with the PCRE2 diagnostic message.

View File

@ -0,0 +1,37 @@
:sig
String regex_replace(String pattern, String replacement, String subject)
String regex_replace(String pattern, String replacement, String subject, String flags)
:params
pattern : PCRE2 regular expression pattern
replacement : replacement string
subject : string where replacements should happen
flags : optional regex flags
return value : a new string with all matches replaced
:see
>regex
regex_match
regex_search
regex_search_all
regex_split
replace
:content
Replaces every match of `pattern` in `subject` and returns the transformed string.
Example:
```uce
String html = regex_replace(
"@([A-Za-z0-9_]+)",
"<a href=\"/users/$1\">@$1</a>",
"Hello @alice and @bob"
);
```
Replacement strings use PCRE2 substitution syntax, including numbered capture references such as `$1` and named references such as `${name}`.
For simple literal search-and-replace, use `replace()`. Use `regex_replace()` when the match condition needs a pattern, captures, character classes, anchors, or flags.
Invalid patterns, invalid flags, or invalid substitution syntax raise a request-visible runtime error.

View File

@ -0,0 +1,59 @@
:sig
DTree regex_search(String pattern, String subject)
DTree regex_search(String pattern, String subject, String flags)
:params
pattern : PCRE2 regular expression pattern
subject : string to search
flags : optional regex flags
return value : a DTree describing the first match
:see
>regex
regex_match
regex_search_all
regex_replace
regex_split
0_DTree
:content
Searches `subject` for the first occurrence of `pattern` and returns structured match data.
Example:
```uce
DTree match = regex_search(
"(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)",
"Contact ops@example.test"
);
if(match["matched"].to_bool())
{
print(match["match"].to_string());
print(match["named"]["user"].to_string());
print(match["named"]["host"].to_string());
}
```
Return shape:
- `matched` is a boolean.
- `pattern` stores the original pattern.
- `flags` stores the supplied flags, or `default`.
- `match` is the full matched text when a match exists.
- `start` and `end` are byte offsets into the original string.
- `captures` is a list of capture objects, with capture `0` representing the full match.
- `named` maps named capture groups to their captured text.
- `named_offsets` maps named capture groups to `index`, `start`, and `end` metadata.
Capture entries contain:
```text
capture["index"]
capture["matched"]
capture["start"]
capture["end"]
capture["text"]
```
If no match is found, `matched` is `false` and match-specific fields are omitted.

View File

@ -0,0 +1,39 @@
:sig
DTree regex_search_all(String pattern, String subject)
DTree regex_search_all(String pattern, String subject, String flags)
:params
pattern : PCRE2 regular expression pattern
subject : string to search
flags : optional regex flags
return value : a DTree containing all non-overlapping matches
:see
>regex
regex_match
regex_search
regex_replace
regex_split
0_DTree
:content
Finds every non-overlapping match of `pattern` in `subject`.
Example:
```uce
DTree tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", "Ship #uce and #docs");
tags["matches"].each([](DTree match, String key) {
print(match["named"]["tag"].to_string(), "\n");
});
```
Return shape:
- `matched` is `true` when at least one match exists.
- `count` is the number of matches.
- `matches` is a list of entries with the same shape returned by `regex_search()`.
- `pattern` and `flags` mirror the call inputs.
Zero-length matches are handled safely; the scanner advances after each zero-length match to avoid infinite loops.

View File

@ -0,0 +1,38 @@
:sig
StringList regex_split(String pattern, String subject)
StringList regex_split(String pattern, String subject, String flags)
:params
pattern : PCRE2 regular expression pattern used as the separator
subject : string to split
flags : optional regex flags
return value : a list of string parts
:see
>regex
regex_match
regex_search
regex_search_all
regex_replace
split
join
StringList
:content
Splits `subject` wherever `pattern` matches.
Example:
```uce
StringList tags = regex_split("\\s*,\\s*", "uce, components, markdown");
print(join(tags, "\n"));
```
This is the pattern-aware companion to `split()`.
Behavior notes:
- Separators are removed from the returned list.
- Empty fields are preserved.
- If the pattern does not match, the result contains the original subject as a single item.
- Zero-length separators are handled safely to avoid infinite loops.

View File

@ -6,7 +6,7 @@ code : HTTP status code
reason : optional reason phrase override reason : optional reason phrase override
:see :see
0_context 0_Request
>types >types
:content :content

View File

@ -8,6 +8,9 @@ return value : 0 if signal was sent, -1 otherwise
:see :see
>task >task
task
task_pid
task_repeat
:content :content
Wraps the standard POSIX `kill()` function. Wraps the standard POSIX `kill()` function.

View File

@ -0,0 +1,18 @@
:sig
bool DTree::to_bool()
:see
>types
0_DTree
json_decode
to_f64
to_u64
:content
Reads a `DTree` value as a boolean.
String values such as `true`, `yes`, `on`, and `1` read as true. Values such as `false`, `no`, `off`, `0`, and `null` read as false.
Numeric values read as true when non-zero. Empty strings read as false.
Use this when consuming request data, JSON-decoded values, config trees, or component props where the original input may be string-shaped.

17
site/doc/pages/to_f64.txt Normal file
View File

@ -0,0 +1,17 @@
:sig
f64 DTree::to_f64()
:see
>types
0_DTree
json_decode
float_val
to_bool
to_u64
:content
Reads a `DTree` value as a floating-point number.
String values are parsed using the same permissive conversion rules used by the runtime's scalar helpers. Boolean values become `1.0` or `0.0`.
Use this for numeric config values, JSON-decoded fields, component props, and request data that should be treated as a number.

17
site/doc/pages/to_u64.txt Normal file
View File

@ -0,0 +1,17 @@
:sig
u64 DTree::to_u64()
:see
>types
0_DTree
json_decode
int_val
to_bool
to_f64
:content
Reads a `DTree` value as an unsigned integer.
String values are parsed numerically. Boolean values become `1` or `0`. Negative values clamp to `0`.
Use this for counts, identifiers, limits, timestamps, and other non-negative numeric values stored inside a `DTree`.

View File

@ -9,6 +9,11 @@ return value : DTree* returned from function
:see :see
>ob >ob
unit_load
unit_render
unit_info
1_RENDER
1_COMPONENT
:content :content
Calls an exported function inside another UCE file. Calls an exported function inside another UCE file.
@ -17,6 +22,19 @@ Use `unit_call()` when you need structured data exchange between units rather th
The callee must expose an `EXPORT` function whose name matches `function_name`. Arguments are passed through `call_param`, and the return value is a `DTree*` owned by the callee. The callee must expose an `EXPORT` function whose name matches `function_name`. Arguments are passed through `call_param`, and the return value is a `DTree*` owned by the callee.
`unit_call()` also understands the request-bound UCE entrypoint names:
- `RENDER`
- `RENDER:NAME`
- `COMPONENT`
- `COMPONENT:NAME`
- `ONCE`
- `INIT`
When `function_name` matches one of those macro-style entrypoints, `unit_call()` does not look for a plain `EXPORT DTree* ...` function. Instead, it translates the name to the generated C++ symbol, uses the current `Request` context, and passes `call_param` into `context.props`, matching the normal component invocation model.
For `RENDER...` and `COMPONENT...`, the unit's `ONCE(Request& context)` hook is still honored automatically before the selected handler runs.
Example: Example:
```cpp ```cpp
@ -31,6 +49,25 @@ EXPORT DTree* test_func(DTree* call_param)
unit_call("call_file_funcs.uce", "test_func"); unit_call("call_file_funcs.uce", "test_func");
``` ```
Calling a named component handler through `unit_call()`:
```cpp
DTree props;
props["title"] = "Diagnostics";
props["body"] = "Ready";
unit_call("components/card.uce", "COMPONENT:BODY", &props);
```
Calling a page render handler through `unit_call()`:
```cpp
DTree props;
props["section"] = "summary";
unit_call("reports/summary.uce", "RENDER", &props);
```
Related: Related:
- PHP: `include`, `require`, or calling a function from an included module, especially when returning arrays or objects instead of rendering a view - PHP: `include`, `require`, or calling a function from an included module, especially when returning arrays or objects instead of rendering a view

View File

@ -6,8 +6,10 @@ path : optional UCE unit path. If empty, recompiles the current executing unit.
return value : `true` when the unit was compiled and loaded successfully return value : `true` when the unit was compiled and loaded successfully
:see :see
>runtime
unit_info unit_info
units_list units_list
unit_load
:content :content
Triggers a manual recompile of a UCE compilation unit. Triggers a manual recompile of a UCE compilation unit.

View File

@ -6,9 +6,11 @@ path : optional UCE unit path. If empty, uses the current executing unit.
return value : metadata tree for the resolved unit, or an empty tree if the unit cannot be resolved return value : metadata tree for the resolved unit, or an empty tree if the unit cannot be resolved
:see :see
>runtime
units_list units_list
unit_compile unit_compile
0_context 0_Request
unit_load
:content :content
Returns runtime metadata for a UCE compilation unit. Returns runtime metadata for a UCE compilation unit.

View File

@ -6,9 +6,11 @@ file_name : UCE file to load
return value : loaded shared unit, or `null` if the unit could not be loaded return value : loaded shared unit, or `null` if the unit could not be loaded
:see :see
>runtime
unit_render unit_render
unit_call unit_call
load load
unit_info
:content :content
Loads a UCE compilation unit and returns its in-memory `SharedUnit` record. Loads a UCE compilation unit and returns its in-memory `SharedUnit` record.

View File

@ -8,6 +8,10 @@ context : optional request context to pass into the target page
:see :see
>ob >ob
unit_call
unit_load
1_RENDER
component
:content :content
Calls another UCE file and executes its `RENDER(Request& context)` function. Calls another UCE file and executes its `RENDER(Request& context)` function.

View File

@ -2,8 +2,10 @@
std::vector<String> units_list() std::vector<String> units_list()
:see :see
>runtime
unit_info unit_info
unit_compile unit_compile
unit_load
:content :content
Returns the normalized paths of all known `.uce` units. Returns the normalized paths of all known `.uce` units.

View File

@ -7,6 +7,13 @@ String var_dump(DTree t, String prefix = "", String postfix = "\n")
t : object to be dumped into a string t : object to be dumped into a string
return value : string containing a human-friendly representation of 't' return value : string containing a human-friendly representation of 't'
:see
>types
0_DTree
StringMap
json_encode
print
:content :content
Returns a string representation of `t` intended for debugging. Returns a string representation of `t` intended for debugging.

View File

@ -1,108 +1,7 @@
struct DocPage { #include "lib/doc_page.h"
String title;
String content;
StringList sig_lines;
StringList param_lines;
StringList see_lines;
};
StringMap* already_shown_items; StringMap* already_shown_items;
String doc_default_title(String page)
{
String page_title = page;
if(page_title.length() > 1 && page_title[1] == '_')
nibble(page_title, "_");
return(page_title);
}
String doc_markdown_inline(String text)
{
text = trim(text);
if(text == "")
return("");
String html = markdown_to_html(text);
if(html.length() >= 7 && html.substr(0, 3) == "<p>" && html.substr(html.length() - 4) == "</p>")
return(html.substr(3, html.length() - 7));
return(html);
}
String doc_legacy_heading(String section)
{
if(section == "desc")
return("");
if(section == "related")
return("## PHP & JS Equivalents");
return("## " + section);
}
DocPage load_doc_page(String page)
{
DocPage result;
StringList lines = split(file_get_contents("pages/" + page + ".txt"), "\n");
String current_section = "";
bool content_mode = false;
StringList content_lines;
for(auto line : lines)
{
if(!content_mode && line != "" && line.substr(0, 1) == ":")
{
String section = trim(line.substr(1));
if(section == "title" || section == "sig" || section == "params" || section == "see")
{
current_section = section;
continue;
}
if(section == "content")
{
content_mode = true;
current_section = "content";
continue;
}
current_section = "legacy";
String heading = doc_legacy_heading(section);
if(heading != "")
{
if(content_lines.size() > 0 && content_lines.back() != "")
content_lines.push_back("");
content_lines.push_back(heading);
content_lines.push_back("");
}
continue;
}
if(current_section == "title")
{
if(result.title != "")
result.title += "\n";
result.title += line;
}
else if(current_section == "sig")
{
result.sig_lines.push_back(line);
}
else if(current_section == "params")
{
result.param_lines.push_back(line);
}
else if(current_section == "see")
{
if(trim(line) != "")
result.see_lines.push_back(trim(line));
}
else
{
content_lines.push_back(line);
}
}
result.content = join(content_lines, "\n");
result.title = trim(result.title);
return(result);
}
void render_doc_params(StringList param_lines) void render_doc_params(StringList param_lines)
{ {
if(param_lines.size() == 0) if(param_lines.size() == 0)

View File

@ -5,7 +5,7 @@ COMPONENT(Request& context)
starter_boot(context); starter_boot(context);
String current_theme = context.cfg.get_by_path("theme/key").to_string(); 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 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(); String route_path = context.call["starter"]["route"]["l_path"].to_string();
<> <>
<div id="theme-switcher" style="position: fixed; right: 1.5rem; bottom: 1.5rem; z-index: 9999; font-family: inherit;"> <div id="theme-switcher" style="position: fixed; right: 1.5rem; bottom: 1.5rem; z-index: 9999; font-family: inherit;">

View File

@ -3,7 +3,7 @@
COMPONENT(Request& context) COMPONENT(Request& context)
{ {
starter_boot(context); starter_boot(context);
String title = first(context.var["starter"]["page_title"].to_string(), context.cfg.get_by_path("site/default_page_title").to_string()); String title = first(context.call["starter"]["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 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 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 = starter_asset_url(context.cfg.get_by_path("theme/path").to_string() + "icon.png", context);

View File

@ -4,13 +4,13 @@ COMPONENT(Request& context)
{ {
starter_boot(context); starter_boot(context);
if(context.props["main_html"].to_string() != "") if(context.props["main_html"].to_string() != "")
context.var["starter"]["fragments"]["main"] = context.props["main_html"]; context.call["starter"]["fragments"]["main"] = context.props["main_html"];
if(context.props["json"].get_type_name() == "array") if(context.props["json"].get_type_name() == "array")
context.var["starter"]["json"] = context.props["json"]; context.call["starter"]["json"] = context.props["json"];
if(context.props["page_type"].to_string() != "") if(context.props["page_type"].to_string() != "")
context.var["starter"]["page_type"] = context.props["page_type"]; context.call["starter"]["page_type"] = context.props["page_type"];
if(context.props["embed_mode"].to_string() != "") if(context.props["embed_mode"].to_string() != "")
context.var["starter"]["embed_mode"] = context.props["embed_mode"]; context.call["starter"]["embed_mode"] = context.props["embed_mode"];
starter_render_page(context); starter_render_page(context);
} }

View File

@ -10,7 +10,7 @@ RENDER(Request& context)
if(resolved["file"].to_string() != "") if(resolved["file"].to_string() != "")
{ {
if(resolved["param"].to_string() != "") if(resolved["param"].to_string() != "")
context.var["app"]["route"]["param"] = resolved["param"]; context.call["app"]["route"]["param"] = resolved["param"];
unit_render(resolved["file"].to_string(), context); unit_render(resolved["file"].to_string(), context);
} }
else else
@ -19,11 +19,11 @@ RENDER(Request& context)
<> <>
<section class="card"> <section class="card">
<h1>404 Not Found</h1> <h1>404 Not Found</h1>
<p><?= context.var["app"]["error"].to_string() ?></p> <p><?= context.call["app"]["error"].to_string() ?></p>
</section> </section>
</> </>
} }
String main_html = ob_get_close(); String main_html = ob_get_close();
context.var["app"]["fragments"]["main"] = main_html; context.call["app"]["fragments"]["main"] = main_html;
app_render_page(context); app_render_page(context);
} }

View File

@ -3,7 +3,7 @@
String app_fs_root(Request& context) String app_fs_root(Request& context)
{ {
String root = context.var["app"]["fs_root"].to_string(); String root = context.call["app"]["fs_root"].to_string();
if(root == "") if(root == "")
root = cwd_get(); root = cwd_get();
return(root); return(root);
@ -11,7 +11,7 @@ String app_fs_root(Request& context)
String app_script_url(Request& context) String app_script_url(Request& context)
{ {
String url = context.var["app"]["script_url"].to_string(); String url = context.call["app"]["script_url"].to_string();
if(url == "") if(url == "")
url = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]); url = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
return(url); return(url);
@ -19,7 +19,7 @@ String app_script_url(Request& context)
String app_base_url(Request& context) String app_base_url(Request& context)
{ {
String base = context.var["app"]["base_url"].to_string(); String base = context.call["app"]["base_url"].to_string();
if(base == "") if(base == "")
{ {
base = dirname(app_script_url(context)); base = dirname(app_script_url(context));
@ -74,7 +74,7 @@ DTree app_make_route(Request& context)
DTree app_resolve_view(Request& context, String base_dir = "views") DTree app_resolve_view(Request& context, String base_dir = "views")
{ {
DTree result; DTree result;
DTree route = context.var["app"]["route"]; DTree route = context.call["app"]["route"];
String lpath = first(route["l_path"].to_string(), "index"); String lpath = first(route["l_path"].to_string(), "index");
String base = trim(base_dir); String base = trim(base_dir);
if(base != "" && base[base.length() - 1] == '/') if(base != "" && base[base.length() - 1] == '/')
@ -114,12 +114,12 @@ DTree app_resolve_view(Request& context, String base_dir = "views")
void app_register_css(String path, Request& context) void app_register_css(String path, Request& context)
{ {
context.var["app"]["assets"]["css"][path] = path; context.call["app"]["assets"]["css"][path] = path;
} }
void app_register_js(String path, Request& context) void app_register_js(String path, Request& context)
{ {
context.var["app"]["assets"]["js"][path] = path; context.call["app"]["assets"]["js"][path] = path;
} }
String app_asset_url(String path, Request& context) String app_asset_url(String path, Request& context)
@ -133,7 +133,7 @@ String app_asset_url(String path, Request& context)
void app_render_registered_css(Request& context) void app_render_registered_css(Request& context)
{ {
context.var["app"]["assets"]["css"].each([&](DTree item, String key) { context.call["app"]["assets"]["css"].each([&](DTree item, String key) {
String path = item.to_string(); String path = item.to_string();
if(path != "") if(path != "")
{ {
@ -144,7 +144,7 @@ void app_render_registered_css(Request& context)
void app_render_registered_js(Request& context) void app_render_registered_js(Request& context)
{ {
context.var["app"]["assets"]["js"].each([&](DTree item, String key) { context.call["app"]["assets"]["js"].each([&](DTree item, String key) {
String path = item.to_string(); String path = item.to_string();
if(path != "") if(path != "")
{ {
@ -185,8 +185,8 @@ void app_redirect(String path, Request& context)
void app_not_found(String message, Request& context) void app_not_found(String message, Request& context)
{ {
context.set_status(404, "Not Found"); context.set_status(404, "Not Found");
context.var["app"]["error"] = message; context.call["app"]["error"] = message;
context.var["app"]["page_title"] = "404 Not Found"; context.call["app"]["page_title"] = "404 Not Found";
} }
String app_menu_href(String menu_key, DTree menu_item, Request& context) String app_menu_href(String menu_key, DTree menu_item, Request& context)
@ -208,7 +208,7 @@ String app_page_main_html(Request& context)
{ {
String main_html = context.props["main_html"].to_string(); String main_html = context.props["main_html"].to_string();
if(main_html == "") if(main_html == "")
main_html = context.var["app"]["fragments"]["main"].to_string(); main_html = context.call["app"]["fragments"]["main"].to_string();
return(main_html); return(main_html);
} }
@ -231,7 +231,7 @@ bool app_bool_value(DTree value, bool fallback = false)
bool app_request_embed_mode(Request& context) bool app_request_embed_mode(Request& context)
{ {
return(app_bool_value(context.var["app"]["embed_mode"])); return(app_bool_value(context.call["app"]["embed_mode"]));
} }
bool app_page_embed_mode(Request& context) bool app_page_embed_mode(Request& context)
@ -244,7 +244,7 @@ bool app_page_embed_mode(Request& context)
String app_theme_page_component(Request& context) String app_theme_page_component(Request& context)
{ {
String page_type = first(context.var["app"]["page_type"].to_string(), "html"); String page_type = first(context.call["app"]["page_type"].to_string(), "html");
if(page_type == "blank") if(page_type == "blank")
return("themes/common/page.blank.uce"); return("themes/common/page.blank.uce");
if(page_type == "json") if(page_type == "json")
@ -264,9 +264,9 @@ void app_render_page(Request& context)
return; return;
DTree page_props; DTree page_props;
page_props["main_html"] = context.var["app"]["fragments"]["main"]; page_props["main_html"] = context.call["app"]["fragments"]["main"];
if(context.var["app"]["json"].get_type_name() == "array") if(context.call["app"]["json"].get_type_name() == "array")
page_props["json"] = context.var["app"]["json"]; page_props["json"] = context.call["app"]["json"];
String page_component = app_theme_page_component(context); String page_component = app_theme_page_component(context);
if(page_component != "" && file_exists(page_component)) if(page_component != "" && file_exists(page_component))
@ -370,19 +370,19 @@ DTree starter_resolve_view(Request& context, String base_dir = "views")
void app_init(Request& context) void app_init(Request& context)
{ {
if(context.var["app"]["booted"].to_string() == "1") if(context.call["app"]["booted"].to_string() == "1")
return; return;
context.var["app"]["booted"] = "1"; context.call["app"]["booted"] = "1";
context.var["app"]["fs_root"] = cwd_get(); context.call["app"]["fs_root"] = cwd_get();
context.var["app"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]); context.call["app"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
String base_url = dirname(context.var["app"]["script_url"].to_string()); String base_url = dirname(context.call["app"]["script_url"].to_string());
if(base_url == "") if(base_url == "")
base_url = "/"; base_url = "/";
if(base_url[base_url.length() - 1] != '/') if(base_url[base_url.length() - 1] != '/')
base_url.append(1, '/'); base_url.append(1, '/');
context.var["app"]["base_url"] = base_url; context.call["app"]["base_url"] = base_url;
DTree config = get_config(); DTree config = get_config();
String requested_theme = first(context.get["theme"], context.cookies["app_theme"], config["theme"]["key"].to_string()); String requested_theme = first(context.get["theme"], context.cookies["app_theme"], config["theme"]["key"].to_string());
@ -394,12 +394,12 @@ void app_init(Request& context)
}); });
config["theme"]["key"] = requested_theme; config["theme"]["key"] = requested_theme;
context.cfg = config; context.cfg = config;
context.var["cfg"].set_reference(&context.cfg); context.call["cfg"].set_reference(&context.cfg);
context.var["app"]["config"].set_reference(&context.cfg); context.call["app"]["config"].set_reference(&context.cfg);
context.var["app"]["route"] = app_make_route(context); context.call["app"]["route"] = app_make_route(context);
context.var["app"]["page_type"] = "html"; context.call["app"]["page_type"] = "html";
context.var["app"]["page_title"] = first(config["menu"][context.var["app"]["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home"); 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.var["app"]["embed_mode"].set_bool(context.get["embed"] != ""); context.call["app"]["embed_mode"].set_bool(context.get["embed"] != "");
if(context.get["theme"] != "" && context.get["theme"] == requested_theme) if(context.get["theme"] != "" && context.get["theme"] == requested_theme)
{ {

View File

@ -148,7 +148,7 @@ struct AppUser
session_start(); session_start();
context.session[session_key()] = email; context.session[session_key()] = email;
context.var["app"]["current_user"] = user; context.call["app"]["current_user"] = user;
result["result"].set_bool(true); result["result"].set_bool(true);
result["profile"] = user; result["profile"] = user;
return(result); return(result);
@ -156,7 +156,7 @@ struct AppUser
bool is_signed_in() bool is_signed_in()
{ {
if(context.var["app"]["current_user"]["email"].to_string() != "") if(context.call["app"]["current_user"]["email"].to_string() != "")
return(true); return(true);
String user_id = context.session[session_key()]; String user_id = context.session[session_key()];
if(user_id == "") if(user_id == "")
@ -167,20 +167,20 @@ struct AppUser
context.session.erase(session_key()); context.session.erase(session_key());
return(false); return(false);
} }
context.var["app"]["current_user"] = user; context.call["app"]["current_user"] = user;
return(true); return(true);
} }
DTree current() DTree current()
{ {
if(is_signed_in()) if(is_signed_in())
return(context.var["app"]["current_user"]); return(context.call["app"]["current_user"]);
return(DTree()); return(DTree());
} }
void logout() void logout()
{ {
context.session.erase(session_key()); context.session.erase(session_key());
context.var["app"]["current_user"].clear(); context.call["app"]["current_user"].clear();
} }
}; };

View File

@ -7,8 +7,8 @@ COMPONENT(Request& context)
context.header["Cache-Control"] = "no-cache, no-store, must-revalidate"; context.header["Cache-Control"] = "no-cache, no-store, must-revalidate";
if(context.props["json"].get_type_name() == "array") if(context.props["json"].get_type_name() == "array")
print(json_encode(context.props["json"])); print(json_encode(context.props["json"]));
else if(context.var["starter"]["json"].get_type_name() == "array") else if(context.call["starter"]["json"].get_type_name() == "array")
print(json_encode(context.var["starter"]["json"])); print(json_encode(context.call["starter"]["json"]));
else else
print(starter_page_main_html(context)); print(starter_page_main_html(context));
} }

View File

@ -5,7 +5,7 @@ COMPONENT(Request& context)
starter_boot(context); starter_boot(context);
bool embed_mode = starter_page_embed_mode(context); bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context); String main_html = starter_page_main_html(context);
String current_path = context.var["starter"]["route"]["l_path"].to_string(); String current_path = context.call["starter"]["route"]["l_path"].to_string();
DTree global_props; DTree global_props;
global_props["cookie_consent"].set_bool(false); global_props["cookie_consent"].set_bool(false);

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "Login"; context.call["starter"]["page_title"] = "Login";
StarterUser users(context); StarterUser users(context);
DTree result; DTree result;
if(context.params["REQUEST_METHOD"] == "POST") if(context.params["REQUEST_METHOD"] == "POST")

View File

@ -9,7 +9,7 @@ RENDER(Request& context)
starter_redirect("account/login", context); starter_redirect("account/login", context);
return; return;
} }
context.var["starter"]["page_title"] = "Profile"; context.call["starter"]["page_title"] = "Profile";
DTree user = users.current(); DTree user = users.current();
String roles = ""; String roles = "";
user["roles"].each([&](DTree role, String key) { user["roles"].each([&](DTree role, String key) {

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "Register"; context.call["starter"]["page_title"] = "Register";
StarterUser users(context); StarterUser users(context);
DTree result; DTree result;
if(context.params["REQUEST_METHOD"] == "POST") if(context.params["REQUEST_METHOD"] == "POST")

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "OAuth Callback"; context.call["starter"]["page_title"] = "OAuth Callback";
String code = context.get["code"]; String code = context.get["code"];
String state = context.get["state"]; String state = context.get["state"];
String error = context.get["error"]; String error = context.get["error"];

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "Auth"; context.call["starter"]["page_title"] = "Auth";
starter_register_css("views/marketing.css", context); starter_register_css("views/marketing.css", context);
DTree props; DTree props;

View File

@ -3,19 +3,19 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_type"] = "json"; context.call["starter"]["page_type"] = "json";
DTree body = json_decode(context.in); DTree body = json_decode(context.in);
if(context.params["REQUEST_METHOD"] == "POST" && body["oauth_service"].to_string() != "" && body["oauth_state"].to_string() != "") 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_service"] = body["oauth_service"].to_string();
context.session["oauth_state"] = body["oauth_state"].to_string(); context.session["oauth_state"] = body["oauth_state"].to_string();
context.var["starter"]["json"]["status"] = "success"; context.call["starter"]["json"]["status"] = "success";
} }
else else
{ {
context.set_status(400, "Bad Request"); context.set_status(400, "Bad Request");
context.var["starter"]["json"]["status"] = "error"; context.call["starter"]["json"]["status"] = "error";
context.var["starter"]["json"]["message"] = "Invalid input"; context.call["starter"]["json"]["message"] = "Invalid input";
} }
} }

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "Dashboard"; context.call["starter"]["page_title"] = "Dashboard";
starter_register_css("views/dashboard.css", context); starter_register_css("views/dashboard.css", context);
DTree props; DTree props;

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "Features"; context.call["starter"]["page_title"] = "Features";
starter_register_css("views/marketing.css", context); starter_register_css("views/marketing.css", context);
<> <>
<h1>Features</h1> <h1>Features</h1>

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "Gauges"; context.call["starter"]["page_title"] = "Gauges";
starter_register_css("themes/common/css/gauges.css", context); starter_register_css("themes/common/css/gauges.css", context);
f64 pi = 3.14159265358979323846; f64 pi = 3.14159265358979323846;
@ -69,7 +69,7 @@ RENDER(Request& context)
props.clear(); props.clear();
props["id"] = "needle_demo"; props["id"] = "needle_demo";
props["title"] = "Needle Gauge"; props["title"] = "Needle Gauge";
props["subtitle"] = "The original analog gauge now uses the same elevated panels, typography, and theme-token palette as the arc gauges."; props["subtitle"] = "The original analog gauge uses the same elevated panels, typography, and theme-token palette as the arc gauges.";
props["style"] = "flex:1 1 24rem"; props["style"] = "flex:1 1 24rem";
props["listen"].set_bool(true); props["listen"].set_bool(true);
props["label"] = "CPU"; props["label"] = "CPU";

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "Home"; context.call["starter"]["page_title"] = "Home";
starter_register_css("views/marketing.css", context); starter_register_css("views/marketing.css", context);
DTree props; DTree props;

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "Components"; context.call["starter"]["page_title"] = "Components";
starter_register_css("views/marketing.css", context); starter_register_css("views/marketing.css", context);
<> <>

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_type"] = "blank"; context.call["starter"]["page_type"] = "blank";
<> <>
<?= std::to_string((u64)time()) ?> - Page 2 Section 1 loaded <?= std::to_string((u64)time()) ?> - Page 2 Section 1 loaded
<pre>UCE starter AJAX fragment response</pre> <pre>UCE starter AJAX fragment response</pre>

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "Ajaxy"; context.call["starter"]["page_title"] = "Ajaxy";
<> <>
<h1>Ajax Demo</h1> <h1>Ajax Demo</h1>
<div id="page2-section1"> <div id="page2-section1">

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "Theme Preview"; context.call["starter"]["page_title"] = "Theme Preview";
starter_register_css("views/themes.css", context); starter_register_css("views/themes.css", context);
String current_theme_key = context.cfg.get_by_path("theme/key").to_string(); 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_label = first(context.cfg.get_by_path("theme/label").to_string(), current_theme_key);

View File

@ -3,7 +3,7 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "Themes"; context.call["starter"]["page_title"] = "Themes";
starter_register_css("views/themes.css", context); starter_register_css("views/themes.css", context);
String current_theme = context.cfg.get_by_path("theme/key").to_string(); String current_theme = context.cfg.get_by_path("theme/key").to_string();

View File

@ -3,8 +3,8 @@
RENDER(Request& context) RENDER(Request& context)
{ {
starter_boot(context); starter_boot(context);
context.var["starter"]["page_title"] = "Workspace"; context.call["starter"]["page_title"] = "Workspace";
String section = first(context.var["starter"]["route"]["param"].to_string(), "overview"); String section = first(context.call["starter"]["route"]["param"].to_string(), "overview");
if(section != "overview" && section != "projects" && section != "activity") if(section != "overview" && section != "projects" && section != "activity")
section = "overview"; section = "overview";

4
site/index.uce Normal file
View File

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

View File

@ -25,6 +25,19 @@ RENDER(Request& context)
check("trim()", trim(" padded value ") == "padded value", trim(" padded value ")); check("trim()", trim(" padded value ") == "padded value", trim(" padded value "));
check("replace()", replace("alpha-beta-beta", "beta", "done") == "alpha-done-done", replace("alpha-beta-beta", "beta", "done")); check("replace()", replace("alpha-beta-beta", "beta", "done") == "alpha-done-done", replace("alpha-beta-beta", "beta", "done"));
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");
check("regex_search()", regex_email["matched"].to_bool() && regex_email["named"]["user"].to_string() == "ops" && regex_email["named"]["host"].to_string() == "example.test", json_encode(regex_email));
DTree regex_tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", "#uce #docs");
check("regex_search_all()", regex_tags["count"].to_s64() == 2 && regex_tags["matches"]["1"]["named"]["tag"].to_string() == "docs", json_encode(regex_tags));
check("regex_replace()", regex_replace("#([A-Za-z0-9_]+)", "<tag>$1</tag>", "#uce") == "<tag>uce</tag>", regex_replace("#([A-Za-z0-9_]+)", "<tag>$1</tag>", "#uce"));
auto regex_parts = regex_split("\\s*,\\s*", "uce, components, markdown");
check("regex_split()", regex_parts.size() == 3 && regex_parts[1] == "components", join(regex_parts, " | "));
check("substr() + strpos()", strpos("component suite", "suite") == 10 && substr("component suite", 10) == "suite", "strpos=10 substr='" + substr("component suite", 10) + "'"); check("substr() + strpos()", strpos("component suite", "suite") == 10 && substr("component suite", 10) == "suite", "strpos=10 substr='" + substr("component suite", 10) + "'");
check("str_starts_with()", str_starts_with("websocket-suite", "websocket"), "websocket-suite starts with websocket"); 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("str_ends_with()", str_ends_with("component.uce", ".uce"), "component.uce ends with .uce");
@ -50,4 +63,4 @@ RENDER(Request& context)
site_tests_summary(passed, failed, skipped, "These assertions intentionally stay pure and side-effect free so they remain safe on the public site."); site_tests_summary(passed, failed, skipped, "These assertions intentionally stay pure and side-effect free so they remain safe on the public site.");
site_tests_page_end(); site_tests_page_end();
} }

19
site/tests/index.uce Normal file
View File

@ -0,0 +1,19 @@
#include "testlib.h"
RENDER(Request& context)
{
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("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");
?></div><?
site_tests_page_end();
}

View File

@ -0,0 +1,42 @@
#include "testlib.h"
String preprocessor_nested_literal()
{
ob_start();
<>
<span id="nested-raw-string-terminator">nested )" marker</span>
</>
return(ob_get_close());
}
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++;
};
String nested = preprocessor_nested_literal();
site_tests_page_start("Preprocessor", "Regression coverage for literal output rewriting and parser edge cases.");
?>
<section class="tests-section">
<p id="top-level-raw-string-terminator">top-level )" marker</p>
<?: nested ?>
</section>
<?
check("raw string terminator in nested literal", contains(nested, "nested )\" marker"), nested);
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.");
site_tests_page_end();
}

View File

@ -123,4 +123,4 @@ WS(Request& context)
} }
ws_send_to(ws_connection_id(), json_encode(websocket_suite_event(context, "unknown", nonce))); ws_send_to(ws_connection_id(), json_encode(websocket_suite_event(context, "unknown", nonce)));
} }

View File

@ -20,6 +20,50 @@ bool compiler_code_state_is_neutral(const CompilerCodeState& state)
return(!state.inside_quote && !state.inside_line_comment && !state.inside_block_comment); return(!state.inside_quote && !state.inside_line_comment && !state.inside_block_comment);
} }
String compiler_cpp_raw_string_delimiter(const String& content)
{
StringList candidates = {
"",
"UCE",
"UCE_LITERAL",
"uce_literal_0",
"uce_literal_1"
};
u64 hash = 1469598103934665603ULL;
for(unsigned char c : content)
{
hash ^= c;
hash *= 1099511628211ULL;
}
for(u32 i = 0; i < 64; i += 1)
{
String suffix = to_hex<u64>(hash ^ ((u64)i * 0x9E3779B97F4A7C15ULL), 12);
candidates.push_back("UCE" + suffix);
}
for(auto& delimiter : candidates)
{
String terminator = ")" + delimiter + "\"";
if(content.find(terminator) == String::npos)
return(delimiter);
}
return("");
}
String compiler_cpp_string_literal(const String& content)
{
String delimiter = compiler_cpp_raw_string_delimiter(content);
if(delimiter != "" || content.find(")\"") == String::npos)
return("R\"" + delimiter + "(" + content + ")" + delimiter + "\"");
// This fallback is only reachable for deliberately adversarial content that
// contains every generated raw-string delimiter candidate.
return(json_escape(content));
}
void compiler_code_state_consume(CompilerCodeState& state, String& buffer, const String& content, u32& i) void compiler_code_state_consume(CompilerCodeState& state, String& buffer, const String& content, u32& i)
{ {
char c = content[i]; char c = content[i];
@ -172,16 +216,15 @@ void compiler_append_text_literal_output(String& parsed_content, String& literal
{ {
if(literal_buffer == "") if(literal_buffer == "")
return; return;
parsed_content.append("print(R\"(" + literal_buffer + ")\");"); parsed_content.append("print(" + compiler_cpp_string_literal(literal_buffer) + ");");
literal_buffer.clear(); literal_buffer.clear();
} }
String compiler_process_text_literal(Request* context, SharedUnit* su, String content) String compiler_process_text_literal(Request* context, SharedUnit* su, String content)
{ {
String parsed_content; String parsed_content;
String html_output_start = "print(R\"(";
String html_output_end = ")\");";
String code_buffer = ""; String code_buffer = "";
String literal_buffer = "";
CompilerCodeState code_state; CompilerCodeState code_state;
bool inside_code = false; bool inside_code = false;
bool is_field = false; bool is_field = false;
@ -200,6 +243,7 @@ String compiler_process_text_literal(Request* context, SharedUnit* su, String co
inside_code = true; inside_code = true;
code_buffer = ""; code_buffer = "";
code_state = CompilerCodeState(); code_state = CompilerCodeState();
compiler_append_text_literal_output(parsed_content, literal_buffer);
if(c2 == '=') if(c2 == '=')
{ {
is_field = true; is_field = true;
@ -221,7 +265,7 @@ String compiler_process_text_literal(Request* context, SharedUnit* su, String co
continue; continue;
} }
parsed_content.append(1, c); literal_buffer.append(1, c);
continue; continue;
} }
@ -234,27 +278,23 @@ String compiler_process_text_literal(Request* context, SharedUnit* su, String co
if(escape_field) if(escape_field)
{ {
parsed_content.append( parsed_content.append(
html_output_end +
"print(html_escape( " + "print(html_escape( " +
code_buffer + code_buffer +
" )); " + " )); "
html_output_start
); );
} }
else else
{ {
parsed_content.append( parsed_content.append(
html_output_end +
"print( " + "print( " +
code_buffer + code_buffer +
" ); " + " ); "
html_output_start
); );
} }
} }
else else
{ {
parsed_content.append(html_output_end + code_buffer + html_output_start); parsed_content.append(code_buffer);
} }
continue; continue;
} }
@ -269,7 +309,10 @@ String compiler_process_text_literal(Request* context, SharedUnit* su, String co
compiler_code_state_consume(code_state, code_buffer, content, i); compiler_code_state_consume(code_state, code_buffer, content, i);
} }
return(html_output_start + parsed_content + html_output_end); if(literal_buffer != "")
compiler_append_text_literal_output(parsed_content, literal_buffer);
return(parsed_content);
} }
String compiler_rewrite_named_render_syntax(String content) String compiler_rewrite_named_render_syntax(String content)
@ -444,4 +487,4 @@ String compiler_preprocess_source(Request* context, SharedUnit* su, String conte
{ {
content = compiler_rewrite_named_render_syntax(content); content = compiler_rewrite_named_render_syntax(content);
return(compiler_preprocess_shared_unit_char_wise(context, su, content)); return(compiler_preprocess_shared_unit_char_wise(context, su, content));
} }

View File

@ -13,6 +13,8 @@ const char* UCE_SETUP_SYMBOL = "__uce_set_current_request";
const char* UCE_RENDER_SYMBOL = "__uce_render"; const char* UCE_RENDER_SYMBOL = "__uce_render";
const char* UCE_COMPONENT_SYMBOL = "__uce_component"; const char* UCE_COMPONENT_SYMBOL = "__uce_component";
const char* UCE_WEBSOCKET_SYMBOL = "__uce_websocket"; const char* UCE_WEBSOCKET_SYMBOL = "__uce_websocket";
const char* UCE_ONCE_SYMBOL = "__uce_once";
const char* UCE_INIT_SYMBOL = "__uce_init";
const u64 UCE_UNIT_ABI_VERSION = 1; const u64 UCE_UNIT_ABI_VERSION = 1;
struct SharedUnitFilesystemState struct SharedUnitFilesystemState
@ -48,6 +50,10 @@ struct SharedUnitCompileCheck
bool needs_compile = false; bool needs_compile = false;
}; };
void compiler_unload_failed_shared_unit(SharedUnit* su);
bool compiler_run_unit_init(Request* context, SharedUnit* su, String* error_out = 0);
bool compiler_run_unit_once_if_needed(Request* context, SharedUnit* su, String* error_out = 0);
bool compiler_config_truthy(String raw, bool default_value) bool compiler_config_truthy(String raw, bool default_value)
{ {
raw = to_lower(trim(raw)); raw = to_lower(trim(raw));
@ -67,19 +73,6 @@ bool compiler_jit_compile_on_request_enabled(Request* context)
return(compiler_config_truthy(context->server->config["JIT_COMPILE_ON_REQUEST"], true)); return(compiler_config_truthy(context->server->config["JIT_COMPILE_ON_REQUEST"], true));
} }
u64 compiler_failure_retry_seconds(Request* context)
{
if(!context || !context->server)
return(10);
auto raw = trim(context->server->config["COMPILE_FAILURE_RETRY_SECONDS"]);
if(raw == "")
return(10);
auto value = int_val(raw);
if(value < 0)
return(0);
return((u64)value);
}
bool compiler_is_u64_string(String value) bool compiler_is_u64_string(String value)
{ {
value = trim(value); value = trim(value);
@ -167,16 +160,16 @@ bool compiler_failure_retry_deferred(Request* context, SharedUnit* su, const Sha
{ {
if(!context || !su) if(!context || !su)
return(false); return(false);
auto retry_seconds = compiler_failure_retry_seconds(context);
if(retry_seconds == 0)
return(false);
if(!state.compile_output_exists || state.compile_output_time == 0) if(!state.compile_output_exists || state.compile_output_time == 0)
return(false); return(false);
if(state.source_time == 0) if(state.source_time == 0)
return(false); return(false);
if(state.source_time > state.compile_output_time) auto required_failure_inputs_time = std::max({
return(false); state.source_time,
return(time() < (u64)state.compile_output_time + retry_seconds); state.setup_template_time,
state.compiler_abi_time
});
return(state.compile_output_time >= required_failure_inputs_time);
} }
String compiler_failure_output_for_state(SharedUnit* su, const SharedUnitFilesystemState& state) String compiler_failure_output_for_state(SharedUnit* su, const SharedUnitFilesystemState& state)
@ -653,6 +646,8 @@ void load_shared_unit(Request* context, SharedUnit* su)
su->on_render = 0; su->on_render = 0;
su->on_component = 0; su->on_component = 0;
su->on_websocket = 0; su->on_websocket = 0;
su->on_once = 0;
su->on_init = 0;
su->on_setup = 0; su->on_setup = 0;
su->so_handle = 0; su->so_handle = 0;
su->compiler_messages = ""; su->compiler_messages = "";
@ -695,17 +690,31 @@ void load_shared_unit(Request* context, SharedUnit* su)
dlerror(); dlerror();
su->on_websocket = (request_ref_handler)dlsym(su->so_handle, UCE_WEBSOCKET_SYMBOL); su->on_websocket = (request_ref_handler)dlsym(su->so_handle, UCE_WEBSOCKET_SYMBOL);
dlerror(); dlerror();
su->on_once = (request_ref_handler)dlsym(su->so_handle, UCE_ONCE_SYMBOL);
dlerror();
su->on_init = (request_ref_handler)dlsym(su->so_handle, UCE_INIT_SYMBOL);
dlerror();
su->api_declarations = split(file_get_contents(su->api_file_name), "\n"); su->api_declarations = split(file_get_contents(su->api_file_name), "\n");
String init_error = "";
if(!compiler_run_unit_init(context, su, &init_error))
{
if(init_error != "")
su->compiler_messages = init_error;
compiler_unload_failed_shared_unit(su);
}
//else //else
// printf("(i) loaded unit %s\n", su->file_name.c_str()); // printf("(i) loaded unit %s\n", su->file_name.c_str());
} }
else else
{ {
const char* dl_error = dlerror();
su->compiler_messages = "could not open " + su->so_name; su->compiler_messages = "could not open " + su->so_name;
if(dl_error && String(dl_error) != "")
su->compiler_messages += ": " + String(dl_error);
su->compile_status = "load_error"; su->compile_status = "load_error";
su->compile_error_status = su->compiler_messages; su->compile_error_status = su->compiler_messages;
su->last_error = time(); su->last_error = time();
printf("Error loading unit %s, could not open %s\n", su->file_name.c_str(), su->so_name.c_str()); printf("Error loading unit %s, %s\n", su->file_name.c_str(), su->compiler_messages.c_str());
} }
} }
@ -1176,6 +1185,183 @@ struct UnitInvocationScope
} }
}; };
enum class UnitCallMacroKind
{
none,
render,
component,
once,
init
};
struct UnitCallMacroTarget
{
UnitCallMacroKind kind = UnitCallMacroKind::none;
String handler_name;
};
struct RequestPropsScope
{
Request* context = 0;
DTree previous_props;
RequestPropsScope(Request* context, const DTree& props)
{
this->context = context;
if(this->context)
{
previous_props = this->context->props;
this->context->props = props;
}
}
~RequestPropsScope()
{
if(context)
context->props = previous_props;
}
};
void compiler_unload_failed_shared_unit(SharedUnit* su)
{
if(!su)
return;
if(su->so_handle)
dlclose(su->so_handle);
su->so_handle = 0;
su->api_functions.clear();
su->on_setup = 0;
su->on_render = 0;
su->on_component = 0;
su->on_websocket = 0;
su->on_once = 0;
su->on_init = 0;
}
bool compiler_run_unit_init(Request* context, SharedUnit* su, String* error_out)
{
if(!su || !su->on_init)
return(true);
if(!context)
{
if(error_out)
*error_out = "internal error: INIT() requires a Request context";
return(false);
}
if(!su->on_setup)
{
if(error_out)
*error_out = "internal error: " + String(UCE_SETUP_SYMBOL) + "() not defined in " + su->file_name;
return(false);
}
UnitInvocationScope invoke_scope(context, su);
su->on_setup(context);
try
{
su->on_init(*context);
return(true);
}
catch(...)
{
su->runtime_error_status = "uncaught exception during INIT";
su->compile_status = "load_error";
su->compile_error_status = su->runtime_error_status;
su->last_error = time();
if(error_out)
*error_out = su->runtime_error_status;
return(false);
}
}
bool compiler_run_unit_once_if_needed(Request* context, SharedUnit* su, String* error_out)
{
if(!su || !su->on_once)
return(true);
if(!context)
{
if(error_out)
*error_out = "internal error: ONCE() requires a Request context";
return(false);
}
if(!su->on_setup)
{
if(error_out)
*error_out = "internal error: " + String(UCE_SETUP_SYMBOL) + "() not defined in " + su->file_name;
return(false);
}
if(context->once_units.find(su->file_name) != context->once_units.end())
return(true);
context->once_units.insert(su->file_name);
UnitInvocationScope invoke_scope(context, su);
su->on_setup(context);
try
{
su->on_once(*context);
return(true);
}
catch(...)
{
context->once_units.erase(su->file_name);
su->runtime_error_status = "uncaught exception during ONCE";
su->last_error = time();
if(error_out)
*error_out = su->runtime_error_status;
throw;
}
}
String unit_call_macro_trim(String function_name)
{
function_name = trim(function_name);
if(function_name.length() >= 2 && function_name.substr(function_name.length() - 2) == "()")
function_name = trim(function_name.substr(0, function_name.length() - 2));
return(function_name);
}
UnitCallMacroTarget unit_call_macro_target(String function_name)
{
UnitCallMacroTarget target;
function_name = unit_call_macro_trim(function_name);
if(function_name == "RENDER")
{
target.kind = UnitCallMacroKind::render;
return(target);
}
if(function_name.rfind("RENDER:", 0) == 0)
{
target.kind = UnitCallMacroKind::render;
target.handler_name = trim(function_name.substr(7));
return(target);
}
if(function_name == "COMPONENT")
{
target.kind = UnitCallMacroKind::component;
return(target);
}
if(function_name.rfind("COMPONENT:", 0) == 0)
{
target.kind = UnitCallMacroKind::component;
target.handler_name = trim(function_name.substr(10));
return(target);
}
if(function_name == "ONCE")
{
target.kind = UnitCallMacroKind::once;
return(target);
}
if(function_name == "INIT")
{
target.kind = UnitCallMacroKind::init;
return(target);
}
return(target);
}
} }
String component_normalize_path(String name) String component_normalize_path(String name)
@ -1262,6 +1448,70 @@ String component_handler_symbol(String render_name)
return(String(UCE_COMPONENT_SYMBOL) + "_" + safe_name(render_name)); return(String(UCE_COMPONENT_SYMBOL) + "_" + safe_name(render_name));
} }
String compiler_missing_request_handler_message(UnitCallMacroKind kind, String handler_name)
{
handler_name = trim(handler_name);
if(kind == UnitCallMacroKind::render)
{
if(handler_name == "" || handler_name == "render")
return("no RENDER() entry point");
return("no RENDER:" + handler_name + "() entry point");
}
if(kind == UnitCallMacroKind::component)
{
if(handler_name == "")
return("no COMPONENT() entry point");
return("no COMPONENT:" + handler_name + "() entry point");
}
if(kind == UnitCallMacroKind::once)
return("no ONCE() entry point");
if(kind == UnitCallMacroKind::init)
return("no INIT() entry point");
return("request handler not found");
}
bool compiler_prepare_request_handler(Request* context, SharedUnit* su, String* error_out = 0, bool run_once = false)
{
if(!su->on_setup)
{
if(error_out)
*error_out = "internal error: " + String(UCE_SETUP_SYMBOL) + "() not defined in " + su->file_name;
return(false);
}
if(run_once && !compiler_run_unit_once_if_needed(context, su, error_out))
return(false);
return(true);
}
void compiler_execute_request_handler(
Request* context,
SharedUnit* su,
request_ref_handler handler,
bool count_request,
String runtime_error_status
)
{
UnitInvocationScope invoke_scope(context, su);
su->on_setup(context);
f64 render_start = time_precise();
compiler_begin_render_result(su, count_request);
try
{
handler(*context);
compiler_record_render_result(su, time_precise() - render_start, true);
}
catch(...)
{
compiler_record_render_result(
su,
time_precise() - render_start,
false,
runtime_error_status
);
throw;
}
}
request_ref_handler get_page_render_handler(SharedUnit* su, String render_name) request_ref_handler get_page_render_handler(SharedUnit* su, String render_name)
{ {
String symbol = page_render_handler_symbol(render_name); String symbol = page_render_handler_symbol(render_name);
@ -1300,46 +1550,24 @@ bool compiler_invoke_render(Request* context, String file_name, String render_na
if(!su) if(!su)
return(false); return(false);
if(!su->on_setup) if(!compiler_prepare_request_handler(context, su, error_out, true))
{
if(error_out)
*error_out = "internal error: " + String(UCE_SETUP_SYMBOL) + "() not defined in " + file_name;
return(false); return(false);
}
auto handler = get_page_render_handler(su, render_name); auto handler = get_page_render_handler(su, render_name);
if(!handler) if(!handler)
{ {
if(error_out) if(error_out)
{ *error_out = compiler_missing_request_handler_message(UnitCallMacroKind::render, render_name);
if(trim(render_name) == "" || trim(render_name) == "render")
*error_out = "no RENDER() entry point";
else
*error_out = "no RENDER:" + render_name + "() entry point";
}
return(false); return(false);
} }
UnitInvocationScope invoke_scope(context, su); compiler_execute_request_handler(
su->on_setup(context); context,
f64 render_start = time_precise(); su,
bool count_request = compiler_is_request_entry_unit(context, su); handler,
compiler_begin_render_result(su, count_request); compiler_is_request_entry_unit(context, su),
try "uncaught exception during render"
{ );
handler(*context);
compiler_record_render_result(su, time_precise() - render_start, true);
}
catch(...)
{
compiler_record_render_result(
su,
time_precise() - render_start,
false,
"uncaught exception during render"
);
throw;
}
return(true); return(true);
} }
@ -1349,45 +1577,24 @@ bool compiler_invoke_component(Request* context, String file_name, String render
if(!su) if(!su)
return(false); return(false);
if(!su->on_setup) if(!compiler_prepare_request_handler(context, su, error_out, true))
{
if(error_out)
*error_out = "internal error: " + String(UCE_SETUP_SYMBOL) + "() not defined in " + file_name;
return(false); return(false);
}
auto handler = get_component_handler(su, render_name); auto handler = get_component_handler(su, render_name);
if(!handler) if(!handler)
{ {
if(error_out) if(error_out)
{ *error_out = compiler_missing_request_handler_message(UnitCallMacroKind::component, render_name);
if(trim(render_name) == "")
*error_out = "no COMPONENT() entry point";
else
*error_out = "no COMPONENT:" + render_name + "() entry point";
}
return(false); return(false);
} }
UnitInvocationScope invoke_scope(context, su); compiler_execute_request_handler(
su->on_setup(context); context,
f64 render_start = time_precise(); su,
compiler_begin_render_result(su, false); handler,
try false,
{ "uncaught exception during component render"
handler(*context); );
compiler_record_render_result(su, time_precise() - render_start, true);
}
catch(...)
{
compiler_record_render_result(
su,
time_precise() - render_start,
false,
"uncaught exception during component render"
);
throw;
}
return(true); return(true);
} }
@ -1421,26 +1628,13 @@ void compiler_invoke_websocket(Request* context, String file_name)
return; return;
} }
UnitInvocationScope invoke_scope(context, su); compiler_execute_request_handler(
su->on_setup(context); context,
f64 render_start = time_precise(); su,
bool count_request = compiler_is_request_entry_unit(context, su); su->on_websocket,
compiler_begin_render_result(su, count_request); compiler_is_request_entry_unit(context, su),
try "uncaught exception during websocket handler"
{ );
su->on_websocket(*context);
compiler_record_render_result(su, time_precise() - render_start, true);
}
catch(...)
{
compiler_record_render_result(
su,
time_precise() - render_start,
false,
"uncaught exception during websocket handler"
);
throw;
}
} }
void unit_render(String file_name) void unit_render(String file_name)
@ -1499,14 +1693,11 @@ void component_render(String name, DTree props, Request& context)
return; return;
} }
DTree previous_props = context.props; RequestPropsScope props_scope(&context, props);
context.props = props;
String error_message = ""; String error_message = "";
if(!compiler_invoke_component(&context, resolved_name, render_name, &error_message) && error_message != "") if(!compiler_invoke_component(&context, resolved_name, render_name, &error_message) && error_message != "")
print(component_error_banner(error_message)); print(component_error_banner(error_message));
context.props = previous_props;
} }
String component(String name) String component(String name)
@ -1558,16 +1749,52 @@ DTree* unit_call(String file_name, String function_name, DTree* call_param)
} }
else else
{ {
auto f = (dtree_call_handler)dlsym(su->so_handle, function_name.c_str()); auto macro_target = unit_call_macro_target(function_name);
if(!f) if(macro_target.kind != UnitCallMacroKind::none)
{ {
print("Error: unit_call() function '", function_name, "' not found"); RequestPropsScope props_scope(context, (call_param ? *call_param : DTree()));
String error_message = "";
if(macro_target.kind == UnitCallMacroKind::render)
{
if(!compiler_invoke_render(context, su->file_name, macro_target.handler_name, &error_message) && error_message != "")
print("Error: unit_call() ", error_message);
}
else if(macro_target.kind == UnitCallMacroKind::component)
{
if(!compiler_invoke_component(context, su->file_name, macro_target.handler_name, &error_message) && error_message != "")
print("Error: unit_call() ", error_message);
}
else
{
UnitInvocationScope invoke_scope(context, su);
su->on_setup(context);
request_ref_handler handler = 0;
if(macro_target.kind == UnitCallMacroKind::once)
handler = su->on_once;
else if(macro_target.kind == UnitCallMacroKind::init)
handler = su->on_init;
if(!handler)
print("Error: unit_call() ", compiler_missing_request_handler_message(macro_target.kind, macro_target.handler_name));
else
handler(*context);
}
} }
else else
{ {
UnitInvocationScope invoke_scope(context, su); auto f = (dtree_call_handler)dlsym(su->so_handle, function_name.c_str());
su->on_setup(context); if(!f)
result = f(call_param); {
print("Error: unit_call() function '", function_name, "' not found");
}
else
{
UnitInvocationScope invoke_scope(context, su);
su->on_setup(context);
result = f(call_param);
}
} }
} }
} }

View File

@ -2,6 +2,8 @@
#define RENDER(X) extern "C" void __uce_render(X) #define RENDER(X) extern "C" void __uce_render(X)
#define COMPONENT(X) extern "C" void __uce_component(X) #define COMPONENT(X) extern "C" void __uce_component(X)
#define ONCE(X) extern "C" void __uce_once(X)
#define INIT(X) extern "C" void __uce_init(X)
#define WS(X) extern "C" void __uce_websocket(X) #define WS(X) extern "C" void __uce_websocket(X)
#define EXPORT extern "C" #define EXPORT extern "C"

View File

@ -1,5 +1,9 @@
#include "functionlib.h" #include "functionlib.h"
#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>
#include <stdexcept>
String var_dump(StringMap map, String prefix, String postfix) String var_dump(StringMap map, String prefix, String postfix)
{ {
String result = ""; String result = "";
@ -151,6 +155,347 @@ String replace(String s, String search, String replace_with)
return(result); return(result);
} }
namespace {
String regex_flags_label(String flags)
{
return(flags == "" ? "default" : flags);
}
void regex_throw(String function_name, String message)
{
throw std::runtime_error(function_name + "(): " + message);
}
String regex_pcre2_error(int error_code)
{
PCRE2_UCHAR buffer[256];
pcre2_get_error_message(error_code, buffer, sizeof(buffer));
return(String(reinterpret_cast<char*>(buffer)));
}
uint32_t regex_compile_options(String flags, String function_name)
{
uint32_t options = PCRE2_UTF | PCRE2_UCP;
for(char flag : flags)
{
switch(flag)
{
case('i'):
options |= PCRE2_CASELESS;
break;
case('m'):
options |= PCRE2_MULTILINE;
break;
case('s'):
options |= PCRE2_DOTALL;
break;
case('x'):
options |= PCRE2_EXTENDED;
break;
case('u'):
options |= PCRE2_UTF | PCRE2_UCP;
break;
case('a'):
options &= ~PCRE2_UTF;
options &= ~PCRE2_UCP;
break;
default:
regex_throw(function_name, "unknown regex flag '" + String(1, flag) + "'");
}
}
return(options);
}
struct RegexCode {
pcre2_code* code = 0;
RegexCode(String pattern, String flags, String function_name)
{
int error_code = 0;
PCRE2_SIZE error_offset = 0;
code = pcre2_compile(
reinterpret_cast<PCRE2_SPTR>(pattern.c_str()),
pattern.length(),
regex_compile_options(flags, function_name),
&error_code,
&error_offset,
0
);
if(!code)
regex_throw(function_name, "could not compile pattern at offset " + std::to_string((u64)error_offset) + ": " + regex_pcre2_error(error_code));
pcre2_jit_compile(code, PCRE2_JIT_COMPLETE);
}
~RegexCode()
{
if(code)
pcre2_code_free(code);
}
};
struct RegexMatchData {
pcre2_match_data* data = 0;
RegexMatchData(pcre2_code* code)
{
data = pcre2_match_data_create_from_pattern(code, 0);
if(!data)
regex_throw("regex", "could not allocate match data");
}
~RegexMatchData()
{
if(data)
pcre2_match_data_free(data);
}
};
String regex_subject_slice(String subject, PCRE2_SIZE start, PCRE2_SIZE end)
{
if(start == PCRE2_UNSET || end == PCRE2_UNSET || end < start || start > subject.length())
return("");
if(end > subject.length())
end = subject.length();
return(subject.substr(start, end - start));
}
size_t regex_next_utf8_offset(String subject, size_t offset)
{
if(offset >= subject.length())
return(subject.length() + 1);
unsigned char c = (unsigned char)subject[offset];
size_t step = 1;
if((c & 0x80) == 0)
step = 1;
else if((c & 0xE0) == 0xC0)
step = 2;
else if((c & 0xF0) == 0xE0)
step = 3;
else if((c & 0xF8) == 0xF0)
step = 4;
if(offset + step > subject.length())
step = 1;
return(offset + step);
}
void regex_add_named_captures(DTree& result, pcre2_code* code, String subject, PCRE2_SIZE* ovector, int rc)
{
uint32_t name_count = 0;
uint32_t entry_size = 0;
PCRE2_SPTR name_table = 0;
pcre2_pattern_info(code, PCRE2_INFO_NAMECOUNT, &name_count);
if(name_count == 0)
return;
pcre2_pattern_info(code, PCRE2_INFO_NAMEENTRYSIZE, &entry_size);
pcre2_pattern_info(code, PCRE2_INFO_NAMETABLE, &name_table);
for(uint32_t i = 0; i < name_count; i += 1)
{
PCRE2_SPTR entry = name_table + (i * entry_size);
uint32_t group_index = (entry[0] << 8) | entry[1];
String name(reinterpret_cast<const char*>(entry + 2));
if(group_index >= (uint32_t)rc)
continue;
PCRE2_SIZE start = ovector[group_index * 2];
PCRE2_SIZE end = ovector[group_index * 2 + 1];
if(start == PCRE2_UNSET || end == PCRE2_UNSET)
continue;
result["named"][name] = regex_subject_slice(subject, start, end);
result["named_offsets"][name]["index"] = (f64)group_index;
result["named_offsets"][name]["start"] = (f64)start;
result["named_offsets"][name]["end"] = (f64)end;
}
}
DTree regex_build_match_tree(String pattern, String flags, String subject, pcre2_code* code, pcre2_match_data* match_data, int rc)
{
DTree result;
result["matched"].set_bool(rc >= 0);
result["pattern"] = pattern;
result["flags"] = regex_flags_label(flags);
if(rc < 0)
return(result);
PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(match_data);
result["start"] = (f64)ovector[0];
result["end"] = (f64)ovector[1];
result["match"] = regex_subject_slice(subject, ovector[0], ovector[1]);
for(int i = 0; i < rc; i += 1)
{
DTree capture;
PCRE2_SIZE start = ovector[i * 2];
PCRE2_SIZE end = ovector[i * 2 + 1];
capture["index"] = (f64)i;
capture["matched"].set_bool(start != PCRE2_UNSET && end != PCRE2_UNSET);
if(start != PCRE2_UNSET && end != PCRE2_UNSET)
{
capture["start"] = (f64)start;
capture["end"] = (f64)end;
capture["text"] = regex_subject_slice(subject, start, end);
}
result["captures"].push(capture);
}
regex_add_named_captures(result, code, subject, ovector, rc);
return(result);
}
int regex_match_at(RegexCode& regex, RegexMatchData& match_data, String subject, size_t offset, uint32_t options, String function_name)
{
int rc = pcre2_match(
regex.code,
reinterpret_cast<PCRE2_SPTR>(subject.c_str()),
subject.length(),
offset,
options,
match_data.data,
0
);
if(rc == PCRE2_ERROR_NOMATCH)
return(rc);
if(rc < 0)
regex_throw(function_name, "match failed: " + regex_pcre2_error(rc));
return(rc);
}
}
bool regex_match(String pattern, String subject, String flags)
{
RegexCode regex(pattern, flags, "regex_match");
RegexMatchData match_data(regex.code);
int rc = regex_match_at(regex, match_data, subject, 0, PCRE2_ANCHORED | PCRE2_ENDANCHORED, "regex_match");
return(rc >= 0);
}
DTree regex_search(String pattern, String subject, String flags)
{
RegexCode regex(pattern, flags, "regex_search");
RegexMatchData match_data(regex.code);
int rc = regex_match_at(regex, match_data, subject, 0, 0, "regex_search");
return(regex_build_match_tree(pattern, flags, subject, regex.code, match_data.data, rc));
}
DTree regex_search_all(String pattern, String subject, String flags)
{
RegexCode regex(pattern, flags, "regex_search_all");
RegexMatchData match_data(regex.code);
DTree result;
result["matched"].set_bool(false);
result["pattern"] = pattern;
result["flags"] = regex_flags_label(flags);
size_t offset = 0;
while(offset <= subject.length())
{
int rc = regex_match_at(regex, match_data, subject, offset, 0, "regex_search_all");
if(rc == PCRE2_ERROR_NOMATCH)
break;
DTree match = regex_build_match_tree(pattern, flags, subject, regex.code, match_data.data, rc);
result["matches"].push(match);
result["matched"].set_bool(true);
PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(match_data.data);
size_t start = ovector[0];
size_t end = ovector[1];
if(end > offset)
offset = end;
else
offset = regex_next_utf8_offset(subject, offset);
}
result["count"] = (f64)result["matches"].deref()._map.size();
return(result);
}
String regex_replace(String pattern, String replacement, String subject, String flags)
{
RegexCode regex(pattern, flags, "regex_replace");
PCRE2_SIZE output_length = 0;
uint32_t options = PCRE2_SUBSTITUTE_GLOBAL | PCRE2_SUBSTITUTE_OVERFLOW_LENGTH;
int rc = pcre2_substitute(
regex.code,
reinterpret_cast<PCRE2_SPTR>(subject.c_str()),
subject.length(),
0,
options,
0,
0,
reinterpret_cast<PCRE2_SPTR>(replacement.c_str()),
replacement.length(),
0,
&output_length
);
if(rc != PCRE2_ERROR_NOMEMORY && rc < 0)
regex_throw("regex_replace", "substitution failed: " + regex_pcre2_error(rc));
String output;
output.resize(output_length);
rc = pcre2_substitute(
regex.code,
reinterpret_cast<PCRE2_SPTR>(subject.c_str()),
subject.length(),
0,
PCRE2_SUBSTITUTE_GLOBAL,
0,
0,
reinterpret_cast<PCRE2_SPTR>(replacement.c_str()),
replacement.length(),
reinterpret_cast<PCRE2_UCHAR*>(&output[0]),
&output_length
);
if(rc < 0)
regex_throw("regex_replace", "substitution failed: " + regex_pcre2_error(rc));
output.resize(output_length);
return(output);
}
StringList regex_split(String pattern, String subject, String flags)
{
RegexCode regex(pattern, flags, "regex_split");
RegexMatchData match_data(regex.code);
StringList result;
size_t offset = 0;
size_t last_end = 0;
while(offset <= subject.length())
{
int rc = regex_match_at(regex, match_data, subject, offset, 0, "regex_split");
if(rc == PCRE2_ERROR_NOMATCH)
break;
PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(match_data.data);
size_t start = ovector[0];
size_t end = ovector[1];
result.push_back(subject.substr(last_end, start - last_end));
last_end = end;
if(end > offset)
offset = end;
else
offset = regex_next_utf8_offset(subject, offset);
}
result.push_back(subject.substr(last_end));
return(result);
}
String trim(String raw) String trim(String raw)
{ {
s64 len = raw.length(); s64 len = raw.length();

View File

@ -14,6 +14,11 @@ bool str_starts_with(String haystack, String needle);
bool str_ends_with(String haystack, String needle); bool str_ends_with(String haystack, String needle);
bool contains(String haystack, String needle); bool contains(String haystack, String needle);
String replace(String s, String search, String replace_with); String replace(String s, String search, String replace_with);
bool regex_match(String pattern, String subject, String flags = "");
DTree regex_search(String pattern, String subject, String flags = "");
DTree regex_search_all(String pattern, String subject, String flags = "");
String regex_replace(String pattern, String replacement, String subject, String flags = "");
StringList regex_split(String pattern, String subject, String flags = "");
String trim(String raw); String trim(String raw);
StringList split_space(String str); StringList split_space(String str);

View File

@ -7,6 +7,7 @@
#include <functional> #include <functional>
#include <sstream> #include <sstream>
#include <atomic> #include <atomic>
#include <set>
typedef unsigned char u8; typedef unsigned char u8;
typedef signed char s8; typedef signed char s8;
@ -84,6 +85,8 @@ struct SharedUnit {
request_ref_handler on_render = 0; request_ref_handler on_render = 0;
request_ref_handler on_component = 0; request_ref_handler on_component = 0;
request_ref_handler on_websocket = 0; request_ref_handler on_websocket = 0;
request_ref_handler on_once = 0;
request_ref_handler on_init = 0;
String compiler_messages; String compiler_messages;
String compile_status = "unknown"; String compile_status = "unknown";
@ -156,8 +159,9 @@ struct Request {
StringMap cookies; StringMap cookies;
StringMap session; StringMap session;
String session_loaded_hash = ""; String session_loaded_hash = "";
std::set<String> once_units;
//DTree var; DTree call;
DTree cfg; DTree cfg;
DTree props; DTree props;
DTree connection; DTree connection;

View File

@ -702,6 +702,13 @@ String ws_connection_id()
return(context->resources.websocket_connection_id); return(context->resources.websocket_connection_id);
} }
String ws_message()
{
if(!context)
return("");
return(context->in);
}
String ws_scope() String ws_scope()
{ {
return(current_ws_scope()); return(current_ws_scope());

View File

@ -3,6 +3,7 @@ def register(registry):
("doc index", "/doc/index.uce", "<html>"), ("doc index", "/doc/index.uce", "<html>"),
("doc singlepage", "/doc/singlepage.uce", "<html>"), ("doc singlepage", "/doc/singlepage.uce", "<html>"),
("doc component page", "/doc/index.uce?p=component", "component()"), ("doc component page", "/doc/index.uce?p=component", "component()"),
("doc regex page", "/doc/index.uce?p=regex_search", "regex_search"),
("doc relative time page", "/doc/index.uce?p=time_format_relative", "time_format_relative"), ("doc relative time page", "/doc/index.uce?p=time_format_relative", "time_format_relative"),
] ]
@ -15,4 +16,4 @@ def register(registry):
return run return run
registry.case(name, make_case(), tags=["http", "smoke", "uce", "public"]) registry.case(name, make_case(), tags=["http", "smoke", "uce", "public"])

View File

@ -10,6 +10,7 @@ def register(registry):
pages = [ pages = [
("site tests index", "/tests/index.uce", "Coverage Index", ["http", "suite", "uce", "public"]), ("site tests index", "/tests/index.uce", "Coverage Index", ["http", "suite", "uce", "public"]),
("site tests core", "/tests/core.uce", "Core APIs", ["http", "suite", "uce", "public"]), ("site tests core", "/tests/core.uce", "Core APIs", ["http", "suite", "uce", "public"]),
("site tests preprocessor", "/tests/preprocessor.uce", "top-level )\" marker", ["http", "suite", "uce", "public"]),
("site tests http", "/tests/http.uce", "HTTP And Session", ["http", "suite", "uce", "public"]), ("site tests http", "/tests/http.uce", "HTTP And Session", ["http", "suite", "uce", "public"]),
("site tests components", "/tests/components.uce", "Components", ["http", "suite", "uce", "public"]), ("site tests components", "/tests/components.uce", "Components", ["http", "suite", "uce", "public"]),
("site tests markdown", "/tests/markdown.uce", "Markdown", ["http", "suite", "uce", "public"]), ("site tests markdown", "/tests/markdown.uce", "Markdown", ["http", "suite", "uce", "public"]),
@ -35,4 +36,4 @@ def register(registry):
return run return run
registry.case(name, make_case(), tags=tags) registry.case(name, make_case(), tags=tags)