working on documentation and more API functions
This commit is contained in:
parent
cd445f3c9b
commit
9f7625c7fd
31
README.md
31
README.md
@ -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)`
|
||||
- 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
|
||||
- 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
|
||||
- the preprocessor has two jobs:
|
||||
- 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++`
|
||||
- `mysql_config`
|
||||
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
|
||||
- standard Linux development headers for `dl`, `pthread`, sockets, and backtrace support
|
||||
|
||||
The binary is written to:
|
||||
@ -52,7 +53,7 @@ bin/uce_fastcgi.linux.bin
|
||||
|
||||
## 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
|
||||
- `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.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::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.
|
||||
|
||||
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
|
||||
|
||||
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 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.
|
||||
|
||||
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
|
||||
|
||||
@ -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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
@ -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.
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
@ -232,7 +240,7 @@ On a Debian or Ubuntu host, start with the packages needed to build and run UCE
|
||||
|
||||
```bash
|
||||
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:
|
||||
@ -240,6 +248,7 @@ The exact package names may vary by distro. The important requirements are:
|
||||
- `nginx`
|
||||
- `clang++`
|
||||
- `mysql_config`
|
||||
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
|
||||
- normal Linux development headers for threads, sockets, `dl`, and backtrace support
|
||||
|
||||
### 2. Put the repo on the server
|
||||
@ -484,7 +493,7 @@ Common failure modes:
|
||||
- WebSocket upgrade fails
|
||||
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
|
||||
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
|
||||
Tighten the server root and add explicit deny rules for non-public directories.
|
||||
|
||||
|
||||
@ -20,8 +20,8 @@ JIT_COMPILE_ON_REQUEST=1
|
||||
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
|
||||
PROACTIVE_COMPILE_ENABLED=1
|
||||
|
||||
# AFTER A FAILED COMPILE, SERVE THE LAST COMPILER OUTPUT AND WAIT THIS MANY SECONDS
|
||||
# BEFORE TRYING AGAIN IF THE SOURCE FILE HAS NOT CHANGED
|
||||
# AFTER A FAILED COMPILE, UCE NOW REUSES THE PERSISTED COMPILER OUTPUT UNTIL
|
||||
# THE SOURCE OR COMPILER INPUTS CHANGE. THIS SETTING IS KEPT FOR COMPATIBILITY.
|
||||
COMPILE_FAILURE_RETRY_SECONDS=10
|
||||
|
||||
# PERIODIC KNOWN-.uce RECHECK INTERVAL IN SECONDS
|
||||
|
||||
@ -14,7 +14,7 @@ mkdir work > /dev/null 2>&1
|
||||
COMPILER="clang++"
|
||||
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\""
|
||||
|
||||
echo "Compliling executable..."
|
||||
|
||||
@ -38,6 +38,7 @@ RENDER(Request& context)
|
||||
<? render_card("dtree.uce", "DTree", "Dynamic hierarchical data tree"); ?>
|
||||
<? 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("regex.uce", "Regular Expressions", "PCRE2 matching, captures, replacement, and splitting"); ?>
|
||||
<? render_card("string.uce", "String", "String operations"); ?>
|
||||
<? render_card("str_replace.uce", "String Replace", "Search and replace in strings"); ?>
|
||||
<? render_card("utf8.uce", "UTF-8", "Unicode string handling"); ?>
|
||||
@ -61,6 +62,7 @@ RENDER(Request& context)
|
||||
<div class="grid-heading">Advanced</div>
|
||||
<? render_card("call_file.uce", "unit_call()", "Dynamic file inclusion"); ?>
|
||||
<? 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("script.uce", "Script", "UCE script integration"); ?>
|
||||
<? render_card("websockets.ws.uce", "WebSockets", "Real-time WebSocket chat"); ?>
|
||||
|
||||
53
site/demo/once-init.uce
Normal file
53
site/demo/once-init.uce
Normal 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></>
|
||||
}
|
||||
@ -11,6 +11,7 @@ RENDER(Request& context)
|
||||
</h1>
|
||||
|
||||
<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.
|
||||
@ -36,4 +37,4 @@ RENDER(Request& context)
|
||||
<pre><?= var_dump(context.params) ?></pre>
|
||||
</details>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
74
site/demo/regex.uce
Normal file
74
site/demo/regex.uce
Normal 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
7
site/doc/areas/regex.txt
Normal file
@ -0,0 +1,7 @@
|
||||
Regular Expressions
|
||||
|
||||
regex_match
|
||||
regex_search
|
||||
regex_search_all
|
||||
regex_replace
|
||||
regex_split
|
||||
@ -17,6 +17,11 @@ split
|
||||
split_space
|
||||
split_utf8
|
||||
replace
|
||||
regex_match
|
||||
regex_search
|
||||
regex_search_all
|
||||
regex_replace
|
||||
regex_split
|
||||
to_lower
|
||||
to_upper
|
||||
trim
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
Task API
|
||||
|
||||
kill
|
||||
task
|
||||
task_repeat
|
||||
task_pid
|
||||
task_kill
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
Types
|
||||
|
||||
0_Request
|
||||
array_merge
|
||||
DTree
|
||||
0_DTree
|
||||
get_by_path
|
||||
set_status
|
||||
String
|
||||
StringList
|
||||
StringMap
|
||||
to_bool
|
||||
to_f64
|
||||
to_u64
|
||||
|
||||
@ -1,104 +1,25 @@
|
||||
struct DocPage {
|
||||
String title;
|
||||
String content;
|
||||
StringList sig_lines;
|
||||
StringList param_lines;
|
||||
StringList see_lines;
|
||||
};
|
||||
#include "lib/doc_page.h"
|
||||
|
||||
String doc_default_title(String page)
|
||||
void render_doc_page_link(String page, String label = "", String badge = "")
|
||||
{
|
||||
String page_title = page;
|
||||
if(page_title.length() > 1 && page_title[1] == '_')
|
||||
nibble(page_title, "_");
|
||||
return(page_title);
|
||||
}
|
||||
page = trim(page);
|
||||
if(page == "")
|
||||
return;
|
||||
if(label == "")
|
||||
label = doc_index_label(page);
|
||||
if(badge == "")
|
||||
badge = doc_page_kind_badge(doc_page_kind(page));
|
||||
|
||||
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)
|
||||
?><a href="index.uce?p=<?= uri_encode(page) ?>"><?= label ?><?
|
||||
if(badge != "")
|
||||
{
|
||||
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);
|
||||
}
|
||||
?><span class="badge"><?= badge ?></span><?
|
||||
}
|
||||
|
||||
result.content = join(content_lines, "\n");
|
||||
result.title = trim(result.title);
|
||||
return(result);
|
||||
else
|
||||
{
|
||||
?><span class="dim">()</span><?
|
||||
}
|
||||
?></a><?
|
||||
}
|
||||
|
||||
void render_doc_params(StringList param_lines)
|
||||
@ -130,13 +51,19 @@ void render_see_section(String name)
|
||||
s32 idx = 0;
|
||||
for(auto line : lines)
|
||||
{
|
||||
line = trim(line);
|
||||
if(line == "")
|
||||
{
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
if(idx == 0)
|
||||
{
|
||||
<><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;
|
||||
}
|
||||
@ -154,11 +81,19 @@ void render_doc_see_links(StringList see_lines)
|
||||
{
|
||||
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
|
||||
{
|
||||
?><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/"))
|
||||
{
|
||||
String ft = nibble(file_name, ".");
|
||||
if(ft.substr(0, 2) == "0_")
|
||||
{
|
||||
String fn = ft;
|
||||
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>
|
||||
<?
|
||||
}
|
||||
String label = doc_index_label(ft);
|
||||
String badge = doc_page_kind_badge(doc_page_kind(ft));
|
||||
?><div class="func-item"><? render_doc_page_link(ft, label, badge); ?></div><?
|
||||
}
|
||||
?></div>
|
||||
</main>
|
||||
|
||||
153
site/doc/lib/doc_page.h
Normal file
153
site/doc/lib/doc_page.h
Normal 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);
|
||||
}
|
||||
@ -6,6 +6,11 @@ Request& context;
|
||||
|
||||
:see
|
||||
>types
|
||||
set_status
|
||||
component
|
||||
unit_render
|
||||
ws_message
|
||||
session_start
|
||||
|
||||
: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`.
|
||||
|
||||
@ -8,6 +8,8 @@ COMPONENT(Request& context)
|
||||
>component
|
||||
>component_render
|
||||
>1_RENDER
|
||||
>1_INIT
|
||||
>1_ONCE
|
||||
>1_WS
|
||||
|
||||
: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)`.
|
||||
|
||||
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
|
||||
|
||||
This keeps page rendering and component rendering separate:
|
||||
|
||||
50
site/doc/pages/1_INIT.txt
Normal file
50
site/doc/pages/1_INIT.txt
Normal 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
49
site/doc/pages/1_ONCE.txt
Normal 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
|
||||
@ -6,6 +6,10 @@ RENDER(Request& context)
|
||||
|
||||
:see
|
||||
>ob
|
||||
>1_COMPONENT
|
||||
>1_INIT
|
||||
>1_ONCE
|
||||
>1_WS
|
||||
|
||||
:content
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
If the page is invoked from another UCE file via `unit_render(file_name, context)`, the callee receives that same `context`.
|
||||
|
||||
@ -6,6 +6,10 @@ WS(Request& context)
|
||||
|
||||
:see
|
||||
>websocket
|
||||
>1_COMPONENT
|
||||
>1_INIT
|
||||
>1_ONCE
|
||||
>1_RENDER
|
||||
|
||||
:content
|
||||
Defines the WebSocket message handler for the current `.ws.uce` page.
|
||||
|
||||
@ -8,7 +8,7 @@ UCE source preprocessing
|
||||
load
|
||||
unit_render
|
||||
unit_call
|
||||
0_context
|
||||
0_Request
|
||||
1_COMPONENT
|
||||
|
||||
: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(expression);` without HTML escaping.
|
||||
- `#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.
|
||||
- `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`.
|
||||
- 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.
|
||||
- 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 literal output back into code mode.
|
||||
- `<? ... ?>` 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.
|
||||
- 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 ...`.
|
||||
- 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
|
||||
|
||||
@ -113,15 +115,32 @@ RENDER(Request& context)
|
||||
|
||||
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
|
||||
|
||||
- Literal mode can start 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.
|
||||
- `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.
|
||||
- `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
|
||||
|
||||
@ -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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Debugging
|
||||
|
||||
@ -3,6 +3,10 @@ String
|
||||
|
||||
:see
|
||||
>types
|
||||
>string
|
||||
split_utf8
|
||||
substr
|
||||
html_escape
|
||||
|
||||
:content
|
||||
Primary string type used throughout UCE.
|
||||
|
||||
25
site/doc/pages/StringList.txt
Normal file
25
site/doc/pages/StringList.txt
Normal 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
|
||||
@ -3,6 +3,10 @@ StringMap
|
||||
|
||||
:see
|
||||
>types
|
||||
0_Request
|
||||
parse_query
|
||||
encode_query
|
||||
array_merge
|
||||
|
||||
:content
|
||||
Associative container mapping `String` keys to `String` values.
|
||||
|
||||
@ -9,6 +9,9 @@ return value : merged result
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
StringMap
|
||||
json_decode
|
||||
|
||||
:content
|
||||
Merges two maps or trees using PHP-like merge behavior.
|
||||
|
||||
@ -3,6 +3,9 @@ String component(String name, [DTree props], [Request& context])
|
||||
|
||||
:see
|
||||
>ob
|
||||
>component_render
|
||||
>1_COMPONENT
|
||||
>1_RENDER
|
||||
|
||||
:content
|
||||
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 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
|
||||
|
||||
- exact file name
|
||||
- exact file name with `.uce`
|
||||
- the same two forms under `components/`
|
||||
|
||||
## Example
|
||||
## Common Patterns
|
||||
|
||||
Default component handler:
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
@ -36,6 +43,66 @@ props["title"] = "Status";
|
||||
<><?: 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
|
||||
|
||||
- PHP: reusable template partials or helper-rendered view fragments returned as strings
|
||||
|
||||
@ -3,6 +3,10 @@ bool component_exists(String name)
|
||||
|
||||
:see
|
||||
>ob
|
||||
component
|
||||
component_render
|
||||
component_resolve
|
||||
1_COMPONENT
|
||||
|
||||
:content
|
||||
Checks whether a component file can be resolved from the current page context.
|
||||
|
||||
@ -3,6 +3,10 @@ void component_render(String name, [DTree props], [Request& context])
|
||||
|
||||
:see
|
||||
>ob
|
||||
component
|
||||
component_exists
|
||||
component_resolve
|
||||
1_COMPONENT
|
||||
|
||||
:content
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
## Example
|
||||
|
||||
@ -3,6 +3,10 @@ String component_resolve(String name)
|
||||
|
||||
:see
|
||||
>ob
|
||||
component
|
||||
component_exists
|
||||
component_render
|
||||
1_COMPONENT
|
||||
|
||||
:content
|
||||
Resolves a component name to the concrete `.uce` file path that will be loaded.
|
||||
|
||||
@ -5,6 +5,11 @@ f64 float_val(String s)
|
||||
s : string to be converted
|
||||
return value : a f64 containing the number (0 if no number could be identified).
|
||||
|
||||
:see
|
||||
>string
|
||||
int_val
|
||||
String
|
||||
|
||||
:content
|
||||
Extracts a floating point number from a `String`.
|
||||
|
||||
|
||||
@ -7,9 +7,10 @@ delim : optional path separator
|
||||
return value : the resolved child node, or an empty `DTree` when the path cannot be followed
|
||||
|
||||
:see
|
||||
DTree
|
||||
0_context
|
||||
0_DTree
|
||||
0_Request
|
||||
>types
|
||||
json_decode
|
||||
|
||||
:content
|
||||
Traverses a nested `DTree` without creating missing keys.
|
||||
|
||||
@ -5,6 +5,12 @@ String html_escape(String s)
|
||||
s : string to be escaped
|
||||
return value : an HTML-safe escaped version of 's'
|
||||
|
||||
:see
|
||||
>string
|
||||
json_encode
|
||||
print
|
||||
component
|
||||
|
||||
:content
|
||||
Returns a version of the input string where special HTML characters are replaced by entities:
|
||||
|
||||
|
||||
@ -6,6 +6,11 @@ s : string to be converted
|
||||
base : number system base (default 10)
|
||||
return value : a u64 containing the number (0 if no number could be identified).
|
||||
|
||||
:see
|
||||
>string
|
||||
float_val
|
||||
String
|
||||
|
||||
:content
|
||||
Extracts an integer value from `s`.
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ s : string containing JSON data
|
||||
return value : a DTree object containing the deserialized JSON data
|
||||
|
||||
:see
|
||||
DTree
|
||||
0_DTree
|
||||
json_encode
|
||||
to_bool
|
||||
to_f64
|
||||
|
||||
@ -7,6 +7,13 @@ s : string to encode as a JSON string literal
|
||||
t : DTree object to be serialized
|
||||
return value : string containing the JSON result
|
||||
|
||||
:see
|
||||
>types
|
||||
json_decode
|
||||
0_DTree
|
||||
String
|
||||
html_escape
|
||||
|
||||
:content
|
||||
Serializes either a `String` or a `DTree` into JSON notation.
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ markdown_to_html
|
||||
component
|
||||
component_render
|
||||
json_encode
|
||||
DTree
|
||||
0_DTree
|
||||
|
||||
:content
|
||||
Parses Markdown source into a structured `DTree` document tree.
|
||||
|
||||
@ -7,7 +7,7 @@ code : optional HTTP redirect status, defaults to `302`
|
||||
|
||||
:see
|
||||
set_status
|
||||
0_context
|
||||
0_Request
|
||||
|
||||
:content
|
||||
Sets the `Location` response header and updates the current HTTP status code.
|
||||
|
||||
43
site/doc/pages/regex_match.txt
Normal file
43
site/doc/pages/regex_match.txt
Normal 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.
|
||||
37
site/doc/pages/regex_replace.txt
Normal file
37
site/doc/pages/regex_replace.txt
Normal 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.
|
||||
59
site/doc/pages/regex_search.txt
Normal file
59
site/doc/pages/regex_search.txt
Normal 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.
|
||||
39
site/doc/pages/regex_search_all.txt
Normal file
39
site/doc/pages/regex_search_all.txt
Normal 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.
|
||||
38
site/doc/pages/regex_split.txt
Normal file
38
site/doc/pages/regex_split.txt
Normal 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.
|
||||
@ -6,7 +6,7 @@ code : HTTP status code
|
||||
reason : optional reason phrase override
|
||||
|
||||
:see
|
||||
0_context
|
||||
0_Request
|
||||
>types
|
||||
|
||||
:content
|
||||
|
||||
@ -8,6 +8,9 @@ return value : 0 if signal was sent, -1 otherwise
|
||||
|
||||
:see
|
||||
>task
|
||||
task
|
||||
task_pid
|
||||
task_repeat
|
||||
|
||||
:content
|
||||
Wraps the standard POSIX `kill()` function.
|
||||
|
||||
18
site/doc/pages/to_bool.txt
Normal file
18
site/doc/pages/to_bool.txt
Normal 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
17
site/doc/pages/to_f64.txt
Normal 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
17
site/doc/pages/to_u64.txt
Normal 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`.
|
||||
@ -9,6 +9,11 @@ return value : DTree* returned from function
|
||||
|
||||
:see
|
||||
>ob
|
||||
unit_load
|
||||
unit_render
|
||||
unit_info
|
||||
1_RENDER
|
||||
1_COMPONENT
|
||||
|
||||
:content
|
||||
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.
|
||||
|
||||
`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:
|
||||
|
||||
```cpp
|
||||
@ -31,6 +49,25 @@ EXPORT DTree* test_func(DTree* call_param)
|
||||
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:
|
||||
|
||||
- PHP: `include`, `require`, or calling a function from an included module, especially when returning arrays or objects instead of rendering a view
|
||||
|
||||
@ -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
|
||||
|
||||
:see
|
||||
>runtime
|
||||
unit_info
|
||||
units_list
|
||||
unit_load
|
||||
|
||||
:content
|
||||
Triggers a manual recompile of a UCE compilation unit.
|
||||
|
||||
@ -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
|
||||
|
||||
:see
|
||||
>runtime
|
||||
units_list
|
||||
unit_compile
|
||||
0_context
|
||||
0_Request
|
||||
unit_load
|
||||
|
||||
:content
|
||||
Returns runtime metadata for a UCE compilation unit.
|
||||
|
||||
@ -6,9 +6,11 @@ file_name : UCE file to load
|
||||
return value : loaded shared unit, or `null` if the unit could not be loaded
|
||||
|
||||
:see
|
||||
>runtime
|
||||
unit_render
|
||||
unit_call
|
||||
load
|
||||
unit_info
|
||||
|
||||
:content
|
||||
Loads a UCE compilation unit and returns its in-memory `SharedUnit` record.
|
||||
|
||||
@ -8,6 +8,10 @@ context : optional request context to pass into the target page
|
||||
|
||||
:see
|
||||
>ob
|
||||
unit_call
|
||||
unit_load
|
||||
1_RENDER
|
||||
component
|
||||
|
||||
:content
|
||||
Calls another UCE file and executes its `RENDER(Request& context)` function.
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
std::vector<String> units_list()
|
||||
|
||||
:see
|
||||
>runtime
|
||||
unit_info
|
||||
unit_compile
|
||||
unit_load
|
||||
|
||||
:content
|
||||
Returns the normalized paths of all known `.uce` units.
|
||||
|
||||
@ -7,6 +7,13 @@ String var_dump(DTree t, String prefix = "", String postfix = "\n")
|
||||
t : object to be dumped into a string
|
||||
return value : string containing a human-friendly representation of 't'
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
StringMap
|
||||
json_encode
|
||||
print
|
||||
|
||||
:content
|
||||
Returns a string representation of `t` intended for debugging.
|
||||
|
||||
|
||||
@ -1,108 +1,7 @@
|
||||
struct DocPage {
|
||||
String title;
|
||||
String content;
|
||||
StringList sig_lines;
|
||||
StringList param_lines;
|
||||
StringList see_lines;
|
||||
};
|
||||
#include "lib/doc_page.h"
|
||||
|
||||
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)
|
||||
{
|
||||
if(param_lines.size() == 0)
|
||||
|
||||
@ -5,7 +5,7 @@ COMPONENT(Request& context)
|
||||
starter_boot(context);
|
||||
String current_theme = context.cfg.get_by_path("theme/key").to_string();
|
||||
String current_label = first(context.cfg.get_by_path("theme/label").to_string(), current_theme);
|
||||
String route_path = context.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;">
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
COMPONENT(Request& 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 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);
|
||||
|
||||
@ -4,13 +4,13 @@ COMPONENT(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
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")
|
||||
context.var["starter"]["json"] = context.props["json"];
|
||||
context.call["starter"]["json"] = context.props["json"];
|
||||
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() != "")
|
||||
context.var["starter"]["embed_mode"] = context.props["embed_mode"];
|
||||
context.call["starter"]["embed_mode"] = context.props["embed_mode"];
|
||||
starter_render_page(context);
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ RENDER(Request& context)
|
||||
if(resolved["file"].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);
|
||||
}
|
||||
else
|
||||
@ -19,11 +19,11 @@ RENDER(Request& context)
|
||||
<>
|
||||
<section class="card">
|
||||
<h1>404 Not Found</h1>
|
||||
<p><?= context.var["app"]["error"].to_string() ?></p>
|
||||
<p><?= context.call["app"]["error"].to_string() ?></p>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
String main_html = ob_get_close();
|
||||
context.var["app"]["fragments"]["main"] = main_html;
|
||||
context.call["app"]["fragments"]["main"] = main_html;
|
||||
app_render_page(context);
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
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 == "")
|
||||
root = cwd_get();
|
||||
return(root);
|
||||
@ -11,7 +11,7 @@ String app_fs_root(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 == "")
|
||||
url = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
|
||||
return(url);
|
||||
@ -19,7 +19,7 @@ String app_script_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 == "")
|
||||
{
|
||||
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 result;
|
||||
DTree route = context.var["app"]["route"];
|
||||
DTree route = context.call["app"]["route"];
|
||||
String lpath = first(route["l_path"].to_string(), "index");
|
||||
String base = trim(base_dir);
|
||||
if(base != "" && base[base.length() - 1] == '/')
|
||||
@ -114,12 +114,12 @@ DTree app_resolve_view(Request& context, String base_dir = "views")
|
||||
|
||||
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)
|
||||
{
|
||||
context.var["app"]["assets"]["js"][path] = path;
|
||||
context.call["app"]["assets"]["js"][path] = path;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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();
|
||||
if(path != "")
|
||||
{
|
||||
@ -144,7 +144,7 @@ void app_render_registered_css(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();
|
||||
if(path != "")
|
||||
{
|
||||
@ -185,8 +185,8 @@ void app_redirect(String path, Request& context)
|
||||
void app_not_found(String message, Request& context)
|
||||
{
|
||||
context.set_status(404, "Not Found");
|
||||
context.var["app"]["error"] = message;
|
||||
context.var["app"]["page_title"] = "404 Not Found";
|
||||
context.call["app"]["error"] = message;
|
||||
context.call["app"]["page_title"] = "404 Not Found";
|
||||
}
|
||||
|
||||
String app_menu_href(String menu_key, DTree menu_item, Request& context)
|
||||
@ -208,7 +208,7 @@ String app_page_main_html(Request& context)
|
||||
{
|
||||
String main_html = context.props["main_html"].to_string();
|
||||
if(main_html == "")
|
||||
main_html = context.var["app"]["fragments"]["main"].to_string();
|
||||
main_html = context.call["app"]["fragments"]["main"].to_string();
|
||||
return(main_html);
|
||||
}
|
||||
|
||||
@ -231,7 +231,7 @@ bool app_bool_value(DTree value, bool fallback = false)
|
||||
|
||||
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)
|
||||
@ -244,7 +244,7 @@ bool app_page_embed_mode(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")
|
||||
return("themes/common/page.blank.uce");
|
||||
if(page_type == "json")
|
||||
@ -264,9 +264,9 @@ void app_render_page(Request& context)
|
||||
return;
|
||||
|
||||
DTree page_props;
|
||||
page_props["main_html"] = context.var["app"]["fragments"]["main"];
|
||||
if(context.var["app"]["json"].get_type_name() == "array")
|
||||
page_props["json"] = context.var["app"]["json"];
|
||||
page_props["main_html"] = context.call["app"]["fragments"]["main"];
|
||||
if(context.call["app"]["json"].get_type_name() == "array")
|
||||
page_props["json"] = context.call["app"]["json"];
|
||||
|
||||
String page_component = app_theme_page_component(context);
|
||||
if(page_component != "" && file_exists(page_component))
|
||||
@ -370,19 +370,19 @@ DTree starter_resolve_view(Request& context, String base_dir = "views")
|
||||
|
||||
void app_init(Request& context)
|
||||
{
|
||||
if(context.var["app"]["booted"].to_string() == "1")
|
||||
if(context.call["app"]["booted"].to_string() == "1")
|
||||
return;
|
||||
|
||||
context.var["app"]["booted"] = "1";
|
||||
context.var["app"]["fs_root"] = cwd_get();
|
||||
context.var["app"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
|
||||
context.call["app"]["booted"] = "1";
|
||||
context.call["app"]["fs_root"] = cwd_get();
|
||||
context.call["app"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
|
||||
|
||||
String base_url = dirname(context.var["app"]["script_url"].to_string());
|
||||
String base_url = dirname(context.call["app"]["script_url"].to_string());
|
||||
if(base_url == "")
|
||||
base_url = "/";
|
||||
if(base_url[base_url.length() - 1] != '/')
|
||||
base_url.append(1, '/');
|
||||
context.var["app"]["base_url"] = base_url;
|
||||
context.call["app"]["base_url"] = base_url;
|
||||
|
||||
DTree config = get_config();
|
||||
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;
|
||||
context.cfg = config;
|
||||
context.var["cfg"].set_reference(&context.cfg);
|
||||
context.var["app"]["config"].set_reference(&context.cfg);
|
||||
context.var["app"]["route"] = app_make_route(context);
|
||||
context.var["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.var["app"]["embed_mode"].set_bool(context.get["embed"] != "");
|
||||
context.call["cfg"].set_reference(&context.cfg);
|
||||
context.call["app"]["config"].set_reference(&context.cfg);
|
||||
context.call["app"]["route"] = app_make_route(context);
|
||||
context.call["app"]["page_type"] = "html";
|
||||
context.call["app"]["page_title"] = first(config["menu"][context.call["app"]["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
|
||||
context.call["app"]["embed_mode"].set_bool(context.get["embed"] != "");
|
||||
|
||||
if(context.get["theme"] != "" && context.get["theme"] == requested_theme)
|
||||
{
|
||||
|
||||
@ -148,7 +148,7 @@ struct AppUser
|
||||
|
||||
session_start();
|
||||
context.session[session_key()] = email;
|
||||
context.var["app"]["current_user"] = user;
|
||||
context.call["app"]["current_user"] = user;
|
||||
result["result"].set_bool(true);
|
||||
result["profile"] = user;
|
||||
return(result);
|
||||
@ -156,7 +156,7 @@ struct AppUser
|
||||
|
||||
bool is_signed_in()
|
||||
{
|
||||
if(context.var["app"]["current_user"]["email"].to_string() != "")
|
||||
if(context.call["app"]["current_user"]["email"].to_string() != "")
|
||||
return(true);
|
||||
String user_id = context.session[session_key()];
|
||||
if(user_id == "")
|
||||
@ -167,20 +167,20 @@ struct AppUser
|
||||
context.session.erase(session_key());
|
||||
return(false);
|
||||
}
|
||||
context.var["app"]["current_user"] = user;
|
||||
context.call["app"]["current_user"] = user;
|
||||
return(true);
|
||||
}
|
||||
|
||||
DTree current()
|
||||
{
|
||||
if(is_signed_in())
|
||||
return(context.var["app"]["current_user"]);
|
||||
return(context.call["app"]["current_user"]);
|
||||
return(DTree());
|
||||
}
|
||||
|
||||
void logout()
|
||||
{
|
||||
context.session.erase(session_key());
|
||||
context.var["app"]["current_user"].clear();
|
||||
context.call["app"]["current_user"].clear();
|
||||
}
|
||||
};
|
||||
|
||||
@ -7,8 +7,8 @@ COMPONENT(Request& context)
|
||||
context.header["Cache-Control"] = "no-cache, no-store, must-revalidate";
|
||||
if(context.props["json"].get_type_name() == "array")
|
||||
print(json_encode(context.props["json"]));
|
||||
else if(context.var["starter"]["json"].get_type_name() == "array")
|
||||
print(json_encode(context.var["starter"]["json"]));
|
||||
else if(context.call["starter"]["json"].get_type_name() == "array")
|
||||
print(json_encode(context.call["starter"]["json"]));
|
||||
else
|
||||
print(starter_page_main_html(context));
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ COMPONENT(Request& context)
|
||||
starter_boot(context);
|
||||
bool embed_mode = starter_page_embed_mode(context);
|
||||
String main_html = starter_page_main_html(context);
|
||||
String current_path = context.var["starter"]["route"]["l_path"].to_string();
|
||||
String current_path = context.call["starter"]["route"]["l_path"].to_string();
|
||||
|
||||
DTree global_props;
|
||||
global_props["cookie_consent"].set_bool(false);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.var["starter"]["page_title"] = "Login";
|
||||
context.call["starter"]["page_title"] = "Login";
|
||||
StarterUser users(context);
|
||||
DTree result;
|
||||
if(context.params["REQUEST_METHOD"] == "POST")
|
||||
|
||||
@ -9,7 +9,7 @@ RENDER(Request& context)
|
||||
starter_redirect("account/login", context);
|
||||
return;
|
||||
}
|
||||
context.var["starter"]["page_title"] = "Profile";
|
||||
context.call["starter"]["page_title"] = "Profile";
|
||||
DTree user = users.current();
|
||||
String roles = "";
|
||||
user["roles"].each([&](DTree role, String key) {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.var["starter"]["page_title"] = "Register";
|
||||
context.call["starter"]["page_title"] = "Register";
|
||||
StarterUser users(context);
|
||||
DTree result;
|
||||
if(context.params["REQUEST_METHOD"] == "POST")
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.var["starter"]["page_title"] = "OAuth Callback";
|
||||
context.call["starter"]["page_title"] = "OAuth Callback";
|
||||
String code = context.get["code"];
|
||||
String state = context.get["state"];
|
||||
String error = context.get["error"];
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.var["starter"]["page_title"] = "Auth";
|
||||
context.call["starter"]["page_title"] = "Auth";
|
||||
starter_register_css("views/marketing.css", context);
|
||||
|
||||
DTree props;
|
||||
|
||||
@ -3,19 +3,19 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.var["starter"]["page_type"] = "json";
|
||||
context.call["starter"]["page_type"] = "json";
|
||||
|
||||
DTree body = json_decode(context.in);
|
||||
if(context.params["REQUEST_METHOD"] == "POST" && body["oauth_service"].to_string() != "" && body["oauth_state"].to_string() != "")
|
||||
{
|
||||
context.session["oauth_service"] = body["oauth_service"].to_string();
|
||||
context.session["oauth_state"] = body["oauth_state"].to_string();
|
||||
context.var["starter"]["json"]["status"] = "success";
|
||||
context.call["starter"]["json"]["status"] = "success";
|
||||
}
|
||||
else
|
||||
{
|
||||
context.set_status(400, "Bad Request");
|
||||
context.var["starter"]["json"]["status"] = "error";
|
||||
context.var["starter"]["json"]["message"] = "Invalid input";
|
||||
context.call["starter"]["json"]["status"] = "error";
|
||||
context.call["starter"]["json"]["message"] = "Invalid input";
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.var["starter"]["page_title"] = "Dashboard";
|
||||
context.call["starter"]["page_title"] = "Dashboard";
|
||||
starter_register_css("views/dashboard.css", context);
|
||||
|
||||
DTree props;
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.var["starter"]["page_title"] = "Features";
|
||||
context.call["starter"]["page_title"] = "Features";
|
||||
starter_register_css("views/marketing.css", context);
|
||||
<>
|
||||
<h1>Features</h1>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& 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);
|
||||
f64 pi = 3.14159265358979323846;
|
||||
|
||||
@ -69,7 +69,7 @@ RENDER(Request& context)
|
||||
props.clear();
|
||||
props["id"] = "needle_demo";
|
||||
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["listen"].set_bool(true);
|
||||
props["label"] = "CPU";
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.var["starter"]["page_title"] = "Home";
|
||||
context.call["starter"]["page_title"] = "Home";
|
||||
starter_register_css("views/marketing.css", context);
|
||||
|
||||
DTree props;
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.var["starter"]["page_title"] = "Components";
|
||||
context.call["starter"]["page_title"] = "Components";
|
||||
starter_register_css("views/marketing.css", context);
|
||||
|
||||
<>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& 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
|
||||
<pre>UCE starter AJAX fragment response</pre>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.var["starter"]["page_title"] = "Ajaxy";
|
||||
context.call["starter"]["page_title"] = "Ajaxy";
|
||||
<>
|
||||
<h1>Ajax Demo</h1>
|
||||
<div id="page2-section1">
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& 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);
|
||||
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);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.var["starter"]["page_title"] = "Themes";
|
||||
context.call["starter"]["page_title"] = "Themes";
|
||||
starter_register_css("views/themes.css", context);
|
||||
String current_theme = context.cfg.get_by_path("theme/key").to_string();
|
||||
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
starter_boot(context);
|
||||
context.var["starter"]["page_title"] = "Workspace";
|
||||
String section = first(context.var["starter"]["route"]["param"].to_string(), "overview");
|
||||
context.call["starter"]["page_title"] = "Workspace";
|
||||
String section = first(context.call["starter"]["route"]["param"].to_string(), "overview");
|
||||
if(section != "overview" && section != "projects" && section != "activity")
|
||||
section = "overview";
|
||||
|
||||
|
||||
4
site/index.uce
Normal file
4
site/index.uce
Normal file
@ -0,0 +1,4 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
context.header["Location"] = "/info/";
|
||||
}
|
||||
@ -25,6 +25,19 @@ RENDER(Request& context)
|
||||
|
||||
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("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("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");
|
||||
@ -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_page_end();
|
||||
}
|
||||
}
|
||||
|
||||
19
site/tests/index.uce
Normal file
19
site/tests/index.uce
Normal 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();
|
||||
}
|
||||
42
site/tests/preprocessor.uce
Normal file
42
site/tests/preprocessor.uce
Normal 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();
|
||||
}
|
||||
@ -123,4 +123,4 @@ WS(Request& context)
|
||||
}
|
||||
|
||||
ws_send_to(ws_connection_id(), json_encode(websocket_suite_event(context, "unknown", nonce)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
char c = content[i];
|
||||
@ -172,16 +216,15 @@ void compiler_append_text_literal_output(String& parsed_content, String& literal
|
||||
{
|
||||
if(literal_buffer == "")
|
||||
return;
|
||||
parsed_content.append("print(R\"(" + literal_buffer + ")\");");
|
||||
parsed_content.append("print(" + compiler_cpp_string_literal(literal_buffer) + ");");
|
||||
literal_buffer.clear();
|
||||
}
|
||||
|
||||
String compiler_process_text_literal(Request* context, SharedUnit* su, String content)
|
||||
{
|
||||
String parsed_content;
|
||||
String html_output_start = "print(R\"(";
|
||||
String html_output_end = ")\");";
|
||||
String code_buffer = "";
|
||||
String literal_buffer = "";
|
||||
CompilerCodeState code_state;
|
||||
bool inside_code = false;
|
||||
bool is_field = false;
|
||||
@ -200,6 +243,7 @@ String compiler_process_text_literal(Request* context, SharedUnit* su, String co
|
||||
inside_code = true;
|
||||
code_buffer = "";
|
||||
code_state = CompilerCodeState();
|
||||
compiler_append_text_literal_output(parsed_content, literal_buffer);
|
||||
if(c2 == '=')
|
||||
{
|
||||
is_field = true;
|
||||
@ -221,7 +265,7 @@ String compiler_process_text_literal(Request* context, SharedUnit* su, String co
|
||||
continue;
|
||||
}
|
||||
|
||||
parsed_content.append(1, c);
|
||||
literal_buffer.append(1, c);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -234,27 +278,23 @@ String compiler_process_text_literal(Request* context, SharedUnit* su, String co
|
||||
if(escape_field)
|
||||
{
|
||||
parsed_content.append(
|
||||
html_output_end +
|
||||
"print(html_escape( " +
|
||||
code_buffer +
|
||||
" )); " +
|
||||
html_output_start
|
||||
" )); "
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
parsed_content.append(
|
||||
html_output_end +
|
||||
"print( " +
|
||||
code_buffer +
|
||||
" ); " +
|
||||
html_output_start
|
||||
" ); "
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
parsed_content.append(html_output_end + code_buffer + html_output_start);
|
||||
parsed_content.append(code_buffer);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
@ -444,4 +487,4 @@ String compiler_preprocess_source(Request* context, SharedUnit* su, String conte
|
||||
{
|
||||
content = compiler_rewrite_named_render_syntax(content);
|
||||
return(compiler_preprocess_shared_unit_char_wise(context, su, content));
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,8 @@ const char* UCE_SETUP_SYMBOL = "__uce_set_current_request";
|
||||
const char* UCE_RENDER_SYMBOL = "__uce_render";
|
||||
const char* UCE_COMPONENT_SYMBOL = "__uce_component";
|
||||
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;
|
||||
|
||||
struct SharedUnitFilesystemState
|
||||
@ -48,6 +50,10 @@ struct SharedUnitCompileCheck
|
||||
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)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
value = trim(value);
|
||||
@ -167,16 +160,16 @@ bool compiler_failure_retry_deferred(Request* context, SharedUnit* su, const Sha
|
||||
{
|
||||
if(!context || !su)
|
||||
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)
|
||||
return(false);
|
||||
if(state.source_time == 0)
|
||||
return(false);
|
||||
if(state.source_time > state.compile_output_time)
|
||||
return(false);
|
||||
return(time() < (u64)state.compile_output_time + retry_seconds);
|
||||
auto required_failure_inputs_time = std::max({
|
||||
state.source_time,
|
||||
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)
|
||||
@ -653,6 +646,8 @@ void load_shared_unit(Request* context, SharedUnit* su)
|
||||
su->on_render = 0;
|
||||
su->on_component = 0;
|
||||
su->on_websocket = 0;
|
||||
su->on_once = 0;
|
||||
su->on_init = 0;
|
||||
su->on_setup = 0;
|
||||
su->so_handle = 0;
|
||||
su->compiler_messages = "";
|
||||
@ -695,17 +690,31 @@ void load_shared_unit(Request* context, SharedUnit* su)
|
||||
dlerror();
|
||||
su->on_websocket = (request_ref_handler)dlsym(su->so_handle, UCE_WEBSOCKET_SYMBOL);
|
||||
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");
|
||||
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
|
||||
// printf("(i) loaded unit %s\n", su->file_name.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
const char* dl_error = dlerror();
|
||||
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_error_status = su->compiler_messages;
|
||||
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)
|
||||
@ -1262,6 +1448,70 @@ String component_handler_symbol(String 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)
|
||||
{
|
||||
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)
|
||||
return(false);
|
||||
|
||||
if(!su->on_setup)
|
||||
{
|
||||
if(error_out)
|
||||
*error_out = "internal error: " + String(UCE_SETUP_SYMBOL) + "() not defined in " + file_name;
|
||||
if(!compiler_prepare_request_handler(context, su, error_out, true))
|
||||
return(false);
|
||||
}
|
||||
|
||||
auto handler = get_page_render_handler(su, render_name);
|
||||
if(!handler)
|
||||
{
|
||||
if(error_out)
|
||||
{
|
||||
if(trim(render_name) == "" || trim(render_name) == "render")
|
||||
*error_out = "no RENDER() entry point";
|
||||
else
|
||||
*error_out = "no RENDER:" + render_name + "() entry point";
|
||||
}
|
||||
*error_out = compiler_missing_request_handler_message(UnitCallMacroKind::render, render_name);
|
||||
return(false);
|
||||
}
|
||||
|
||||
UnitInvocationScope invoke_scope(context, su);
|
||||
su->on_setup(context);
|
||||
f64 render_start = time_precise();
|
||||
bool count_request = compiler_is_request_entry_unit(context, su);
|
||||
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,
|
||||
"uncaught exception during render"
|
||||
);
|
||||
throw;
|
||||
}
|
||||
compiler_execute_request_handler(
|
||||
context,
|
||||
su,
|
||||
handler,
|
||||
compiler_is_request_entry_unit(context, su),
|
||||
"uncaught exception during render"
|
||||
);
|
||||
return(true);
|
||||
}
|
||||
|
||||
@ -1349,45 +1577,24 @@ bool compiler_invoke_component(Request* context, String file_name, String render
|
||||
if(!su)
|
||||
return(false);
|
||||
|
||||
if(!su->on_setup)
|
||||
{
|
||||
if(error_out)
|
||||
*error_out = "internal error: " + String(UCE_SETUP_SYMBOL) + "() not defined in " + file_name;
|
||||
if(!compiler_prepare_request_handler(context, su, error_out, true))
|
||||
return(false);
|
||||
}
|
||||
|
||||
auto handler = get_component_handler(su, render_name);
|
||||
if(!handler)
|
||||
{
|
||||
if(error_out)
|
||||
{
|
||||
if(trim(render_name) == "")
|
||||
*error_out = "no COMPONENT() entry point";
|
||||
else
|
||||
*error_out = "no COMPONENT:" + render_name + "() entry point";
|
||||
}
|
||||
*error_out = compiler_missing_request_handler_message(UnitCallMacroKind::component, render_name);
|
||||
return(false);
|
||||
}
|
||||
|
||||
UnitInvocationScope invoke_scope(context, su);
|
||||
su->on_setup(context);
|
||||
f64 render_start = time_precise();
|
||||
compiler_begin_render_result(su, false);
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
compiler_execute_request_handler(
|
||||
context,
|
||||
su,
|
||||
handler,
|
||||
false,
|
||||
"uncaught exception during component render"
|
||||
);
|
||||
return(true);
|
||||
}
|
||||
|
||||
@ -1421,26 +1628,13 @@ void compiler_invoke_websocket(Request* context, String file_name)
|
||||
return;
|
||||
}
|
||||
|
||||
UnitInvocationScope invoke_scope(context, su);
|
||||
su->on_setup(context);
|
||||
f64 render_start = time_precise();
|
||||
bool count_request = compiler_is_request_entry_unit(context, su);
|
||||
compiler_begin_render_result(su, count_request);
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
compiler_execute_request_handler(
|
||||
context,
|
||||
su,
|
||||
su->on_websocket,
|
||||
compiler_is_request_entry_unit(context, su),
|
||||
"uncaught exception during websocket handler"
|
||||
);
|
||||
}
|
||||
|
||||
void unit_render(String file_name)
|
||||
@ -1499,14 +1693,11 @@ void component_render(String name, DTree props, Request& context)
|
||||
return;
|
||||
}
|
||||
|
||||
DTree previous_props = context.props;
|
||||
context.props = props;
|
||||
RequestPropsScope props_scope(&context, props);
|
||||
|
||||
String error_message = "";
|
||||
if(!compiler_invoke_component(&context, resolved_name, render_name, &error_message) && error_message != "")
|
||||
print(component_error_banner(error_message));
|
||||
|
||||
context.props = previous_props;
|
||||
}
|
||||
|
||||
String component(String name)
|
||||
@ -1558,16 +1749,52 @@ DTree* unit_call(String file_name, String function_name, DTree* call_param)
|
||||
}
|
||||
else
|
||||
{
|
||||
auto f = (dtree_call_handler)dlsym(su->so_handle, function_name.c_str());
|
||||
if(!f)
|
||||
auto macro_target = unit_call_macro_target(function_name);
|
||||
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
|
||||
{
|
||||
UnitInvocationScope invoke_scope(context, su);
|
||||
su->on_setup(context);
|
||||
result = f(call_param);
|
||||
auto f = (dtree_call_handler)dlsym(su->so_handle, function_name.c_str());
|
||||
if(!f)
|
||||
{
|
||||
print("Error: unit_call() function '", function_name, "' not found");
|
||||
}
|
||||
else
|
||||
{
|
||||
UnitInvocationScope invoke_scope(context, su);
|
||||
su->on_setup(context);
|
||||
result = f(call_param);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
#define RENDER(X) extern "C" void __uce_render(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 EXPORT extern "C"
|
||||
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
#include "functionlib.h"
|
||||
|
||||
#define PCRE2_CODE_UNIT_WIDTH 8
|
||||
#include <pcre2.h>
|
||||
#include <stdexcept>
|
||||
|
||||
String var_dump(StringMap map, String prefix, String postfix)
|
||||
{
|
||||
String result = "";
|
||||
@ -151,6 +155,347 @@ String replace(String s, String search, String replace_with)
|
||||
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)
|
||||
{
|
||||
s64 len = raw.length();
|
||||
|
||||
@ -14,6 +14,11 @@ bool str_starts_with(String haystack, String needle);
|
||||
bool str_ends_with(String haystack, String needle);
|
||||
bool contains(String haystack, String needle);
|
||||
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);
|
||||
StringList split_space(String str);
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
#include <atomic>
|
||||
#include <set>
|
||||
|
||||
typedef unsigned char u8;
|
||||
typedef signed char s8;
|
||||
@ -84,6 +85,8 @@ struct SharedUnit {
|
||||
request_ref_handler on_render = 0;
|
||||
request_ref_handler on_component = 0;
|
||||
request_ref_handler on_websocket = 0;
|
||||
request_ref_handler on_once = 0;
|
||||
request_ref_handler on_init = 0;
|
||||
|
||||
String compiler_messages;
|
||||
String compile_status = "unknown";
|
||||
@ -156,8 +159,9 @@ struct Request {
|
||||
StringMap cookies;
|
||||
StringMap session;
|
||||
String session_loaded_hash = "";
|
||||
std::set<String> once_units;
|
||||
|
||||
//DTree var;
|
||||
DTree call;
|
||||
DTree cfg;
|
||||
DTree props;
|
||||
DTree connection;
|
||||
|
||||
@ -702,6 +702,13 @@ String ws_connection_id()
|
||||
return(context->resources.websocket_connection_id);
|
||||
}
|
||||
|
||||
String ws_message()
|
||||
{
|
||||
if(!context)
|
||||
return("");
|
||||
return(context->in);
|
||||
}
|
||||
|
||||
String ws_scope()
|
||||
{
|
||||
return(current_ws_scope());
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -3,6 +3,7 @@ def register(registry):
|
||||
("doc index", "/doc/index.uce", "<html>"),
|
||||
("doc singlepage", "/doc/singlepage.uce", "<html>"),
|
||||
("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"),
|
||||
]
|
||||
|
||||
@ -15,4 +16,4 @@ def register(registry):
|
||||
|
||||
return run
|
||||
|
||||
registry.case(name, make_case(), tags=["http", "smoke", "uce", "public"])
|
||||
registry.case(name, make_case(), tags=["http", "smoke", "uce", "public"])
|
||||
|
||||
@ -10,6 +10,7 @@ def register(registry):
|
||||
pages = [
|
||||
("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 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 components", "/tests/components.uce", "Components", ["http", "suite", "uce", "public"]),
|
||||
("site tests markdown", "/tests/markdown.uce", "Markdown", ["http", "suite", "uce", "public"]),
|
||||
@ -35,4 +36,4 @@ def register(registry):
|
||||
|
||||
return run
|
||||
|
||||
registry.case(name, make_case(), tags=tags)
|
||||
registry.case(name, make_case(), tags=tags)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user