diff --git a/README.md b/README.md index 6a4ebd1..0127e4b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ UCE is a PHP-inspired server-side runtime that lets you build web pages and hand - WebSocket pages can additionally expose `WS(Request& context)` - sub-rendering and components pass structured data through `context.props` - nginx can forward normal `.uce` requests and ordinary `.ws.uce` page loads to the FastCGI socket, while real WebSocket upgrade requests for `.ws.uce` endpoints go to the built-in HTTP/WebSocket listener -- the nginx-published application tree now lives under `site/` +- the nginx-published application tree lives under `site/` - you can include C++ code as much as you want, but only .uce files called via API functions and entry points will be pre-processed - the preprocessor has two jobs: - allow for inline HTML within C++ and the use of templating tags inside of that HTML @@ -42,6 +42,7 @@ The current build expects: - `clang++` - `mysql_config` +- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu) - standard Linux development headers for `dl`, `pthread`, sockets, and backtrace support The binary is written to: @@ -52,7 +53,7 @@ bin/uce_fastcgi.linux.bin ## Runtime Model -UCE pages now use explicit request handlers instead of implicit globals: +UCE pages use explicit request handlers instead of implicit globals: - `RENDER(Request& context)` for normal HTTP rendering - `WS(Request& context)` for inbound WebSocket messages @@ -68,7 +69,7 @@ Useful related runtime patterns: - `context.params`, `context.get`, `context.post`, `context.cookies`, `context.session`, and `context.header` for request/response state - `context.set_status(code[, reason])` to set the HTTP response status -Useful helpers for that data model now include: +Useful helpers for that data model include: - `DTree::get_by_path("a/b/c")` for path-style config traversal without creating missing keys - `DTree::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended @@ -90,9 +91,14 @@ COMPONENT:BODY(Request& context) Those are intended for sub-rendering through helpers such as `component("components/card:BODY", props, context)` rather than direct page entry. +Additional lifecycle hooks are also available on ordinary `.uce` units: + +- `INIT(Request& context)` runs once when a worker loads that unit's shared object into memory +- `ONCE(Request& context)` runs once per request before the first `RENDER()` or `COMPONENT...` entrypoint from that file + ## Template Output -UCE now treats template parsing as one shared code-vs-literal state machine. +UCE treats template parsing as one shared code-vs-literal state machine. - `<>` and `?>` both enter literal output mode - `` and `` by default for user-visible text. Use `` only for trusted markup or content that has already been escaped. -The parser now treats C++ `//` and `/* ... */` comments as comments in both normal code and `` islands, so quotes or delimiter markers inside comments do not confuse template parsing. +The parser treats C++ `//` and `/* ... */` comments as comments in both normal code and `` islands, so quotes or delimiter markers inside comments do not confuse template parsing. -The preprocessing implementation is now split between `src/lib/compiler.cpp` and `src/lib/compiler-parser.cpp`. `compiler.cpp` owns unit compilation and cache orchestration, while `compiler-parser.cpp` owns source rewriting and template parsing. +The preprocessing implementation is split between `src/lib/compiler.cpp` and `src/lib/compiler-parser.cpp`. `compiler.cpp` owns unit compilation and cache orchestration, while `compiler-parser.cpp` owns source rewriting and template parsing. ## Components @@ -141,6 +147,8 @@ Components expose `COMPONENT(Request& context)` as their default entrypoint and The component helpers call only `COMPONENT...` handlers. A file meant purely for component use can define `COMPONENT()` without defining `RENDER()`, which keeps direct page entry and component entry cleanly separated. Inside a component file, `component(":NAME", props, context)` and `component_render(":NAME", props, context)` target another named component handler in that same file. +If the component file also defines `ONCE(Request& context)`, that hook runs once per request before the file's first component/render entrypoint. If it defines `INIT(Request& context)`, that hook runs once when the worker loads the unit. + ## WebSockets The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate API to page code: @@ -158,7 +166,7 @@ The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate By default, the WebSocket scope is the current page file, so `ws_send()` queues a message for clients connected to that same `.ws.uce` endpoint. -Each live WebSocket connection now owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client. +Each live WebSocket connection owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client. The current inbound payload is available directly as `context.in`, and the runtime mirrors message metadata into `context.params` using keys such as `WS_CONNECTION_ID`, `WS_SCOPE`, `WS_CONNECTION_COUNT`, `WS_OPCODE`, `WS_MESSAGE_TYPE`, and `WS_DOCUMENT_URI`. @@ -166,11 +174,11 @@ The current inbound payload is available directly as `context.in`, and the runti Set `binary = true` on `ws_send()` or `ws_send_to()` to queue a binary frame instead of a text frame. -The runtime now accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS(Request& context)`. +The runtime accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS(Request& context)`. ## Error Reporting -Unhandled exceptions and recovered fatal request signals now return a `500 Internal Server Error` response with a plain-text trace instead of simply dropping the upstream connection and leaving nginx to show a generic `502`. +Unhandled exceptions and recovered fatal request signals return a `500 Internal Server Error` response with a plain-text trace instead of simply dropping the upstream connection and leaving nginx to show a generic `502`. The demo page `site/test/error-reporting.uce` can be used to exercise: @@ -232,7 +240,7 @@ On a Debian or Ubuntu host, start with the packages needed to build and run UCE ```bash apt update -apt install -y nginx clang mariadb-client libmariadb-dev build-essential +apt install -y nginx clang mariadb-client libmariadb-dev libpcre2-dev build-essential ``` The exact package names may vary by distro. The important requirements are: @@ -240,6 +248,7 @@ The exact package names may vary by distro. The important requirements are: - `nginx` - `clang++` - `mysql_config` +- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu) - normal Linux development headers for threads, sockets, `dl`, and backtrace support ### 2. Put the repo on the server @@ -484,7 +493,7 @@ Common failure modes: - WebSocket upgrade fails Check that nginx is routing `.ws.uce` to `proxy_pass`, not `fastcgi_pass`, and that `HTTP_PORT` is reachable on localhost. - Requests compile but immediately crash - Check `journalctl -u uce.service`. Generated units now carry an ABI metadata sidecar and should be recompiled automatically after runtime ABI changes, but clearing stale artifacts under `BIN_DIRECTORY` is still a useful last-resort recovery step if the cache has been damaged manually. + Check `journalctl -u uce.service`. Generated units carry an ABI metadata sidecar and should be recompiled automatically after runtime ABI changes, but clearing stale artifacts under `BIN_DIRECTORY` is still a useful last-resort recovery step if the cache has been damaged manually. - nginx serves raw source or internal files Tighten the server root and add explicit deny rules for non-public directories. diff --git a/etc/uce/settings.cfg b/etc/uce/settings.cfg index 56933fa..0d11f59 100644 --- a/etc/uce/settings.cfg +++ b/etc/uce/settings.cfg @@ -20,8 +20,8 @@ JIT_COMPILE_ON_REQUEST=1 # ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP PROACTIVE_COMPILE_ENABLED=1 -# AFTER A FAILED COMPILE, SERVE THE LAST COMPILER OUTPUT AND WAIT THIS MANY SECONDS -# BEFORE TRYING AGAIN IF THE SOURCE FILE HAS NOT CHANGED +# AFTER A FAILED COMPILE, UCE NOW REUSES THE PERSISTED COMPILER OUTPUT UNTIL +# THE SOURCE OR COMPILER INPUTS CHANGE. THIS SETTING IS KEPT FOR COMPATIBILITY. COMPILE_FAILURE_RETRY_SECONDS=10 # PERIODIC KNOWN-.uce RECHECK INTERVAL IN SECONDS diff --git a/scripts/build_linux.sh b/scripts/build_linux.sh index 8498a10..d45faae 100755 --- a/scripts/build_linux.sh +++ b/scripts/build_linux.sh @@ -14,7 +14,7 @@ mkdir work > /dev/null 2>&1 COMPILER="clang++" FLAGS="-g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math" -LIBS="-ldl -lm -lpthread `mysql_config --cflags --libs`" +LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs`" SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\"" echo "Compliling executable..." diff --git a/site/demo/index.uce b/site/demo/index.uce index ee3ae77..2e29d99 100644 --- a/site/demo/index.uce +++ b/site/demo/index.uce @@ -38,6 +38,7 @@ RENDER(Request& context) + @@ -61,6 +62,7 @@ RENDER(Request& context)
Advanced
+ diff --git a/site/demo/once-init.uce b/site/demo/once-init.uce new file mode 100644 index 0000000..d8d86da --- /dev/null +++ b/site/demo/once-init.uce @@ -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; + + <> + + +} + +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\")"; + + <> + +

+ UCE Test: + ONCE() and INIT() +

+

+ 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. +

+ + + + +} diff --git a/site/demo/preprocessor-comments.uce b/site/demo/preprocessor-comments.uce index 7a2c632..9555c52 100644 --- a/site/demo/preprocessor-comments.uce +++ b/site/demo/preprocessor-comments.uce @@ -11,6 +11,7 @@ RENDER(Request& context)

This page exists to prove the template parser ignores quotes and template markers that appear inside C++ comments.

+

It also renders a literal raw-string terminator sequence safely: )".

marker. @@ -36,4 +37,4 @@ RENDER(Request& context)
-} \ No newline at end of file +} diff --git a/site/demo/regex.uce b/site/demo/regex.uce new file mode 100644 index 0000000..c6c44a7 --- /dev/null +++ b/site/demo/regex.uce @@ -0,0 +1,74 @@ + +#include "demo_guard.h" + +void regex_row(String label, String result, String expect) +{ + bool pass = (result == expect); + <> + + + "> + +} + +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("(?[A-Za-z0-9._%+-]+)@(?[A-Za-z0-9.-]+)", text); + DTree tags = regex_search_all("#(?[A-Za-z0-9_]+)", text); + StringList pieces = regex_split("\\s*,\\s*", "uce, components, markdown"); + + <> + + +

+ UCE Test: + Regular Expressions +

+ +

UCE regex functions use PCRE2 and return ordinary UCE strings, lists, and DTree values.

+

sample =

+ +

Validation And Search

+
+ +

All Matches

+
+ +

Replace And Split

+ $1\", \"#uce\")", regex_replace("#([A-Za-z0-9_]+)", "$1", "#uce"), "uce"); + 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"); + ?>
+ +

Structured Result

+
+ +

Read the regex API documentation

+ +} diff --git a/site/doc/areas/regex.txt b/site/doc/areas/regex.txt new file mode 100644 index 0000000..c500660 --- /dev/null +++ b/site/doc/areas/regex.txt @@ -0,0 +1,7 @@ +Regular Expressions + +regex_match +regex_search +regex_search_all +regex_replace +regex_split diff --git a/site/doc/areas/string.txt b/site/doc/areas/string.txt index 0cc2ffe..5488cea 100644 --- a/site/doc/areas/string.txt +++ b/site/doc/areas/string.txt @@ -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 diff --git a/site/doc/areas/task.txt b/site/doc/areas/task.txt index 82809d0..054902b 100644 --- a/site/doc/areas/task.txt +++ b/site/doc/areas/task.txt @@ -1,6 +1,6 @@ Task API -kill task task_repeat task_pid +task_kill diff --git a/site/doc/areas/types.txt b/site/doc/areas/types.txt index 30a1964..68e230a 100644 --- a/site/doc/areas/types.txt +++ b/site/doc/areas/types.txt @@ -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 diff --git a/site/doc/index.uce b/site/doc/index.uce index 2331516..2af2856 100644 --- a/site/doc/index.uce +++ b/site/doc/index.uce @@ -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) == "

" && html.substr(html.length() - 4) == "

") - 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) + ?> 0 && content_lines.back() != "") - content_lines.push_back(""); - content_lines.push_back(heading); - content_lines.push_back(""); - } - continue; - } - - if(current_section == "title") - { - if(result.title != "") - result.title += "\n"; - result.title += line; - } - else if(current_section == "sig") - { - result.sig_lines.push_back(line); - } - else if(current_section == "params") - { - result.param_lines.push_back(line); - } - else if(current_section == "see") - { - if(trim(line) != "") - result.see_lines.push_back(trim(line)); - } - else - { - content_lines.push_back(line); - } + ?>()

    } - else if(line != "") + else { - <>
  • ()
  • + ?>
  • ') { - 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)) + { + ?>
    @@ -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, "_"); - ?> - - - - - - - -
diff --git a/site/doc/lib/doc_page.h b/site/doc/lib/doc_page.h new file mode 100644 index 0000000..c6ec0a7 --- /dev/null +++ b/site/doc/lib/doc_page.h @@ -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) == "

" && html.substr(html.length() - 4) == "

") + 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); +} diff --git a/site/doc/pages/0_Request.txt b/site/doc/pages/0_Request.txt index 9b1f0fe..3fa2665 100644 --- a/site/doc/pages/0_Request.txt +++ b/site/doc/pages/0_Request.txt @@ -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`. diff --git a/site/doc/pages/1_COMPONENT.txt b/site/doc/pages/1_COMPONENT.txt index 0e22964..6a4e945 100644 --- a/site/doc/pages/1_COMPONENT.txt +++ b/site/doc/pages/1_COMPONENT.txt @@ -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: diff --git a/site/doc/pages/1_INIT.txt b/site/doc/pages/1_INIT.txt new file mode 100644 index 0000000..89f21a5 --- /dev/null +++ b/site/doc/pages/1_INIT.txt @@ -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 cached_labels; + +INIT(Request& context) +{ + if(cached_labels.empty()) + cached_labels["ready"] = "Ready"; +} + +COMPONENT(Request& context) +{ + <> +

+ +} +``` + +## Related Concepts + +- PHP: opcode-cache preload or one-time bootstrap work per worker process +- JavaScript / Node.js: module-load initialization or lazy singleton setup diff --git a/site/doc/pages/1_ONCE.txt b/site/doc/pages/1_ONCE.txt new file mode 100644 index 0000000..1d23653 --- /dev/null +++ b/site/doc/pages/1_ONCE.txt @@ -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) +{ + <> +
"> + +
+ +} +``` + +## 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 diff --git a/site/doc/pages/1_RENDER.txt b/site/doc/pages/1_RENDER.txt index 4889ce3..61ebc83 100644 --- a/site/doc/pages/1_RENDER.txt +++ b/site/doc/pages/1_RENDER.txt @@ -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`. diff --git a/site/doc/pages/1_WS.txt b/site/doc/pages/1_WS.txt index 92ba960..5362c4d 100644 --- a/site/doc/pages/1_WS.txt +++ b/site/doc/pages/1_WS.txt @@ -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. diff --git a/site/doc/pages/3_C++ Preprocessor.txt b/site/doc/pages/3_C++ Preprocessor.txt index fa428e9..28a9c57 100644 --- a/site/doc/pages/3_C++ Preprocessor.txt +++ b/site/doc/pages/3_C++ Preprocessor.txt @@ -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, `` emits `print(html_escape(expression));`. - Inside a literal block, `` emits `print(expression);` without HTML escaping. - `#load "other.uce"` injects another UCE unit at compile time. -- `RENDER(Request& context)`, `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 `` 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 `` 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 diff --git a/site/doc/pages/String.txt b/site/doc/pages/String.txt index c78d51c..aaf9a4d 100644 --- a/site/doc/pages/String.txt +++ b/site/doc/pages/String.txt @@ -3,6 +3,10 @@ String :see >types +>string +split_utf8 +substr +html_escape :content Primary string type used throughout UCE. diff --git a/site/doc/pages/StringList.txt b/site/doc/pages/StringList.txt new file mode 100644 index 0000000..84b883a --- /dev/null +++ b/site/doc/pages/StringList.txt @@ -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`. + +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 diff --git a/site/doc/pages/StringMap.txt b/site/doc/pages/StringMap.txt index fab05cb..f77fcc8 100644 --- a/site/doc/pages/StringMap.txt +++ b/site/doc/pages/StringMap.txt @@ -3,6 +3,10 @@ StringMap :see >types +0_Request +parse_query +encode_query +array_merge :content Associative container mapping `String` keys to `String` values. diff --git a/site/doc/pages/array_merge.txt b/site/doc/pages/array_merge.txt index 035decc..3d4a064 100644 --- a/site/doc/pages/array_merge.txt +++ b/site/doc/pages/array_merge.txt @@ -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. diff --git a/site/doc/pages/component.txt b/site/doc/pages/component.txt index 349a080..541f7fe 100644 --- a/site/doc/pages/component.txt +++ b/site/doc/pages/component.txt @@ -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"; <> ``` +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) +{ + <> +
+ +
+ +} + +COMPONENT:BODY(Request& context) +{ + <> +

+ +} +``` + +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 +<> +
+ +
+ +``` + +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 diff --git a/site/doc/pages/component_exists.txt b/site/doc/pages/component_exists.txt index 968c59d..a584618 100644 --- a/site/doc/pages/component_exists.txt +++ b/site/doc/pages/component_exists.txt @@ -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. diff --git a/site/doc/pages/component_render.txt b/site/doc/pages/component_render.txt index dfc06ca..20d5908 100644 --- a/site/doc/pages/component_render.txt +++ b/site/doc/pages/component_render.txt @@ -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 diff --git a/site/doc/pages/component_resolve.txt b/site/doc/pages/component_resolve.txt index 32bad2f..a5798f4 100644 --- a/site/doc/pages/component_resolve.txt +++ b/site/doc/pages/component_resolve.txt @@ -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. diff --git a/site/doc/pages/float_val.txt b/site/doc/pages/float_val.txt index d644650..bdacb8d 100644 --- a/site/doc/pages/float_val.txt +++ b/site/doc/pages/float_val.txt @@ -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`. diff --git a/site/doc/pages/get_by_path.txt b/site/doc/pages/get_by_path.txt index ad7d982..72e4454 100644 --- a/site/doc/pages/get_by_path.txt +++ b/site/doc/pages/get_by_path.txt @@ -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. diff --git a/site/doc/pages/html_escape.txt b/site/doc/pages/html_escape.txt index e077867..92922a1 100644 --- a/site/doc/pages/html_escape.txt +++ b/site/doc/pages/html_escape.txt @@ -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: diff --git a/site/doc/pages/int_val.txt b/site/doc/pages/int_val.txt index 1defa69..094e94e 100644 --- a/site/doc/pages/int_val.txt +++ b/site/doc/pages/int_val.txt @@ -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`. diff --git a/site/doc/pages/json_decode.txt b/site/doc/pages/json_decode.txt index f4c3f91..0de4867 100644 --- a/site/doc/pages/json_decode.txt +++ b/site/doc/pages/json_decode.txt @@ -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 diff --git a/site/doc/pages/json_encode.txt b/site/doc/pages/json_encode.txt index 30bd807..593830a 100644 --- a/site/doc/pages/json_encode.txt +++ b/site/doc/pages/json_encode.txt @@ -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. diff --git a/site/doc/pages/markdown_to_ast.txt b/site/doc/pages/markdown_to_ast.txt index c318d82..48dffc6 100644 --- a/site/doc/pages/markdown_to_ast.txt +++ b/site/doc/pages/markdown_to_ast.txt @@ -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. diff --git a/site/doc/pages/redirect.txt b/site/doc/pages/redirect.txt index 5bb958a..2f9f436 100644 --- a/site/doc/pages/redirect.txt +++ b/site/doc/pages/redirect.txt @@ -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. diff --git a/site/doc/pages/regex_match.txt b/site/doc/pages/regex_match.txt new file mode 100644 index 0000000..30751b7 --- /dev/null +++ b/site/doc/pages/regex_match.txt @@ -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. diff --git a/site/doc/pages/regex_replace.txt b/site/doc/pages/regex_replace.txt new file mode 100644 index 0000000..0aace60 --- /dev/null +++ b/site/doc/pages/regex_replace.txt @@ -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_]+)", + "@$1", + "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. diff --git a/site/doc/pages/regex_search.txt b/site/doc/pages/regex_search.txt new file mode 100644 index 0000000..2d3e0b3 --- /dev/null +++ b/site/doc/pages/regex_search.txt @@ -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( + "(?[A-Za-z0-9._%+-]+)@(?[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. diff --git a/site/doc/pages/regex_search_all.txt b/site/doc/pages/regex_search_all.txt new file mode 100644 index 0000000..2adfb00 --- /dev/null +++ b/site/doc/pages/regex_search_all.txt @@ -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("#(?[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. diff --git a/site/doc/pages/regex_split.txt b/site/doc/pages/regex_split.txt new file mode 100644 index 0000000..77c4957 --- /dev/null +++ b/site/doc/pages/regex_split.txt @@ -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. diff --git a/site/doc/pages/set_status.txt b/site/doc/pages/set_status.txt index ed8711a..e7623a4 100644 --- a/site/doc/pages/set_status.txt +++ b/site/doc/pages/set_status.txt @@ -6,7 +6,7 @@ code : HTTP status code reason : optional reason phrase override :see -0_context +0_Request >types :content diff --git a/site/doc/pages/task_kill.txt b/site/doc/pages/task_kill.txt index abbd70e..a785f78 100644 --- a/site/doc/pages/task_kill.txt +++ b/site/doc/pages/task_kill.txt @@ -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. diff --git a/site/doc/pages/to_bool.txt b/site/doc/pages/to_bool.txt new file mode 100644 index 0000000..c31772f --- /dev/null +++ b/site/doc/pages/to_bool.txt @@ -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. diff --git a/site/doc/pages/to_f64.txt b/site/doc/pages/to_f64.txt new file mode 100644 index 0000000..5c98d36 --- /dev/null +++ b/site/doc/pages/to_f64.txt @@ -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. diff --git a/site/doc/pages/to_u64.txt b/site/doc/pages/to_u64.txt new file mode 100644 index 0000000..bcf80df --- /dev/null +++ b/site/doc/pages/to_u64.txt @@ -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`. diff --git a/site/doc/pages/unit_call.txt b/site/doc/pages/unit_call.txt index 556e6a8..62063e2 100644 --- a/site/doc/pages/unit_call.txt +++ b/site/doc/pages/unit_call.txt @@ -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 diff --git a/site/doc/pages/unit_compile.txt b/site/doc/pages/unit_compile.txt index 9d3cd84..5b33071 100644 --- a/site/doc/pages/unit_compile.txt +++ b/site/doc/pages/unit_compile.txt @@ -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. diff --git a/site/doc/pages/unit_info.txt b/site/doc/pages/unit_info.txt index aa6d154..4963902 100644 --- a/site/doc/pages/unit_info.txt +++ b/site/doc/pages/unit_info.txt @@ -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. diff --git a/site/doc/pages/unit_load.txt b/site/doc/pages/unit_load.txt index 6a6a083..6278025 100644 --- a/site/doc/pages/unit_load.txt +++ b/site/doc/pages/unit_load.txt @@ -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. diff --git a/site/doc/pages/unit_render.txt b/site/doc/pages/unit_render.txt index 6fc717e..d4a4532 100644 --- a/site/doc/pages/unit_render.txt +++ b/site/doc/pages/unit_render.txt @@ -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. diff --git a/site/doc/pages/units_list.txt b/site/doc/pages/units_list.txt index 0b57d8b..968b9b0 100644 --- a/site/doc/pages/units_list.txt +++ b/site/doc/pages/units_list.txt @@ -2,8 +2,10 @@ std::vector units_list() :see +>runtime unit_info unit_compile +unit_load :content Returns the normalized paths of all known `.uce` units. diff --git a/site/doc/pages/var_dump.txt b/site/doc/pages/var_dump.txt index 9b1b769..5758268 100644 --- a/site/doc/pages/var_dump.txt +++ b/site/doc/pages/var_dump.txt @@ -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. diff --git a/site/doc/singlepage.uce b/site/doc/singlepage.uce index 8a57252..8bc2df7 100644 --- a/site/doc/singlepage.uce +++ b/site/doc/singlepage.uce @@ -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) == "

" && html.substr(html.length() - 4) == "

") - 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) diff --git a/site/examples/uce-starter/components/example/theme-switcher.uce b/site/examples/uce-starter/components/example/theme-switcher.uce index 28f7de7..d33ffeb 100644 --- a/site/examples/uce-starter/components/example/theme-switcher.uce +++ b/site/examples/uce-starter/components/example/theme-switcher.uce @@ -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(); <>
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

-

+

} 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"; <> - 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."); + ?>
+ 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

+ +
+ (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)