working on documentation and more API functions
This commit is contained in:
@@ -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"); ?>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+38
-130
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
-102
@@ -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";
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
context.header["Location"] = "/info/";
|
||||
}
|
||||
+14
-1
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user