diff --git a/site/examples/uce-starter/components/theme/head.uce b/site/examples/uce-starter/components/theme/head.uce
index 77ddaa7..7771b74 100644
--- a/site/examples/uce-starter/components/theme/head.uce
+++ b/site/examples/uce-starter/components/theme/head.uce
@@ -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);
diff --git a/site/examples/uce-starter/components/theme/page_shell.uce b/site/examples/uce-starter/components/theme/page_shell.uce
index 43cc9e5..ba8196d 100644
--- a/site/examples/uce-starter/components/theme/page_shell.uce
+++ b/site/examples/uce-starter/components/theme/page_shell.uce
@@ -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);
}
diff --git a/site/examples/uce-starter/index.uce b/site/examples/uce-starter/index.uce
index 6c5513f..9d8d209 100644
--- a/site/examples/uce-starter/index.uce
+++ b/site/examples/uce-starter/index.uce
@@ -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)
<>
404 Not Found
- = context.var["app"]["error"].to_string() ?>
+ = context.call["app"]["error"].to_string() ?>
>
}
String main_html = ob_get_close();
- context.var["app"]["fragments"]["main"] = main_html;
+ context.call["app"]["fragments"]["main"] = main_html;
app_render_page(context);
}
diff --git a/site/examples/uce-starter/lib/app.uce b/site/examples/uce-starter/lib/app.uce
index 49247c6..6bfcf75 100644
--- a/site/examples/uce-starter/lib/app.uce
+++ b/site/examples/uce-starter/lib/app.uce
@@ -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)
{
diff --git a/site/examples/uce-starter/lib/user.class.h b/site/examples/uce-starter/lib/user.class.h
index 6932641..246937d 100644
--- a/site/examples/uce-starter/lib/user.class.h
+++ b/site/examples/uce-starter/lib/user.class.h
@@ -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();
}
};
diff --git a/site/examples/uce-starter/themes/common/page.json.uce b/site/examples/uce-starter/themes/common/page.json.uce
index 99b4cfd..766f6a3 100644
--- a/site/examples/uce-starter/themes/common/page.json.uce
+++ b/site/examples/uce-starter/themes/common/page.json.uce
@@ -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));
}
diff --git a/site/examples/uce-starter/themes/localfirst/page.html.uce b/site/examples/uce-starter/themes/localfirst/page.html.uce
index 5a7771c..9f38608 100644
--- a/site/examples/uce-starter/themes/localfirst/page.html.uce
+++ b/site/examples/uce-starter/themes/localfirst/page.html.uce
@@ -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);
diff --git a/site/examples/uce-starter/views/account/login.uce b/site/examples/uce-starter/views/account/login.uce
index 89d5682..4dfe0ff 100644
--- a/site/examples/uce-starter/views/account/login.uce
+++ b/site/examples/uce-starter/views/account/login.uce
@@ -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")
diff --git a/site/examples/uce-starter/views/account/profile.uce b/site/examples/uce-starter/views/account/profile.uce
index 17fbc14..33257ac 100644
--- a/site/examples/uce-starter/views/account/profile.uce
+++ b/site/examples/uce-starter/views/account/profile.uce
@@ -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) {
diff --git a/site/examples/uce-starter/views/account/register.uce b/site/examples/uce-starter/views/account/register.uce
index 8f92f47..e4bc7ab 100644
--- a/site/examples/uce-starter/views/account/register.uce
+++ b/site/examples/uce-starter/views/account/register.uce
@@ -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")
diff --git a/site/examples/uce-starter/views/auth/callback.uce b/site/examples/uce-starter/views/auth/callback.uce
index 5775764..e2fd61e 100644
--- a/site/examples/uce-starter/views/auth/callback.uce
+++ b/site/examples/uce-starter/views/auth/callback.uce
@@ -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"];
diff --git a/site/examples/uce-starter/views/auth/demo.uce b/site/examples/uce-starter/views/auth/demo.uce
index c259605..9f50958 100644
--- a/site/examples/uce-starter/views/auth/demo.uce
+++ b/site/examples/uce-starter/views/auth/demo.uce
@@ -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;
diff --git a/site/examples/uce-starter/views/auth/store-oauth-session.uce b/site/examples/uce-starter/views/auth/store-oauth-session.uce
index 01868e4..63ef19e 100644
--- a/site/examples/uce-starter/views/auth/store-oauth-session.uce
+++ b/site/examples/uce-starter/views/auth/store-oauth-session.uce
@@ -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";
}
}
diff --git a/site/examples/uce-starter/views/dashboard.uce b/site/examples/uce-starter/views/dashboard.uce
index 14c71b6..6e3ae20 100644
--- a/site/examples/uce-starter/views/dashboard.uce
+++ b/site/examples/uce-starter/views/dashboard.uce
@@ -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;
diff --git a/site/examples/uce-starter/views/features.uce b/site/examples/uce-starter/views/features.uce
index 3455fee..d162daf 100644
--- a/site/examples/uce-starter/views/features.uce
+++ b/site/examples/uce-starter/views/features.uce
@@ -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);
<>
Features
diff --git a/site/examples/uce-starter/views/gauges.uce b/site/examples/uce-starter/views/gauges.uce
index 1c6bf73..ed8af5e 100644
--- a/site/examples/uce-starter/views/gauges.uce
+++ b/site/examples/uce-starter/views/gauges.uce
@@ -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";
diff --git a/site/examples/uce-starter/views/index.uce b/site/examples/uce-starter/views/index.uce
index 9b3fa50..ec1dfcb 100644
--- a/site/examples/uce-starter/views/index.uce
+++ b/site/examples/uce-starter/views/index.uce
@@ -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;
diff --git a/site/examples/uce-starter/views/page1.uce b/site/examples/uce-starter/views/page1.uce
index d44d6e1..520f6f9 100644
--- a/site/examples/uce-starter/views/page1.uce
+++ b/site/examples/uce-starter/views/page1.uce
@@ -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);
<>
diff --git a/site/examples/uce-starter/views/page2-section1.uce b/site/examples/uce-starter/views/page2-section1.uce
index c520b13..8720bca 100644
--- a/site/examples/uce-starter/views/page2-section1.uce
+++ b/site/examples/uce-starter/views/page2-section1.uce
@@ -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
UCE starter AJAX fragment response
diff --git a/site/examples/uce-starter/views/page2.uce b/site/examples/uce-starter/views/page2.uce
index e1a571a..a73c189 100644
--- a/site/examples/uce-starter/views/page2.uce
+++ b/site/examples/uce-starter/views/page2.uce
@@ -3,7 +3,7 @@
RENDER(Request& context)
{
starter_boot(context);
- context.var["starter"]["page_title"] = "Ajaxy";
+ context.call["starter"]["page_title"] = "Ajaxy";
<>
Ajax Demo
diff --git a/site/examples/uce-starter/views/theme-preview.uce b/site/examples/uce-starter/views/theme-preview.uce
index d62ae54..c3251e2 100644
--- a/site/examples/uce-starter/views/theme-preview.uce
+++ b/site/examples/uce-starter/views/theme-preview.uce
@@ -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);
diff --git a/site/examples/uce-starter/views/themes.uce b/site/examples/uce-starter/views/themes.uce
index 4a14a23..1839628 100644
--- a/site/examples/uce-starter/views/themes.uce
+++ b/site/examples/uce-starter/views/themes.uce
@@ -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();
diff --git a/site/examples/uce-starter/views/workspace/index.uce b/site/examples/uce-starter/views/workspace/index.uce
index 620bf31..d61522f 100644
--- a/site/examples/uce-starter/views/workspace/index.uce
+++ b/site/examples/uce-starter/views/workspace/index.uce
@@ -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";
diff --git a/site/index.uce b/site/index.uce
new file mode 100644
index 0000000..bc10f8e
--- /dev/null
+++ b/site/index.uce
@@ -0,0 +1,4 @@
+RENDER(Request& context)
+{
+ context.header["Location"] = "/info/";
+}
\ No newline at end of file
diff --git a/site/tests/core.uce b/site/tests/core.uce
index 4a82731..bafab38 100644
--- a/site/tests/core.uce
+++ b/site/tests/core.uce
@@ -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("(?
[A-Za-z0-9._%+-]+)@(?[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("#(?[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_]+)", "$1", "#uce") == "uce", regex_replace("#([A-Za-z0-9_]+)", "$1", "#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();
-}
\ No newline at end of file
+}
diff --git a/site/tests/index.uce b/site/tests/index.uce
new file mode 100644
index 0000000..ed2fc21
--- /dev/null
+++ b/site/tests/index.uce
@@ -0,0 +1,19 @@
+#include "testlib.h"
+
+RENDER(Request& context)
+{
+ site_tests_page_start("Coverage Index", "Public and local-only UCE regression coverage pages.");
+ ?>
+ 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");
+ ?>
+ site_tests_page_end();
+}
diff --git a/site/tests/preprocessor.uce b/site/tests/preprocessor.uce
new file mode 100644
index 0000000..ae4fb44
--- /dev/null
+++ b/site/tests/preprocessor.uce
@@ -0,0 +1,42 @@
+#include "testlib.h"
+
+String preprocessor_nested_literal()
+{
+ ob_start();
+ <>
+ nested )" marker
+ >
+ 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.");
+ ?>
+
+ top-level )" marker
+
+
+
+
+ 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();
+}
diff --git a/site/tests/websockets.ws.uce b/site/tests/websockets.ws.uce
index ce39a76..aa43de7 100644
--- a/site/tests/websockets.ws.uce
+++ b/site/tests/websockets.ws.uce
@@ -123,4 +123,4 @@ WS(Request& context)
}
ws_send_to(ws_connection_id(), json_encode(websocket_suite_event(context, "unknown", nonce)));
-}
\ No newline at end of file
+}
diff --git a/src/lib/compiler-parser.cpp b/src/lib/compiler-parser.cpp
index 088790c..3de7332 100644
--- a/src/lib/compiler-parser.cpp
+++ b/src/lib/compiler-parser.cpp
@@ -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(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));
-}
\ No newline at end of file
+}
diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp
index e619989..7ba62da 100644
--- a/src/lib/compiler.cpp
+++ b/src/lib/compiler.cpp
@@ -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);
+ }
}
}
}
diff --git a/src/lib/compiler.h b/src/lib/compiler.h
index 0715790..3c9dd99 100644
--- a/src/lib/compiler.h
+++ b/src/lib/compiler.h
@@ -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"
diff --git a/src/lib/functionlib.cpp b/src/lib/functionlib.cpp
index 96d1d32..6d052db 100644
--- a/src/lib/functionlib.cpp
+++ b/src/lib/functionlib.cpp
@@ -1,5 +1,9 @@
#include "functionlib.h"
+#define PCRE2_CODE_UNIT_WIDTH 8
+#include
+#include
+
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(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(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(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(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(subject.c_str()),
+ subject.length(),
+ 0,
+ options,
+ 0,
+ 0,
+ reinterpret_cast(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(subject.c_str()),
+ subject.length(),
+ 0,
+ PCRE2_SUBSTITUTE_GLOBAL,
+ 0,
+ 0,
+ reinterpret_cast(replacement.c_str()),
+ replacement.length(),
+ reinterpret_cast(&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();
diff --git a/src/lib/functionlib.h b/src/lib/functionlib.h
index ff54ee3..4257e78 100644
--- a/src/lib/functionlib.h
+++ b/src/lib/functionlib.h
@@ -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);
diff --git a/src/lib/types.h b/src/lib/types.h
index e026d8b..1cf9ef7 100644
--- a/src/lib/types.h
+++ b/src/lib/types.h
@@ -7,6 +7,7 @@
#include
#include
#include
+#include
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 once_units;
- //DTree var;
+ DTree call;
DTree cfg;
DTree props;
DTree connection;
diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp
index 7f02939..c4a87a1 100644
--- a/src/linux_fastcgi.cpp
+++ b/src/linux_fastcgi.cpp
@@ -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());
diff --git a/tests/plugins/__pycache__/uce_http_smoke.cpython-313.pyc b/tests/plugins/__pycache__/uce_http_smoke.cpython-313.pyc
index c20f552..6f8ce29 100644
Binary files a/tests/plugins/__pycache__/uce_http_smoke.cpython-313.pyc and b/tests/plugins/__pycache__/uce_http_smoke.cpython-313.pyc differ
diff --git a/tests/plugins/__pycache__/uce_site_suite.cpython-313.pyc b/tests/plugins/__pycache__/uce_site_suite.cpython-313.pyc
index 7d8c04f..a21c68e 100644
Binary files a/tests/plugins/__pycache__/uce_site_suite.cpython-313.pyc and b/tests/plugins/__pycache__/uce_site_suite.cpython-313.pyc differ
diff --git a/tests/plugins/uce_http_smoke.py b/tests/plugins/uce_http_smoke.py
index 6a5b48c..7ce4ad0 100644
--- a/tests/plugins/uce_http_smoke.py
+++ b/tests/plugins/uce_http_smoke.py
@@ -3,6 +3,7 @@ def register(registry):
("doc index", "/doc/index.uce", ""),
("doc singlepage", "/doc/singlepage.uce", ""),
("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"])
\ No newline at end of file
+ registry.case(name, make_case(), tags=["http", "smoke", "uce", "public"])
diff --git a/tests/plugins/uce_site_suite.py b/tests/plugins/uce_site_suite.py
index 0b313c4..50e0983 100644
--- a/tests/plugins/uce_site_suite.py
+++ b/tests/plugins/uce_site_suite.py
@@ -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)
\ No newline at end of file
+ registry.case(name, make_case(), tags=tags)