decided in favor of dedicated COMPONENT() macro, updates to documentation
This commit is contained in:
parent
be514d63d6
commit
2b5586d7df
17
README.md
17
README.md
@ -60,14 +60,23 @@ UCE pages now use explicit request handlers instead of implicit globals:
|
|||||||
Useful related runtime patterns:
|
Useful related runtime patterns:
|
||||||
|
|
||||||
- `render_file(String file_name)` or `render_file(String file_name, Request& context)` to invoke another page
|
- `render_file(String file_name)` or `render_file(String file_name, Request& context)` to invoke another page
|
||||||
|
- `context.cfg` for request-local structured configuration
|
||||||
- `context.call` for invocation or message-local structured input
|
- `context.call` for invocation or message-local structured input
|
||||||
- `context.connection` for broker-owned per-WebSocket-connection state shared across `WS(Request& context)` calls
|
- `context.connection` for broker-owned per-WebSocket-connection state shared across `WS(Request& context)` calls
|
||||||
- `context.params`, `context.get`, `context.post`, `context.cookies`, `context.session`, and `context.header` for request/response state
|
- `context.params`, `context.get`, `context.post`, `context.cookies`, `context.session`, and `context.header` for request/response state
|
||||||
|
- `context.set_status(code[, reason])` to set the HTTP response status
|
||||||
|
|
||||||
Named component-style render handlers are also supported:
|
Useful helpers for that data model now include:
|
||||||
|
|
||||||
|
- `DTree::get_by_path("a/b/c")` for path-style config traversal without creating missing keys
|
||||||
|
- `json_encode(String)` for emitting JavaScript-safe string literals directly
|
||||||
|
- `ascii_safe_name(String)` for conservative ASCII identifier normalization
|
||||||
|
- `path_join(base, child)` for filesystem-style path assembly
|
||||||
|
|
||||||
|
Named component handlers are also supported:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
RENDER:BODY(Request& context)
|
COMPONENT:BODY(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<p><?= context.call["body"].to_string() ?></p>
|
<p><?= context.call["body"].to_string() ?></p>
|
||||||
@ -114,7 +123,9 @@ When you want returned component markup inside a literal block, prefer:
|
|||||||
|
|
||||||
because `<?= ... ?>` HTML-escapes the returned markup. For direct output from C++ code, use `render_component(...)`.
|
because `<?= ... ?>` HTML-escapes the returned markup. For direct output from C++ code, use `render_component(...)`.
|
||||||
|
|
||||||
Components may expose additional named handlers with `RENDER:NAME(Request& context)`. The default handler remains the ordinary `RENDER(Request& context)`.
|
Components expose `COMPONENT(Request& context)` as their default entrypoint and may expose additional named handlers with `COMPONENT:NAME(Request& context)`.
|
||||||
|
|
||||||
|
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 `render_component(":NAME", props, context)` target another named component handler in that same file.
|
||||||
|
|
||||||
## WebSockets
|
## WebSockets
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
String Functions
|
String Functions
|
||||||
|
|
||||||
|
ascii_safe_name
|
||||||
concat
|
concat
|
||||||
filter
|
filter
|
||||||
first
|
first
|
||||||
join
|
join
|
||||||
|
json_encode
|
||||||
nibble
|
nibble
|
||||||
print
|
print
|
||||||
split
|
split
|
||||||
|
|||||||
@ -11,6 +11,7 @@ file_put_contents
|
|||||||
get_cwd
|
get_cwd
|
||||||
ls
|
ls
|
||||||
mkdir
|
mkdir
|
||||||
|
path_join
|
||||||
set_cwd
|
set_cwd
|
||||||
shell_escape
|
shell_escape
|
||||||
shell_exec
|
shell_exec
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
Types
|
Types
|
||||||
|
|
||||||
DTree
|
DTree
|
||||||
|
get_by_path
|
||||||
|
set_status
|
||||||
String
|
String
|
||||||
StringMap
|
StringMap
|
||||||
|
|||||||
@ -28,6 +28,9 @@ Name of the session cookie
|
|||||||
:DTree var
|
:DTree var
|
||||||
Variable user-defined data
|
Variable user-defined data
|
||||||
|
|
||||||
|
:DTree cfg
|
||||||
|
Request-local configuration tree. Apps can keep structured configuration here and traverse it with `context.cfg.get_by_path("path/to/value")`.
|
||||||
|
|
||||||
:DTree call
|
:DTree call
|
||||||
Invocation or message-local structured data
|
Invocation or message-local structured data
|
||||||
|
|
||||||
@ -43,6 +46,9 @@ Headers to be sent back to the browser
|
|||||||
:StringList set_cookies;
|
:StringList set_cookies;
|
||||||
Cookies that should be sent back to the browser
|
Cookies that should be sent back to the browser
|
||||||
|
|
||||||
|
:context.set_status(s32 code[, String reason])
|
||||||
|
Sets the HTTP status line and updates `context.flags.status`. When `reason` is omitted, UCE uses a built-in standard reason phrase for common status codes.
|
||||||
|
|
||||||
:u64 random_seed
|
:u64 random_seed
|
||||||
The current request's "random" noise generator seed
|
The current request's "random" noise generator seed
|
||||||
|
|
||||||
@ -64,4 +70,3 @@ Invokes another UCE file using the current or supplied request context
|
|||||||
:see
|
:see
|
||||||
>types
|
>types
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
36
site/doc/pages/1_COMPONENT.txt
Normal file
36
site/doc/pages/1_COMPONENT.txt
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
:sig
|
||||||
|
COMPONENT(Request& context)
|
||||||
|
|
||||||
|
:desc
|
||||||
|
Defines the default component entrypoint for the current `.uce` file.
|
||||||
|
|
||||||
|
`component()` and `render_component()` invoke `COMPONENT(Request& context)` by default. Named component entrypoints use `COMPONENT:NAME(Request& context)`.
|
||||||
|
|
||||||
|
This keeps page rendering and component rendering separate:
|
||||||
|
|
||||||
|
- direct HTTP requests call `RENDER(Request& context)`
|
||||||
|
- WebSocket messages call `WS(Request& context)`
|
||||||
|
- component helpers call `COMPONENT(Request& context)` or `COMPONENT:NAME(Request& context)`
|
||||||
|
|
||||||
|
A file intended only for component reuse can define `COMPONENT()` without defining `RENDER()`.
|
||||||
|
|
||||||
|
Inside component handlers, props arrive through `context.call`.
|
||||||
|
|
||||||
|
When you call `component(":NAME", props, context)` or `render_component(":NAME", props, context)`, the runtime resolves `:NAME` against the current `.uce` file instead of requiring the file name again.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
`COMPONENT(Request& context)`
|
||||||
|
`{`
|
||||||
|
` <><section><?: component(":BODY", context.call, context) ?></section></>`
|
||||||
|
`}`
|
||||||
|
|
||||||
|
`COMPONENT:BODY(Request& context)`
|
||||||
|
`{`
|
||||||
|
` <><p><?= context.call["body"] ?></p></>`
|
||||||
|
`}`
|
||||||
|
|
||||||
|
:see
|
||||||
|
>component
|
||||||
|
>render_component
|
||||||
|
>1_RENDER
|
||||||
|
>1_WS
|
||||||
@ -6,19 +6,17 @@ Defines the main HTTP render handler for the current `.uce` page.
|
|||||||
|
|
||||||
When a page is requested over HTTP, the runtime loads the target file and calls its `RENDER(Request& context)` function.
|
When a page is requested over HTTP, the runtime loads the target file and calls its `RENDER(Request& context)` function.
|
||||||
|
|
||||||
Pages may also export additional named render handlers with `RENDER:NAME(Request& context)`.
|
|
||||||
|
|
||||||
Named render handlers are not used for the page's direct HTTP entrypoint. They are intended for component-style sub-rendering through helpers such as `component("components/card:BODY", props, context)` or `render_component("components/card:BODY", props, context)`.
|
|
||||||
|
|
||||||
The default page entrypoint is always the plain `RENDER(Request& context)` handler.
|
The default page entrypoint is always the plain `RENDER(Request& context)` handler.
|
||||||
|
|
||||||
|
Reusable component handlers now live on `COMPONENT(Request& context)` and `COMPONENT:NAME(Request& context)`. The component helpers call those handlers, not `RENDER()`.
|
||||||
|
|
||||||
The request environment is passed explicitly through `context`, including params, cookies, post data, session state, headers, uploaded files, and the current `context.call` tree.
|
The request environment is passed explicitly through `context`, including params, cookies, post data, session state, headers, uploaded files, and the current `context.call` tree.
|
||||||
|
|
||||||
For a normal direct page request, `context.call` starts empty.
|
For a normal direct page request, `context.call` starts empty.
|
||||||
|
|
||||||
If the page is invoked from another UCE file via `render_file(file_name, context)`, the callee receives that same `context`.
|
If the page is invoked from another UCE file via `render_file(file_name, context)`, the callee receives that same `context`.
|
||||||
|
|
||||||
Pages intended to serve WebSocket traffic may expose both `RENDER(Request& context)` and `WS(Request& context)`. In that case `RENDER(Request& context)` serves the initial HTTP response and `WS(Request& context)` handles subsequent WebSocket messages.
|
Pages intended to serve WebSocket traffic may expose both `RENDER(Request& context)` and `WS(Request& context)`. Files may also define `COMPONENT()` handlers when they intentionally need both page and component behavior in one unit. In that case `RENDER(Request& context)` serves the direct HTTP response, `WS(Request& context)` handles subsequent WebSocket messages, and `COMPONENT()` remains available only through the component helpers.
|
||||||
|
|
||||||
:see
|
:see
|
||||||
>ob
|
>ob
|
||||||
|
|||||||
@ -12,7 +12,8 @@ The implementation lives in `src/lib/compiler.cpp`. It does not try to parse all
|
|||||||
- Inside a literal block, `<?= expression ?>` emits `print(html_escape(expression));`.
|
- Inside a literal block, `<?= expression ?>` emits `print(html_escape(expression));`.
|
||||||
- Inside a literal block, `<?: expression ?>` emits `print(expression);` without HTML escaping.
|
- Inside a literal block, `<?: expression ?>` emits `print(expression);` without HTML escaping.
|
||||||
- `#load "other.uce"` injects another UCE unit at compile time.
|
- `#load "other.uce"` injects another UCE unit at compile time.
|
||||||
- `RENDER(Request& context)` and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`.
|
- `RENDER(Request& context)`, `COMPONENT(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.
|
- `EXPORT` is also a normal C++ macro, but the custom pass additionally records exported declarations for metadata.
|
||||||
|
|
||||||
:Pipeline
|
:Pipeline
|
||||||
@ -25,6 +26,7 @@ The implementation lives in `src/lib/compiler.cpp`. It does not try to parse all
|
|||||||
- `<?: ... ?>` becomes `print(...);` and is intended for trusted markup or already-escaped content.
|
- `<?: ... ?>` becomes `print(...);` and is intended for trusted markup or already-escaped content.
|
||||||
- `#load "file.uce"` is replaced with a generated C++ `#include` that points at the loaded unit's preprocessed `.cpp` file under `BIN_DIRECTORY`.
|
- `#load "file.uce"` is replaced with a generated C++ `#include` that points at the loaded unit's preprocessed `.cpp` file under `BIN_DIRECTORY`.
|
||||||
- Lines beginning with `EXPORT` are scanned so their declarations can be written to a sibling `.exports.txt` file.
|
- Lines beginning with `EXPORT` are scanned so their declarations can be written to a sibling `.exports.txt` file.
|
||||||
|
- Lines beginning with `COMPONENT:NAME(...)` are rewritten into exported `component_render_NAME(...)` functions for the component helpers.
|
||||||
- The final generated source is written to `BIN_DIRECTORY + src_path + "/" + source_file + ".cpp"`.
|
- The final generated source is written to `BIN_DIRECTORY + src_path + "/" + source_file + ".cpp"`.
|
||||||
- `scripts/compile` then compiles that generated `.cpp` into `source_file + ".so"` with `clang++ -shared -std=c++20 ...`.
|
- `scripts/compile` then compiles that generated `.cpp` into `source_file + ".so"` with `clang++ -shared -std=c++20 ...`.
|
||||||
|
|
||||||
@ -93,3 +95,4 @@ load
|
|||||||
render_file
|
render_file
|
||||||
call_file
|
call_file
|
||||||
0_context
|
0_context
|
||||||
|
1_COMPONENT
|
||||||
|
|||||||
@ -18,6 +18,7 @@ Useful methods include:
|
|||||||
`to_string()`
|
`to_string()`
|
||||||
`to_json()`
|
`to_json()`
|
||||||
`get_type_name()`
|
`get_type_name()`
|
||||||
|
`get_by_path()`
|
||||||
`set_bool()`
|
`set_bool()`
|
||||||
`remove()`
|
`remove()`
|
||||||
`clear()`
|
`clear()`
|
||||||
|
|||||||
14
site/doc/pages/ascii_safe_name.txt
Normal file
14
site/doc/pages/ascii_safe_name.txt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
:sig
|
||||||
|
String ascii_safe_name(String raw)
|
||||||
|
|
||||||
|
:params
|
||||||
|
raw : input string to normalize
|
||||||
|
return value : ASCII-safe identifier made from letters, digits, and underscores
|
||||||
|
|
||||||
|
:desc
|
||||||
|
Builds a conservative identifier by keeping ASCII letters, digits, and underscores and dropping other characters.
|
||||||
|
|
||||||
|
This is useful when turning user- or config-provided names into handler suffixes, DOM-safe variable stems, or CSS/JS hook names.
|
||||||
|
|
||||||
|
:see
|
||||||
|
>string
|
||||||
@ -10,9 +10,11 @@ Component props are passed in `context.call`.
|
|||||||
|
|
||||||
Because `<?= ... ?>` HTML-escapes its value, embed component markup with `<?: component(...) ?>`, `print(component(...))`, or use `render_component(...)` for direct output.
|
Because `<?= ... ?>` HTML-escapes its value, embed component markup with `<?: component(...) ?>`, `print(component(...))`, or use `render_component(...)` for direct output.
|
||||||
|
|
||||||
When `name` contains a colon, such as `components/card:BODY`, the part after the colon selects a named render handler exported from the component file through `RENDER:BODY(Request& context)`.
|
When `name` contains a colon, such as `components/card:BODY`, the part after the colon selects a named component handler exported from the component file through `COMPONENT:BODY(Request& context)`.
|
||||||
|
|
||||||
The default handler is `RENDER(Request& context)`.
|
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.
|
||||||
|
|
||||||
Resolution order is:
|
Resolution order is:
|
||||||
exact file name
|
exact file name
|
||||||
|
|||||||
20
site/doc/pages/get_by_path.txt
Normal file
20
site/doc/pages/get_by_path.txt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
:sig
|
||||||
|
DTree DTree::get_by_path(String path, String delim = "/")
|
||||||
|
|
||||||
|
:params
|
||||||
|
path : slash-delimited path to traverse
|
||||||
|
delim : optional path separator
|
||||||
|
return value : the resolved child node, or an empty `DTree` when the path cannot be followed
|
||||||
|
|
||||||
|
:desc
|
||||||
|
Traverses a nested `DTree` without creating missing keys.
|
||||||
|
|
||||||
|
Empty path segments are ignored, so leading and trailing `/` characters are harmless. If any intermediate node is not a map or a segment is missing, `get_by_path()` returns an empty `DTree`.
|
||||||
|
|
||||||
|
Typical usage:
|
||||||
|
`context.cfg.get_by_path("theme/options/portal-dark/label").to_string()`
|
||||||
|
|
||||||
|
:see
|
||||||
|
DTree
|
||||||
|
0_context
|
||||||
|
>types
|
||||||
@ -1,10 +1,15 @@
|
|||||||
:sig
|
:sig
|
||||||
|
String json_encode(String s)
|
||||||
String json_encode(DTree t)
|
String json_encode(DTree t)
|
||||||
|
|
||||||
:params
|
:params
|
||||||
|
s : string to encode as a JSON string literal
|
||||||
t : DTree object to be serialized
|
t : DTree object to be serialized
|
||||||
return value : string containing the JSON result
|
return value : string containing the JSON result
|
||||||
|
|
||||||
:desc
|
:desc
|
||||||
Serializes a DTree structure 't' into a String in JSON notation.
|
Serializes either a `String` or a `DTree` into JSON notation.
|
||||||
|
|
||||||
|
When passed a `String`, `json_encode()` returns a quoted and escaped JSON string literal.
|
||||||
|
|
||||||
|
When passed a `DTree`, scalar values are serialized directly and nested map values are emitted as JSON objects.
|
||||||
|
|||||||
15
site/doc/pages/path_join.txt
Normal file
15
site/doc/pages/path_join.txt
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
:sig
|
||||||
|
String path_join(String base, String child)
|
||||||
|
|
||||||
|
:params
|
||||||
|
base : parent path
|
||||||
|
child : child path or absolute override
|
||||||
|
return value : combined path
|
||||||
|
|
||||||
|
:desc
|
||||||
|
Joins two filesystem-style path fragments with a single `/` when needed.
|
||||||
|
|
||||||
|
If `child` is empty, `base` is returned. If `child` already starts with `/`, it is returned unchanged. This makes `path_join()` a better fit for app-level path assembly than open-coded string concatenation.
|
||||||
|
|
||||||
|
:see
|
||||||
|
>sys
|
||||||
@ -6,7 +6,9 @@ Renders another `.uce` file as a component and writes the result directly to the
|
|||||||
|
|
||||||
This is the direct-output counterpart to `component()`.
|
This is the direct-output counterpart to `component()`.
|
||||||
|
|
||||||
Component props are passed through `context.call`, and `name:RENDERFUNC` may be used to select a named handler exported by `RENDER:RENDERFUNC(Request& context)`.
|
Component props are passed through `context.call`, and `name:COMPONENTFUNC` may be used to select a named handler exported by `COMPONENT:COMPONENTFUNC(Request& context)`.
|
||||||
|
|
||||||
|
When `name` starts with `:`, the runtime resolves that named handler against the current `.uce` file.
|
||||||
|
|
||||||
Use `render_component()` when you want to write component output directly from C++ code instead of capturing it as a `String`.
|
Use `render_component()` when you want to write component output directly from C++ code instead of capturing it as a `String`.
|
||||||
|
|
||||||
|
|||||||
15
site/doc/pages/set_status.txt
Normal file
15
site/doc/pages/set_status.txt
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
:sig
|
||||||
|
void context.set_status(s32 code, String reason = "")
|
||||||
|
|
||||||
|
:params
|
||||||
|
code : HTTP status code
|
||||||
|
reason : optional reason phrase override
|
||||||
|
|
||||||
|
:desc
|
||||||
|
Sets the current request status line and mirrors the numeric status into `context.flags.status`.
|
||||||
|
|
||||||
|
When `reason` is omitted, UCE fills in a standard reason phrase for common HTTP status codes such as `200`, `302`, `400`, `404`, and `500`.
|
||||||
|
|
||||||
|
:see
|
||||||
|
0_context
|
||||||
|
>types
|
||||||
@ -1,6 +1,6 @@
|
|||||||
#load "../../lib/app.uce"
|
#load "../../lib/app.uce"
|
||||||
|
|
||||||
RENDER(Request& context)
|
COMPONENT(Request& context)
|
||||||
{
|
{
|
||||||
String title = first(context.call["title"].to_string(), "Sign in with OAuth");
|
String title = first(context.call["title"].to_string(), "Sign in with OAuth");
|
||||||
String subtitle = first(context.call["subtitle"].to_string(), "Choose your preferred authentication method");
|
String subtitle = first(context.call["subtitle"].to_string(), "Choose your preferred authentication method");
|
||||||
@ -42,7 +42,7 @@ RENDER(Request& context)
|
|||||||
</div>
|
</div>
|
||||||
<? services.each([&](DTree service, String service_key) {
|
<? services.each([&](DTree service, String service_key) {
|
||||||
String client_id = context.call[service_key + "_client_id"].to_string();
|
String client_id = context.call[service_key + "_client_id"].to_string();
|
||||||
String handler_name = starter_safe_name(service_key);
|
String handler_name = ascii_safe_name(service_key);
|
||||||
String service_name = service["name"].to_string();
|
String service_name = service["name"].to_string();
|
||||||
String scope = service["scope"].to_string();
|
String scope = service["scope"].to_string();
|
||||||
String auth_url = service["auth_url"].to_string();
|
String auth_url = service["auth_url"].to_string();
|
||||||
@ -63,7 +63,7 @@ RENDER(Request& context)
|
|||||||
if (button) button.style.display = 'none';
|
if (button) button.style.display = 'none';
|
||||||
if (loading) loading.style.display = 'block';
|
if (loading) loading.style.display = 'block';
|
||||||
|
|
||||||
const clientId = <?: starter_json_string(client_id) ?>;
|
const clientId = <?: json_encode(client_id) ?>;
|
||||||
if (!clientId || clientId.indexOf('YOUR_') === 0) {
|
if (!clientId || clientId.indexOf('YOUR_') === 0) {
|
||||||
alert('<?= service_name ?> OAuth is not configured yet.');
|
alert('<?= service_name ?> OAuth is not configured yet.');
|
||||||
if (button) button.style.display = '';
|
if (button) button.style.display = '';
|
||||||
@ -74,21 +74,21 @@ RENDER(Request& context)
|
|||||||
const state = btoa(Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2)).replace(/[^a-zA-Z0-9]/g, '');
|
const state = btoa(Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2)).replace(/[^a-zA-Z0-9]/g, '');
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
redirect_uri: <?: starter_json_string(callback_url) ?>,
|
redirect_uri: <?: json_encode(callback_url) ?>,
|
||||||
scope: <?: starter_json_string(scope) ?>,
|
scope: <?: json_encode(scope) ?>,
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
state: state
|
state: state
|
||||||
});
|
});
|
||||||
|
|
||||||
sessionStorage.setItem('oauth_state', state);
|
sessionStorage.setItem('oauth_state', state);
|
||||||
sessionStorage.setItem('oauth_service', <?: starter_json_string(service_key) ?>);
|
sessionStorage.setItem('oauth_service', <?: json_encode(service_key) ?>);
|
||||||
|
|
||||||
fetch(<?: starter_json_string(store_url) ?>, {
|
fetch(<?: json_encode(store_url) ?>, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({oauth_service: <?: starter_json_string(service_key) ?>, oauth_state: state})
|
body: JSON.stringify({oauth_service: <?: json_encode(service_key) ?>, oauth_state: state})
|
||||||
}).catch(console.error).finally(() => {
|
}).catch(console.error).finally(() => {
|
||||||
window.location.href = <?: starter_json_string(auth_url) ?> + '?' + params.toString();
|
window.location.href = <?: json_encode(auth_url) ?> + '?' + params.toString();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#load "../../lib/app.uce"
|
#load "../../lib/app.uce"
|
||||||
|
|
||||||
RENDER(Request& context)
|
COMPONENT(Request& context)
|
||||||
{
|
{
|
||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
|
|
||||||
|
|||||||
@ -74,7 +74,7 @@ DTree data_format_value(DTree value, DTree row, DTree column)
|
|||||||
return(result);
|
return(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:SUMMARY_METRICS(Request& context)
|
COMPONENT:SUMMARY_METRICS(Request& context)
|
||||||
{
|
{
|
||||||
String title = context.call["title"].to_string();
|
String title = context.call["title"].to_string();
|
||||||
String subtitle = context.call["subtitle"].to_string();
|
String subtitle = context.call["subtitle"].to_string();
|
||||||
@ -103,7 +103,7 @@ RENDER:SUMMARY_METRICS(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:TIMESERIES_CHART(Request& context)
|
COMPONENT:TIMESERIES_CHART(Request& context)
|
||||||
{
|
{
|
||||||
starter_register_js("js/u-format.js", context);
|
starter_register_js("js/u-format.js", context);
|
||||||
starter_register_js("js/u-timeseries-chart.js", context);
|
starter_register_js("js/u-timeseries-chart.js", context);
|
||||||
@ -132,21 +132,21 @@ RENDER:TIMESERIES_CHART(Request& context)
|
|||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
if (typeof UTimeSeriesChart === 'undefined') return;
|
if (typeof UTimeSeriesChart === 'undefined') return;
|
||||||
var chart = new UTimeSeriesChart(<?: starter_json_string(canvas_id) ?>, {
|
var chart = new UTimeSeriesChart(<?: json_encode(canvas_id) ?>, {
|
||||||
xAxisLabel: <?: starter_json_string(x_axis_label) ?>,
|
xAxisLabel: <?: json_encode(x_axis_label) ?>,
|
||||||
yAxisLeftLabel: <?: starter_json_string(y_axis_left_label) ?>,
|
yAxisLeftLabel: <?: json_encode(y_axis_left_label) ?>,
|
||||||
yAxisRightLabel: <?: starter_json_string(y_axis_right_label) ?>,
|
yAxisRightLabel: <?: json_encode(y_axis_right_label) ?>,
|
||||||
yAxisLeftFormat: <?: starter_json_string(y_axis_left_format) ?>,
|
yAxisLeftFormat: <?: json_encode(y_axis_left_format) ?>,
|
||||||
yAxisRightFormat: <?: starter_json_string(y_axis_right_format) ?>
|
yAxisRightFormat: <?: json_encode(y_axis_right_format) ?>
|
||||||
});
|
});
|
||||||
chart.setData(<?= json_encode(context.call["series"]) ?>, <?= json_encode(context.call["x_labels"]) ?>);
|
chart.setData(<?= json_encode(context.call["series"]) ?>, <?= json_encode(context.call["x_labels"]) ?>);
|
||||||
window[<?: starter_json_string("chart_" + chart_id) ?>] = chart;
|
window[<?: json_encode("chart_" + chart_id) ?>] = chart;
|
||||||
}());
|
}());
|
||||||
</script>
|
</script>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:SORTABLE_TABLE(Request& context)
|
COMPONENT:SORTABLE_TABLE(Request& context)
|
||||||
{
|
{
|
||||||
starter_register_js("js/u-format.js", context);
|
starter_register_js("js/u-format.js", context);
|
||||||
starter_register_js("js/u-sortable-table.js", context);
|
starter_register_js("js/u-sortable-table.js", context);
|
||||||
@ -205,13 +205,13 @@ RENDER:SORTABLE_TABLE(Request& context)
|
|||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
if (typeof USortableTable === 'undefined') return;
|
if (typeof USortableTable === 'undefined') return;
|
||||||
USortableTable.init(<?: starter_json_string(table_id) ?>, <?= json_encode(init_options) ?>);
|
USortableTable.init(<?: json_encode(table_id) ?>, <?= json_encode(init_options) ?>);
|
||||||
}());
|
}());
|
||||||
</script>
|
</script>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:DATA_TABLE(Request& context)
|
COMPONENT:DATA_TABLE(Request& context)
|
||||||
{
|
{
|
||||||
String table_id = first(context.call["id"].to_string(), "data-grid-" + std::to_string((u64)time()));
|
String table_id = first(context.call["id"].to_string(), "data-grid-" + std::to_string((u64)time()));
|
||||||
starter_register_js("js/ag-grid/ag-grid-community.min.js", context);
|
starter_register_js("js/ag-grid/ag-grid-community.min.js", context);
|
||||||
@ -259,15 +259,15 @@ RENDER:DATA_TABLE(Request& context)
|
|||||||
(function () {
|
(function () {
|
||||||
if (typeof agGrid === 'undefined') return;
|
if (typeof agGrid === 'undefined') return;
|
||||||
const gridOptions = <?= json_encode(grid_options) ?>;
|
const gridOptions = <?= json_encode(grid_options) ?>;
|
||||||
new agGrid.Grid(document.getElementById(<?: starter_json_string(table_id) ?>), gridOptions);
|
new agGrid.Grid(document.getElementById(<?: json_encode(table_id) ?>), gridOptions);
|
||||||
document.getElementById(<?: starter_json_string(table_id + "-search") ?>)?.addEventListener('input', function () {
|
document.getElementById(<?: json_encode(table_id + "-search") ?>)?.addEventListener('input', function () {
|
||||||
gridOptions.api.setQuickFilter(this.value);
|
gridOptions.api.setQuickFilter(this.value);
|
||||||
});
|
});
|
||||||
document.getElementById(<?: starter_json_string(table_id + "-clear-filters") ?>)?.addEventListener('click', function () {
|
document.getElementById(<?: json_encode(table_id + "-clear-filters") ?>)?.addEventListener('click', function () {
|
||||||
gridOptions.api.setFilterModel(null);
|
gridOptions.api.setFilterModel(null);
|
||||||
gridOptions.api.setQuickFilter('');
|
gridOptions.api.setQuickFilter('');
|
||||||
});
|
});
|
||||||
document.getElementById(<?: starter_json_string(table_id + "-export-csv") ?>)?.addEventListener('click', function () {
|
document.getElementById(<?: json_encode(table_id + "-export-csv") ?>)?.addEventListener('click', function () {
|
||||||
gridOptions.api.exportDataAsCsv();
|
gridOptions.api.exportDataAsCsv();
|
||||||
});
|
});
|
||||||
}());
|
}());
|
||||||
|
|||||||
@ -42,7 +42,7 @@ void marketing_default_features(DTree& features)
|
|||||||
features.push(item);
|
features.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:HERO_SECTION(Request& context)
|
COMPONENT:HERO_SECTION(Request& context)
|
||||||
{
|
{
|
||||||
String title = first(context.call["title"].to_string(), "Welcome to the Present");
|
String title = first(context.call["title"].to_string(), "Welcome to the Present");
|
||||||
String subtitle = first(context.call["subtitle"].to_string(), "Experience no-quite-modern web development with our cutting-edge framework");
|
String subtitle = first(context.call["subtitle"].to_string(), "Experience no-quite-modern web development with our cutting-edge framework");
|
||||||
@ -192,7 +192,7 @@ RENDER:HERO_SECTION(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:FEATURES_GRID(Request& context)
|
COMPONENT:FEATURES_GRID(Request& context)
|
||||||
{
|
{
|
||||||
DTree features = context.call["features"];
|
DTree features = context.call["features"];
|
||||||
marketing_default_features(features);
|
marketing_default_features(features);
|
||||||
@ -286,7 +286,7 @@ RENDER:FEATURES_GRID(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:STATS_SECTION(Request& context)
|
COMPONENT:STATS_SECTION(Request& context)
|
||||||
{
|
{
|
||||||
DTree stats = context.call["stats"];
|
DTree stats = context.call["stats"];
|
||||||
if(stats.get_type_name() != "array" || stats["0"]["label"].to_string() == "")
|
if(stats.get_type_name() != "array" || stats["0"]["label"].to_string() == "")
|
||||||
@ -362,7 +362,7 @@ RENDER:STATS_SECTION(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:BRANDS_SHOWCASE(Request& context)
|
COMPONENT:BRANDS_SHOWCASE(Request& context)
|
||||||
{
|
{
|
||||||
DTree logos = context.call["logos"];
|
DTree logos = context.call["logos"];
|
||||||
if(logos.get_type_name() != "array" || logos["0"]["name"].to_string() == "")
|
if(logos.get_type_name() != "array" || logos["0"]["name"].to_string() == "")
|
||||||
@ -408,7 +408,7 @@ RENDER:BRANDS_SHOWCASE(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:TESTIMONIALS(Request& context)
|
COMPONENT:TESTIMONIALS(Request& context)
|
||||||
{
|
{
|
||||||
DTree testimonials = context.call["testimonials"];
|
DTree testimonials = context.call["testimonials"];
|
||||||
if(testimonials.get_type_name() != "array" || testimonials["0"]["name"].to_string() == "")
|
if(testimonials.get_type_name() != "array" || testimonials["0"]["name"].to_string() == "")
|
||||||
@ -471,7 +471,7 @@ RENDER:TESTIMONIALS(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:PRICING_TABLE(Request& context)
|
COMPONENT:PRICING_TABLE(Request& context)
|
||||||
{
|
{
|
||||||
DTree plans = context.call["plans"];
|
DTree plans = context.call["plans"];
|
||||||
if(plans.get_type_name() != "array" || plans["0"]["name"].to_string() == "")
|
if(plans.get_type_name() != "array" || plans["0"]["name"].to_string() == "")
|
||||||
@ -544,7 +544,7 @@ RENDER:PRICING_TABLE(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:CTA_SECTION(Request& context)
|
COMPONENT:CTA_SECTION(Request& context)
|
||||||
{
|
{
|
||||||
String title = first(context.call["title"].to_string(), "Ready to Get Started?");
|
String title = first(context.call["title"].to_string(), "Ready to Get Started?");
|
||||||
String subtitle = first(context.call["subtitle"].to_string(), "Join thousands of developers building amazing applications with our framework.");
|
String subtitle = first(context.call["subtitle"].to_string(), "Join thousands of developers building amazing applications with our framework.");
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
#load "../../lib/app.uce"
|
#load "../../lib/app.uce"
|
||||||
|
|
||||||
RENDER(Request& context)
|
COMPONENT(Request& context)
|
||||||
{
|
{
|
||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
String current_theme = starter_theme_value("key", context);
|
String current_theme = context.cfg.get_by_path("theme/key").to_string();
|
||||||
String current_label = first(starter_theme_value("label", context), current_theme);
|
String current_label = first(context.cfg.get_by_path("theme/label").to_string(), current_theme);
|
||||||
String route_path = context.var["starter"]["route"]["l_path"].to_string();
|
String route_path = context.var["starter"]["route"]["l_path"].to_string();
|
||||||
|
|
||||||
<>
|
<>
|
||||||
@ -108,7 +108,7 @@ RENDER(Request& context)
|
|||||||
<div id="theme-menu" class="theme-menu" hidden>
|
<div id="theme-menu" class="theme-menu" hidden>
|
||||||
<div class="theme-menu-title">Available Themes</div>
|
<div class="theme-menu-title">Available Themes</div>
|
||||||
<div class="theme-menu-list">
|
<div class="theme-menu-list">
|
||||||
<? starter_cfg("theme/options", context).each([&](DTree theme_info, String theme_key) {
|
<? context.cfg.get_by_path("theme/options").each([&](DTree theme_info, String theme_key) {
|
||||||
StringMap params;
|
StringMap params;
|
||||||
params["theme"] = theme_key;
|
params["theme"] = theme_key;
|
||||||
String href = starter_link(route_path == "index" ? "" : route_path, params, context);
|
String href = starter_link(route_path == "index" ? "" : route_path, params, context);
|
||||||
|
|||||||
95
site/examples/uce-starter/components/gauges/common.css
Executable file
95
site/examples/uce-starter/components/gauges/common.css
Executable file
@ -0,0 +1,95 @@
|
|||||||
|
.horizontal .progressbar-item {
|
||||||
|
display: flex;
|
||||||
|
min-width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressbar-container.vertical {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical .progressbar-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressbar-label {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal .progressbar-label, .horizontal .progressbar-value {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal .progressbar-label {
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical .progressbar-label {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal .progressbar-value {
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressbar-background {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--bg-color);
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical .progressbar-background {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressbar-bar {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressbar-marker {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
opacity: 0.5;
|
||||||
|
background: var(--text-color, #333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressbar-marker:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal .progressbar-marker {
|
||||||
|
border-width: 0 2px 0 2px;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
min-width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical .progressbar-marker {
|
||||||
|
border-width: 2px 0 2px 0;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
min-height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressbar-background {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.needlegauge-svg .needle {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.needlegauge-item {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
121
site/examples/uce-starter/components/gauges/common.js
Executable file
121
site/examples/uce-starter/components/gauges/common.js
Executable file
@ -0,0 +1,121 @@
|
|||||||
|
// common code for gauges components
|
||||||
|
|
||||||
|
window.GaugeComponents = window.GaugeComponents || {};
|
||||||
|
|
||||||
|
Object.assign(window.GaugeComponents, { // as a namespace
|
||||||
|
|
||||||
|
// utility functions
|
||||||
|
clampValue: function(value, min, max) {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
},
|
||||||
|
|
||||||
|
pickEntryFromRange: function(ranges, value) {
|
||||||
|
if (!Array.isArray(ranges)) return null;
|
||||||
|
for (const entry of ranges) {
|
||||||
|
if (value >= entry.from && value <= entry.to) return entry;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an SVG element with namespace
|
||||||
|
*/
|
||||||
|
createSVGElement: function(tagName, attributes = {}) {
|
||||||
|
const element = document.createElementNS('http://www.w3.org/2000/svg', tagName);
|
||||||
|
Object.entries(attributes).forEach(([key, value]) => {
|
||||||
|
element.setAttribute(key, value);
|
||||||
|
});
|
||||||
|
return element;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets CSS custom property value from computed styles
|
||||||
|
*/
|
||||||
|
getCSSVar: function(varName) {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
|
||||||
|
},
|
||||||
|
|
||||||
|
resolveColor: function(colorSpec, value, pct) {
|
||||||
|
if (Array.isArray(colorSpec)) {
|
||||||
|
const match = this.pickEntryFromRange(colorSpec, value);
|
||||||
|
if (match && match.color) return match.color;
|
||||||
|
}
|
||||||
|
if (typeof colorSpec === 'string' && colorSpec !== '') return colorSpec;
|
||||||
|
if (pct < 60) return this.getCSSVar('--success') || '#10b981';
|
||||||
|
if (pct < 85) return this.getCSSVar('--warning') || '#f59e0b';
|
||||||
|
return this.getCSSVar('--error') || '#ef4444';
|
||||||
|
},
|
||||||
|
|
||||||
|
formatValue: function(value, precision, suffix) {
|
||||||
|
const numericValue = Number(value);
|
||||||
|
const normalizedPrecision = Number.isFinite(precision) ? precision : 1;
|
||||||
|
if (!Number.isFinite(numericValue)) return '--';
|
||||||
|
return numericValue.toFixed(normalizedPrecision) + (suffix || '');
|
||||||
|
},
|
||||||
|
|
||||||
|
gaugeArcPoint: function(pct, radius = 50) {
|
||||||
|
const angle = Math.PI - (pct / 100) * Math.PI;
|
||||||
|
return {
|
||||||
|
x: 60 + radius * Math.cos(angle),
|
||||||
|
y: 60 - radius * Math.sin(angle),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
updateWatermark: function(prefix, pct) {
|
||||||
|
const now = Date.now();
|
||||||
|
this._watermarks = this._watermarks || {};
|
||||||
|
let watermark = this._watermarks[prefix];
|
||||||
|
const resetWindow = 10 * 60 * 1000;
|
||||||
|
if (!watermark || (now - watermark.resetTs) > resetWindow) {
|
||||||
|
watermark = { lo: pct, hi: pct, resetTs: now };
|
||||||
|
this._watermarks[prefix] = watermark;
|
||||||
|
} else {
|
||||||
|
if (pct < watermark.lo) watermark.lo = pct;
|
||||||
|
if (pct > watermark.hi) watermark.hi = pct;
|
||||||
|
}
|
||||||
|
return watermark;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderWatermarkTick: function(lineId, pct) {
|
||||||
|
const line = document.getElementById(lineId);
|
||||||
|
if (!line) return;
|
||||||
|
if (pct == null) {
|
||||||
|
line.setAttribute('opacity', '0');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const outer = this.gaugeArcPoint(pct, 53);
|
||||||
|
const inner = this.gaugeArcPoint(pct, 43);
|
||||||
|
line.setAttribute('x1', outer.x.toFixed(1));
|
||||||
|
line.setAttribute('y1', outer.y.toFixed(1));
|
||||||
|
line.setAttribute('x2', inner.x.toFixed(1));
|
||||||
|
line.setAttribute('y2', inner.y.toFixed(1));
|
||||||
|
line.setAttribute('opacity', '0.7');
|
||||||
|
},
|
||||||
|
|
||||||
|
updateArcGauge: function(options) {
|
||||||
|
const value = Number(options.value);
|
||||||
|
const max = Number(options.max || 100);
|
||||||
|
const normalizedValue = Number.isFinite(value) ? value : 0;
|
||||||
|
const pct = this.clampValue(max === 0 ? 0 : (normalizedValue / max) * 100, 0, 100);
|
||||||
|
const arcLength = (pct / 100) * 157.08;
|
||||||
|
const arc = document.getElementById(options.arcId);
|
||||||
|
const text = document.getElementById(options.textId);
|
||||||
|
const meta = options.metaId ? document.getElementById(options.metaId) : null;
|
||||||
|
if (arc) {
|
||||||
|
arc.setAttribute('stroke-dasharray', arcLength.toFixed(1) + ' 157.08');
|
||||||
|
arc.setAttribute('stroke', this.resolveColor(options.color, normalizedValue, pct));
|
||||||
|
}
|
||||||
|
if (text) {
|
||||||
|
text.textContent = this.formatValue(normalizedValue, options.precision, options.suffix);
|
||||||
|
}
|
||||||
|
if (meta && options.meta != null) {
|
||||||
|
meta.textContent = options.meta;
|
||||||
|
}
|
||||||
|
if (options.watermarkPrefix) {
|
||||||
|
const watermark = this.updateWatermark(options.watermarkPrefix, pct);
|
||||||
|
this.renderWatermarkTick(options.watermarkPrefix + 'WmLo', watermark.lo);
|
||||||
|
this.renderWatermarkTick(options.watermarkPrefix + 'WmHi', watermark.hi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@ -1,6 +1,6 @@
|
|||||||
#load "../../lib/app.uce"
|
#load "../../lib/app.uce"
|
||||||
|
|
||||||
RENDER:PROGRESSBAR(Request& context)
|
COMPONENT:PROGRESSBAR(Request& context)
|
||||||
{
|
{
|
||||||
starter_register_js("components/gauges/common.js", context);
|
starter_register_js("components/gauges/common.js", context);
|
||||||
starter_register_css("themes/common/css/gauges.css", context);
|
starter_register_css("themes/common/css/gauges.css", context);
|
||||||
@ -96,13 +96,13 @@ RENDER:PROGRESSBAR(Request& context)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ProgressbarComponents.start_listen(<?= json_encode(context.call) ?>);
|
ProgressbarComponents.start_listen(<?: json_encode(context.call) ?>);
|
||||||
</script>
|
</script>
|
||||||
<? } ?>
|
<? } ?>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:NEEDLEGAUGE(Request& context)
|
COMPONENT:NEEDLEGAUGE(Request& context)
|
||||||
{
|
{
|
||||||
starter_register_css("themes/common/css/gauges.css", context);
|
starter_register_css("themes/common/css/gauges.css", context);
|
||||||
String gauge_id = first(context.call["id"].to_string(), "needlegauge-" + std::to_string((u64)time()));
|
String gauge_id = first(context.call["id"].to_string(), "needlegauge-" + std::to_string((u64)time()));
|
||||||
@ -167,7 +167,7 @@ RENDER:NEEDLEGAUGE(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:ARCGAUGE(Request& context)
|
COMPONENT:ARCGAUGE(Request& context)
|
||||||
{
|
{
|
||||||
starter_register_js("components/gauges/common.js", context);
|
starter_register_js("components/gauges/common.js", context);
|
||||||
starter_register_css("themes/common/css/gauges.css", context);
|
starter_register_css("themes/common/css/gauges.css", context);
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
#load "../../lib/app.uce"
|
#load "../../lib/app.uce"
|
||||||
|
|
||||||
RENDER(Request& context)
|
COMPONENT(Request& context)
|
||||||
{
|
{
|
||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
DTree user = starter_current_user(context);
|
StarterUser users(context);
|
||||||
|
DTree user = users.current();
|
||||||
bool signed_in = user["email"].to_string() != "";
|
bool signed_in = user["email"].to_string() != "";
|
||||||
String wrapper_class = starter_theme_value("key", context) == "localfirst" ? "admin-account-card" : "nav-account nav-menu";
|
String theme_key = context.cfg.get_by_path("theme/key").to_string();
|
||||||
String links_class = starter_theme_value("key", context) == "localfirst" ? "admin-account-links" : "";
|
String wrapper_class = theme_key == "localfirst" ? "admin-account-card" : "nav-account nav-menu";
|
||||||
String name_class = starter_theme_value("key", context) == "localfirst" ? "admin-account-name" : "account-name";
|
String links_class = theme_key == "localfirst" ? "admin-account-links" : "";
|
||||||
|
String name_class = theme_key == "localfirst" ? "admin-account-name" : "account-name";
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<div class="<?= wrapper_class ?>">
|
<div class="<?= wrapper_class ?>">
|
||||||
@ -20,7 +22,7 @@ RENDER(Request& context)
|
|||||||
<? } else { ?>
|
<? } else { ?>
|
||||||
<? if(links_class != "") { ?><div class="<?= links_class ?>"><? } ?>
|
<? if(links_class != "") { ?><div class="<?= links_class ?>"><? } ?>
|
||||||
<a href="<?= starter_link("account/login", context) ?>">Login</a>
|
<a href="<?= starter_link("account/login", context) ?>">Login</a>
|
||||||
<? if(starter_cfg("users/enable_signup", context).to_string() != "") { ?>
|
<? if(context.cfg.get_by_path("users/enable_signup").to_string() != "") { ?>
|
||||||
<a href="<?= starter_link("account/register", context) ?>">Register</a>
|
<a href="<?= starter_link("account/register", context) ?>">Register</a>
|
||||||
<? } ?>
|
<? } ?>
|
||||||
<? if(links_class != "") { ?></div><? } ?>
|
<? if(links_class != "") { ?></div><? } ?>
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
#load "../../lib/app.uce"
|
#load "../../lib/app.uce"
|
||||||
|
|
||||||
RENDER(Request& context)
|
COMPONENT(Request& context)
|
||||||
{
|
{
|
||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
if(context.var["starter"]["embed_mode"].to_string() != "")
|
if(context.var["starter"]["embed_mode"].to_string() != "")
|
||||||
return;
|
return;
|
||||||
|
|
||||||
String text = first(starter_theme_value("footer_text", context), starter_cfg_string("site/name", context));
|
String text = first(context.cfg.get_by_path("theme/footer_text").to_string(), context.cfg.get_by_path("site/name").to_string());
|
||||||
String inner_class = starter_theme_value("key", context) == "portal-light" ? "footer-inner" : "";
|
String inner_class = context.cfg.get_by_path("theme/key").to_string() == "portal-light" ? "footer-inner" : "";
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<footer>
|
<footer>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#load "../../lib/app.uce"
|
#load "../../lib/app.uce"
|
||||||
|
|
||||||
RENDER(Request& context)
|
COMPONENT(Request& context)
|
||||||
{
|
{
|
||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ RENDER(Request& context)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String theme_key = starter_theme_value("key", context);
|
String theme_key = context.cfg.get_by_path("theme/key").to_string();
|
||||||
bool embed_mode = context.var["starter"]["embed_mode"].to_string() == "1";
|
bool embed_mode = context.var["starter"]["embed_mode"].to_string() == "1";
|
||||||
String body_class = embed_mode ? "embed-mode" : "";
|
String body_class = embed_mode ? "embed-mode" : "";
|
||||||
if(theme_key == "portal-dark")
|
if(theme_key == "portal-dark")
|
||||||
@ -43,9 +43,9 @@ RENDER(Request& context)
|
|||||||
|
|
||||||
<>
|
<>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= starter_theme_value("key", context) ?>">
|
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
|
||||||
<head>
|
<head>
|
||||||
<? render_component("page_shell:HEAD", context.call, context); ?>
|
<? render_component(":HEAD", context.call, context); ?>
|
||||||
</head>
|
</head>
|
||||||
<body<?: body_class != "" ? " class=\"" + html_escape(body_class) + "\"" : "" ?>>
|
<body<?: body_class != "" ? " class=\"" + html_escape(body_class) + "\"" : "" ?>>
|
||||||
<? if(theme_key == "retro-gaming") { ?><div class="retro-stars"></div><? } ?>
|
<? if(theme_key == "retro-gaming") { ?><div class="retro-stars"></div><? } ?>
|
||||||
@ -61,10 +61,10 @@ RENDER(Request& context)
|
|||||||
<nav class="admin-nav">
|
<nav class="admin-nav">
|
||||||
<div class="admin-nav-header">
|
<div class="admin-nav-header">
|
||||||
<a class="admin-nav-title" href="<?= starter_link("", context) ?>">
|
<a class="admin-nav-title" href="<?= starter_link("", context) ?>">
|
||||||
<img class="admin-nav-logo-img" src="<?= starter_asset_url("themes/localfirst/img/local_first_logo.png", context) ?>" alt="<?= starter_cfg_string("site/name", context) ?>" />
|
<img class="admin-nav-logo-img" src="<?= starter_asset_url("themes/localfirst/img/local_first_logo.png", context) ?>" alt="<?= context.cfg.get_by_path("site/name").to_string() ?>" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<? starter_cfg("menu", context).each([&](DTree menu_item, String menu_key) {
|
<? context.cfg.get_by_path("menu").each([&](DTree menu_item, String menu_key) {
|
||||||
if(menu_item["hidden"].to_string() == "1")
|
if(menu_item["hidden"].to_string() == "1")
|
||||||
return;
|
return;
|
||||||
String current_path = context.var["starter"]["route"]["l_path"].to_string();
|
String current_path = context.var["starter"]["route"]["l_path"].to_string();
|
||||||
@ -95,17 +95,17 @@ RENDER(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:HEAD(Request& context)
|
COMPONENT:HEAD(Request& context)
|
||||||
{
|
{
|
||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
String title = first(context.var["starter"]["page_title"].to_string(), starter_cfg_string("site/default_page_title", context));
|
String title = first(context.var["starter"]["page_title"].to_string(), context.cfg.get_by_path("site/default_page_title").to_string());
|
||||||
String description = first(starter_theme_value("meta_description", context), "UCE starter example");
|
String description = first(context.cfg.get_by_path("theme/meta_description").to_string(), "UCE starter example");
|
||||||
String theme_color = first(starter_theme_value("theme_color", context), "#0f172a");
|
String theme_color = first(context.cfg.get_by_path("theme/theme_color").to_string(), "#0f172a");
|
||||||
String icon = starter_asset_url(starter_theme_value("path", context) + "icon.png", context);
|
String icon = starter_asset_url(context.cfg.get_by_path("theme/path").to_string() + "icon.png", context);
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title><?= title + " | " + starter_cfg_string("site/name", context) ?></title>
|
<title><?= title + " | " + context.cfg.get_by_path("site/name").to_string() ?></title>
|
||||||
<meta name="description" content="<?= description ?>">
|
<meta name="description" content="<?= description ?>">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="theme-color" content="<?= theme_color ?>">
|
<meta name="theme-color" content="<?= theme_color ?>">
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
#load "../../lib/app.uce"
|
#load "../../lib/app.uce"
|
||||||
|
|
||||||
RENDER(Request& context)
|
COMPONENT(Request& context)
|
||||||
{
|
{
|
||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<nav<?: starter_theme_value("key", context) == "portal-dark" || starter_theme_value("key", context) == "dark" || starter_theme_value("key", context) == "retro-gaming" ? " class=\"nav-shell\"" : "" ?>>
|
<nav<?: context.cfg.get_by_path("theme/key").to_string() == "portal-dark" || context.cfg.get_by_path("theme/key").to_string() == "dark" || context.cfg.get_by_path("theme/key").to_string() == "retro-gaming" ? " class=\"nav-shell\"" : "" ?>>
|
||||||
<div class="nav-menu">
|
<div class="nav-menu">
|
||||||
<a href="<?= starter_link("", context) ?>"><?= starter_cfg_string("site/name", context) ?></a>
|
<a href="<?= starter_link("", context) ?>"><?= context.cfg.get_by_path("site/name").to_string() ?></a>
|
||||||
<? starter_cfg("menu", context).each([&](DTree menu_item, String menu_key) {
|
<? context.cfg.get_by_path("menu").each([&](DTree menu_item, String menu_key) {
|
||||||
if(menu_item["hidden"].to_string() == "1")
|
if(menu_item["hidden"].to_string() == "1")
|
||||||
return;
|
return;
|
||||||
?><a href="<?= starter_menu_href(menu_key, menu_item, context) ?>"><?= menu_item["title"].to_string() ?></a><?
|
?><a href="<?= starter_menu_href(menu_key, menu_item, context) ?>"><?= menu_item["title"].to_string() ?></a><?
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#load "../../lib/app.uce"
|
#load "../../lib/app.uce"
|
||||||
|
|
||||||
RENDER:APP_FRAME(Request& context)
|
COMPONENT:APP_FRAME(Request& context)
|
||||||
{
|
{
|
||||||
starter_register_css("themes/common/css/workspace.css", context);
|
starter_register_css("themes/common/css/workspace.css", context);
|
||||||
starter_register_js("js/u-workspace-shell.js", context);
|
starter_register_js("js/u-workspace-shell.js", context);
|
||||||
@ -16,7 +16,7 @@ RENDER:APP_FRAME(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:EMPTY_STATE(Request& context)
|
COMPONENT:EMPTY_STATE(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<div class="ws-empty-state<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
<div class="ws-empty-state<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
||||||
@ -28,14 +28,14 @@ RENDER:EMPTY_STATE(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:LIST_STATE(Request& context)
|
COMPONENT:LIST_STATE(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<div class="ws-list-state<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>"><? if(context.call["icon_class"].to_string() != "") { ?><i class="<?= context.call["icon_class"].to_string() ?>"></i><? } ?><span><?= context.call["text"].to_string() ?></span></div>
|
<div class="ws-list-state<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>"><? if(context.call["icon_class"].to_string() != "") { ?><i class="<?= context.call["icon_class"].to_string() ?>"></i><? } ?><span><?= context.call["text"].to_string() ?></span></div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:MOBILE_BAR(Request& context)
|
COMPONENT:MOBILE_BAR(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<div class="ws-mobile-bar<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
<div class="ws-mobile-bar<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
||||||
@ -47,7 +47,7 @@ RENDER:MOBILE_BAR(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:PANEL_HEADER(Request& context)
|
COMPONENT:PANEL_HEADER(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<div class="ws-panel-header<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
<div class="ws-panel-header<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
||||||
@ -60,7 +60,7 @@ RENDER:PANEL_HEADER(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:PANEL(Request& context)
|
COMPONENT:PANEL(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<section<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-panel<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
<section<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-panel<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
||||||
@ -70,7 +70,7 @@ RENDER:PANEL(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:SECTION_HEAD(Request& context)
|
COMPONENT:SECTION_HEAD(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<div class="ws-section-head<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
<div class="ws-section-head<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
||||||
@ -80,7 +80,7 @@ RENDER:SECTION_HEAD(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:SECTION(Request& context)
|
COMPONENT:SECTION(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<section<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-section<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
<section<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-section<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
||||||
@ -90,7 +90,7 @@ RENDER:SECTION(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:SIDEBAR_SHELL(Request& context)
|
COMPONENT:SIDEBAR_SHELL(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<aside<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-sidebar<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
<aside<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-sidebar<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
||||||
@ -100,7 +100,7 @@ RENDER:SIDEBAR_SHELL(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:SIDEBAR_TOOLBAR(Request& context)
|
COMPONENT:SIDEBAR_TOOLBAR(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<div class="ws-sidebar-top<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
<div class="ws-sidebar-top<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
|
||||||
@ -113,7 +113,7 @@ RENDER:SIDEBAR_TOOLBAR(Request& context)
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:STATUS_PILL(Request& context)
|
COMPONENT:STATUS_PILL(Request& context)
|
||||||
{
|
{
|
||||||
String variant = to_lower(first(context.call["variant"].to_string(), "neutral"));
|
String variant = to_lower(first(context.call["variant"].to_string(), "neutral"));
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -1,49 +1,4 @@
|
|||||||
String starter_dirname(String path)
|
#load "user.class.h"
|
||||||
{
|
|
||||||
if(path == "")
|
|
||||||
return("");
|
|
||||||
auto parts = split(path, "/");
|
|
||||||
if(parts.size() <= 1)
|
|
||||||
return("");
|
|
||||||
parts.pop_back();
|
|
||||||
String result = join(parts, "/");
|
|
||||||
if(result == "")
|
|
||||||
return("/");
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
String starter_fs_join(String root, String relative)
|
|
||||||
{
|
|
||||||
if(relative == "")
|
|
||||||
return(root);
|
|
||||||
if(relative[0] == '/')
|
|
||||||
return(relative);
|
|
||||||
if(root == "" || root == "/")
|
|
||||||
return(root + relative);
|
|
||||||
if(root[root.length() - 1] == '/')
|
|
||||||
return(root + relative);
|
|
||||||
return(root + "/" + relative);
|
|
||||||
}
|
|
||||||
|
|
||||||
String starter_safe_name(String raw)
|
|
||||||
{
|
|
||||||
String result = "";
|
|
||||||
for(auto c : raw)
|
|
||||||
{
|
|
||||||
if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))
|
|
||||||
result.append(1, c);
|
|
||||||
else
|
|
||||||
result.append(1, '_');
|
|
||||||
}
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
String starter_json_string(String raw)
|
|
||||||
{
|
|
||||||
DTree value;
|
|
||||||
value = raw;
|
|
||||||
return(json_encode(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
String starter_fs_root(Request& context)
|
String starter_fs_root(Request& context)
|
||||||
{
|
{
|
||||||
@ -66,7 +21,7 @@ String starter_base_url(Request& context)
|
|||||||
String base = context.var["starter"]["base_url"].to_string();
|
String base = context.var["starter"]["base_url"].to_string();
|
||||||
if(base == "")
|
if(base == "")
|
||||||
{
|
{
|
||||||
base = starter_dirname(starter_script_url(context));
|
base = dirname(starter_script_url(context));
|
||||||
if(base == "")
|
if(base == "")
|
||||||
base = "/";
|
base = "/";
|
||||||
if(base[base.length() - 1] != '/')
|
if(base[base.length() - 1] != '/')
|
||||||
@ -77,34 +32,12 @@ String starter_base_url(Request& context)
|
|||||||
|
|
||||||
String starter_fs_path(String relative, Request& context)
|
String starter_fs_path(String relative, Request& context)
|
||||||
{
|
{
|
||||||
return(starter_fs_join(starter_fs_root(context), relative));
|
return(path_join(starter_fs_root(context), relative));
|
||||||
}
|
|
||||||
|
|
||||||
DTree starter_cfg(String path, Request& context)
|
|
||||||
{
|
|
||||||
DTree result = context.var["starter"]["config"];
|
|
||||||
for(auto segment : split(path, "/"))
|
|
||||||
{
|
|
||||||
if(segment == "")
|
|
||||||
continue;
|
|
||||||
result = result[segment];
|
|
||||||
}
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
String starter_cfg_string(String path, Request& context)
|
|
||||||
{
|
|
||||||
return(starter_cfg(path, context).to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String starter_link(String path, Request& context);
|
String starter_link(String path, Request& context);
|
||||||
String starter_link(String path, StringMap params, Request& context);
|
String starter_link(String path, StringMap params, Request& context);
|
||||||
|
|
||||||
String starter_theme_value(String key, Request& context)
|
|
||||||
{
|
|
||||||
return(starter_cfg("theme/" + key, context).to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
String starter_first_route_segment(Request& context)
|
String starter_first_route_segment(Request& context)
|
||||||
{
|
{
|
||||||
String query = context.params["QUERY_STRING"];
|
String query = context.params["QUERY_STRING"];
|
||||||
@ -251,12 +184,6 @@ DTree starter_default_config()
|
|||||||
return(config);
|
return(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
void starter_set_status(Request& context, s32 code, String reason)
|
|
||||||
{
|
|
||||||
context.response_code = "HTTP/1.1 " + std::to_string(code) + " " + reason;
|
|
||||||
context.flags.status = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
void starter_register_css(String path, Request& context)
|
void starter_register_css(String path, Request& context)
|
||||||
{
|
{
|
||||||
context.var["starter"]["assets"]["css"][path] = path;
|
context.var["starter"]["assets"]["css"][path] = path;
|
||||||
@ -321,182 +248,15 @@ String starter_link(String path, StringMap params, Request& context)
|
|||||||
return(url);
|
return(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
String starter_filebase_root(Request& context)
|
|
||||||
{
|
|
||||||
String root = starter_cfg_string("filebase/path", context);
|
|
||||||
if(root == "")
|
|
||||||
root = "/tmp/uce-starter-data";
|
|
||||||
return(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
String starter_hash_id(String raw)
|
|
||||||
{
|
|
||||||
raw = to_lower(trim(raw));
|
|
||||||
return(gen_sha1("uce-starter:" + raw).substr(0, 12));
|
|
||||||
}
|
|
||||||
|
|
||||||
String starter_user_dir(String email, Request& context)
|
|
||||||
{
|
|
||||||
String bucket = starter_hash_id(email);
|
|
||||||
return(starter_filebase_root(context) + "/users/" + bucket.substr(0, 2) + "/" + bucket);
|
|
||||||
}
|
|
||||||
|
|
||||||
String starter_user_file(String email, Request& context)
|
|
||||||
{
|
|
||||||
return(starter_user_dir(email, context) + "/account.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
String starter_password_hash(String password, String salt)
|
|
||||||
{
|
|
||||||
String hash = "uce-starter:" + password + ":" + salt;
|
|
||||||
for(s32 i = 0; i < 2048; i++)
|
|
||||||
hash = gen_sha1(hash + ":" + std::to_string(i));
|
|
||||||
return(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
DTree starter_read_json_file(String file_name)
|
|
||||||
{
|
|
||||||
DTree result;
|
|
||||||
if(!file_exists(file_name))
|
|
||||||
return(result);
|
|
||||||
String raw = trim(file_get_contents(file_name));
|
|
||||||
if(raw == "")
|
|
||||||
return(result);
|
|
||||||
return(json_decode(raw));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool starter_write_json_file(String file_name, DTree data)
|
|
||||||
{
|
|
||||||
mkdir(starter_dirname(file_name));
|
|
||||||
return(file_put_contents(file_name, json_encode(data)));
|
|
||||||
}
|
|
||||||
|
|
||||||
DTree starter_user_load(String email, Request& context)
|
|
||||||
{
|
|
||||||
DTree user = starter_read_json_file(starter_user_file(email, context));
|
|
||||||
if(user.get_type_name() != "array")
|
|
||||||
return(DTree());
|
|
||||||
user["id"] = to_lower(trim(email));
|
|
||||||
return(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
DTree starter_user_create(String email, String password, Request& context)
|
|
||||||
{
|
|
||||||
DTree result;
|
|
||||||
email = to_lower(trim(email));
|
|
||||||
password = trim(password);
|
|
||||||
result["message"] = "";
|
|
||||||
result["result"].set_bool(false);
|
|
||||||
|
|
||||||
if(email == "" || password == "")
|
|
||||||
{
|
|
||||||
result["message"] = "email_and_password_required";
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
if(email.find("@") == String::npos)
|
|
||||||
{
|
|
||||||
result["message"] = "invalid_email";
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
DTree existing = starter_user_load(email, context);
|
|
||||||
if(existing.get_type_name() == "array" && existing["email"].to_string() != "")
|
|
||||||
{
|
|
||||||
result["message"] = "user_exists";
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
String salt = gen_sha1(std::to_string((u64)time()) + ":" + std::to_string((u64)(microtime() * 1000000.0)) + ":" + make_session_id()).substr(0, 24);
|
|
||||||
DTree user;
|
|
||||||
user["email"] = email;
|
|
||||||
user["salt"] = salt;
|
|
||||||
user["password_hash"] = starter_password_hash(password, salt);
|
|
||||||
user["created"] = std::to_string((u64)time());
|
|
||||||
DTree role;
|
|
||||||
role = "user";
|
|
||||||
user["roles"].push(role);
|
|
||||||
|
|
||||||
starter_write_json_file(starter_user_file(email, context), user);
|
|
||||||
user["id"] = email;
|
|
||||||
result["result"].set_bool(true);
|
|
||||||
result["id"] = email;
|
|
||||||
result["profile"] = user;
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
DTree starter_user_sign_in(String email, String password, Request& context)
|
|
||||||
{
|
|
||||||
DTree result;
|
|
||||||
result["result"].set_bool(false);
|
|
||||||
email = to_lower(trim(email));
|
|
||||||
password = trim(password);
|
|
||||||
|
|
||||||
DTree user = starter_user_load(email, context);
|
|
||||||
if(user.get_type_name() != "array" || user["email"].to_string() == "")
|
|
||||||
{
|
|
||||||
result["message"] = "no_such_user";
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
if(user["password_hash"].to_string() == "")
|
|
||||||
{
|
|
||||||
result["message"] = "no_password_set";
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
String expected = starter_password_hash(password, user["salt"].to_string());
|
|
||||||
if(expected != user["password_hash"].to_string())
|
|
||||||
{
|
|
||||||
result["message"] = "invalid_password";
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
session_start();
|
|
||||||
context.session["starter_user_id"] = email;
|
|
||||||
context.var["starter"]["current_user"] = user;
|
|
||||||
result["result"].set_bool(true);
|
|
||||||
result["profile"] = user;
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool starter_is_signed_in(Request& context)
|
|
||||||
{
|
|
||||||
if(context.var["starter"]["current_user"]["email"].to_string() != "")
|
|
||||||
return(true);
|
|
||||||
String user_id = context.session["starter_user_id"];
|
|
||||||
if(user_id == "")
|
|
||||||
return(false);
|
|
||||||
DTree user = starter_user_load(user_id, context);
|
|
||||||
if(user["email"].to_string() == "")
|
|
||||||
{
|
|
||||||
context.session.erase("starter_user_id");
|
|
||||||
return(false);
|
|
||||||
}
|
|
||||||
context.var["starter"]["current_user"] = user;
|
|
||||||
return(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
DTree starter_current_user(Request& context)
|
|
||||||
{
|
|
||||||
if(starter_is_signed_in(context))
|
|
||||||
return(context.var["starter"]["current_user"]);
|
|
||||||
return(DTree());
|
|
||||||
}
|
|
||||||
|
|
||||||
void starter_user_logout(Request& context)
|
|
||||||
{
|
|
||||||
context.session.erase("starter_user_id");
|
|
||||||
context.var["starter"]["current_user"].clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void starter_redirect(String path, Request& context)
|
void starter_redirect(String path, Request& context)
|
||||||
{
|
{
|
||||||
starter_set_status(context, 302, "Found");
|
context.set_status(302, "Found");
|
||||||
context.header["Location"] = starter_link(path, context);
|
context.header["Location"] = starter_link(path, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
void starter_not_found(String message, Request& context)
|
void starter_not_found(String message, Request& context)
|
||||||
{
|
{
|
||||||
starter_set_status(context, 404, "Not Found");
|
context.set_status(404, "Not Found");
|
||||||
context.var["starter"]["error"] = message;
|
context.var["starter"]["error"] = message;
|
||||||
context.var["starter"]["page_title"] = "404 Not Found";
|
context.var["starter"]["page_title"] = "404 Not Found";
|
||||||
}
|
}
|
||||||
@ -511,7 +271,7 @@ String starter_menu_href(String menu_key, DTree menu_item, Request& context)
|
|||||||
String starter_html_class(Request& context)
|
String starter_html_class(Request& context)
|
||||||
{
|
{
|
||||||
String classes = "no-js";
|
String classes = "no-js";
|
||||||
if(starter_theme_value("mode", context) == "dark")
|
if(context.cfg.get_by_path("theme/mode").to_string() == "dark")
|
||||||
classes += " dark-theme";
|
classes += " dark-theme";
|
||||||
return(classes);
|
return(classes);
|
||||||
}
|
}
|
||||||
@ -525,7 +285,7 @@ void starter_boot(Request& context)
|
|||||||
context.var["starter"]["fs_root"] = get_cwd();
|
context.var["starter"]["fs_root"] = get_cwd();
|
||||||
context.var["starter"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
|
context.var["starter"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
|
||||||
|
|
||||||
String base_url = starter_dirname(context.var["starter"]["script_url"].to_string());
|
String base_url = dirname(context.var["starter"]["script_url"].to_string());
|
||||||
if(base_url == "")
|
if(base_url == "")
|
||||||
base_url = "/";
|
base_url = "/";
|
||||||
if(base_url[base_url.length() - 1] != '/')
|
if(base_url[base_url.length() - 1] != '/')
|
||||||
@ -541,7 +301,9 @@ void starter_boot(Request& context)
|
|||||||
config["theme"][key] = item;
|
config["theme"][key] = item;
|
||||||
});
|
});
|
||||||
config["theme"]["key"] = requested_theme;
|
config["theme"]["key"] = requested_theme;
|
||||||
context.var["starter"]["config"] = config;
|
context.cfg = config;
|
||||||
|
context.var["cfg"].set_reference(&context.cfg);
|
||||||
|
context.var["starter"]["config"].set_reference(&context.cfg);
|
||||||
context.var["starter"]["route"] = starter_make_route(context);
|
context.var["starter"]["route"] = starter_make_route(context);
|
||||||
context.var["starter"]["page_type"] = "html";
|
context.var["starter"]["page_type"] = "html";
|
||||||
context.var["starter"]["page_title"] = first(config["menu"][context.var["starter"]["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
|
context.var["starter"]["page_title"] = first(config["menu"][context.var["starter"]["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
|
||||||
@ -554,9 +316,9 @@ void starter_boot(Request& context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
session_start();
|
session_start();
|
||||||
starter_is_signed_in(context);
|
StarterUser(context).is_signed_in();
|
||||||
|
|
||||||
starter_register_css(starter_theme_value("path", context) + "css/style.css", context);
|
starter_register_css(context.cfg.get_by_path("theme/path").to_string() + "css/style.css", context);
|
||||||
starter_register_css("themes/common/fontawesome/css/all.min.css", context);
|
starter_register_css("themes/common/fontawesome/css/all.min.css", context);
|
||||||
starter_register_js("js/u-query.js", context);
|
starter_register_js("js/u-query.js", context);
|
||||||
starter_register_js("js/morphdom.js", context);
|
starter_register_js("js/morphdom.js", context);
|
||||||
|
|||||||
186
site/examples/uce-starter/lib/user.class.h
Normal file
186
site/examples/uce-starter/lib/user.class.h
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
class StarterUser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Request& context;
|
||||||
|
|
||||||
|
explicit StarterUser(Request& request)
|
||||||
|
: context(request)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
static String session_key()
|
||||||
|
{
|
||||||
|
return("starter_user_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
static String normalize_email(String email)
|
||||||
|
{
|
||||||
|
return(to_lower(trim(email)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static String hash_id(String raw)
|
||||||
|
{
|
||||||
|
raw = normalize_email(raw);
|
||||||
|
return(gen_sha1("uce-starter:" + raw).substr(0, 12));
|
||||||
|
}
|
||||||
|
|
||||||
|
String root_path()
|
||||||
|
{
|
||||||
|
return(first(context.cfg.get_by_path("filebase/path").to_string(), "/tmp/uce-starter-data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String dir(String email)
|
||||||
|
{
|
||||||
|
String bucket = hash_id(email);
|
||||||
|
return(path_join(root_path(), "users/" + bucket.substr(0, 2) + "/" + bucket));
|
||||||
|
}
|
||||||
|
|
||||||
|
String file_name(String email)
|
||||||
|
{
|
||||||
|
return(path_join(dir(email), "account.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static String password_hash(String password, String salt)
|
||||||
|
{
|
||||||
|
String hash = "uce-starter:" + password + ":" + salt;
|
||||||
|
for(s32 i = 0; i < 2048; i++)
|
||||||
|
hash = gen_sha1(hash + ":" + std::to_string(i));
|
||||||
|
return(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
static DTree read_json_file(String file_name)
|
||||||
|
{
|
||||||
|
DTree result;
|
||||||
|
if(!file_exists(file_name))
|
||||||
|
return(result);
|
||||||
|
String raw = trim(file_get_contents(file_name));
|
||||||
|
if(raw == "")
|
||||||
|
return(result);
|
||||||
|
return(json_decode(raw));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool write_json_file(String file_name, DTree data)
|
||||||
|
{
|
||||||
|
mkdir(dirname(file_name));
|
||||||
|
return(file_put_contents(file_name, json_encode(data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
DTree load(String email)
|
||||||
|
{
|
||||||
|
DTree user = read_json_file(file_name(email));
|
||||||
|
if(user.get_type_name() != "array")
|
||||||
|
return(DTree());
|
||||||
|
user["id"] = normalize_email(email);
|
||||||
|
return(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
DTree create(String email, String password)
|
||||||
|
{
|
||||||
|
DTree result;
|
||||||
|
email = normalize_email(email);
|
||||||
|
password = trim(password);
|
||||||
|
result["message"] = "";
|
||||||
|
result["result"].set_bool(false);
|
||||||
|
|
||||||
|
if(email == "" || password == "")
|
||||||
|
{
|
||||||
|
result["message"] = "email_and_password_required";
|
||||||
|
return(result);
|
||||||
|
}
|
||||||
|
if(email.find("@") == String::npos)
|
||||||
|
{
|
||||||
|
result["message"] = "invalid_email";
|
||||||
|
return(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
DTree existing = load(email);
|
||||||
|
if(existing.get_type_name() == "array" && existing["email"].to_string() != "")
|
||||||
|
{
|
||||||
|
result["message"] = "user_exists";
|
||||||
|
return(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
String salt = gen_sha1(std::to_string((u64)time()) + ":" + std::to_string((u64)(microtime() * 1000000.0)) + ":" + make_session_id()).substr(0, 24);
|
||||||
|
DTree user;
|
||||||
|
user["email"] = email;
|
||||||
|
user["salt"] = salt;
|
||||||
|
user["password_hash"] = password_hash(password, salt);
|
||||||
|
user["created"] = std::to_string((u64)time());
|
||||||
|
DTree role;
|
||||||
|
role = "user";
|
||||||
|
user["roles"].push(role);
|
||||||
|
|
||||||
|
write_json_file(file_name(email), user);
|
||||||
|
user["id"] = email;
|
||||||
|
result["result"].set_bool(true);
|
||||||
|
result["id"] = email;
|
||||||
|
result["profile"] = user;
|
||||||
|
return(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
DTree sign_in(String email, String password)
|
||||||
|
{
|
||||||
|
DTree result;
|
||||||
|
result["result"].set_bool(false);
|
||||||
|
email = normalize_email(email);
|
||||||
|
password = trim(password);
|
||||||
|
|
||||||
|
DTree user = load(email);
|
||||||
|
if(user.get_type_name() != "array" || user["email"].to_string() == "")
|
||||||
|
{
|
||||||
|
result["message"] = "no_such_user";
|
||||||
|
return(result);
|
||||||
|
}
|
||||||
|
if(user["password_hash"].to_string() == "")
|
||||||
|
{
|
||||||
|
result["message"] = "no_password_set";
|
||||||
|
return(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
String expected = password_hash(password, user["salt"].to_string());
|
||||||
|
if(expected != user["password_hash"].to_string())
|
||||||
|
{
|
||||||
|
result["message"] = "invalid_password";
|
||||||
|
return(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
context.session[session_key()] = email;
|
||||||
|
context.var["starter"]["current_user"] = user;
|
||||||
|
result["result"].set_bool(true);
|
||||||
|
result["profile"] = user;
|
||||||
|
return(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_signed_in()
|
||||||
|
{
|
||||||
|
if(context.var["starter"]["current_user"]["email"].to_string() != "")
|
||||||
|
return(true);
|
||||||
|
String user_id = context.session[session_key()];
|
||||||
|
if(user_id == "")
|
||||||
|
return(false);
|
||||||
|
DTree user = load(user_id);
|
||||||
|
if(user["email"].to_string() == "")
|
||||||
|
{
|
||||||
|
context.session.erase(session_key());
|
||||||
|
return(false);
|
||||||
|
}
|
||||||
|
context.var["starter"]["current_user"] = user;
|
||||||
|
return(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
DTree current()
|
||||||
|
{
|
||||||
|
if(is_signed_in())
|
||||||
|
return(context.var["starter"]["current_user"]);
|
||||||
|
return(DTree());
|
||||||
|
}
|
||||||
|
|
||||||
|
void logout()
|
||||||
|
{
|
||||||
|
context.session.erase(session_key());
|
||||||
|
context.var["starter"]["current_user"].clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -4,10 +4,11 @@ RENDER(Request& context)
|
|||||||
{
|
{
|
||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
context.var["starter"]["page_title"] = "Login";
|
context.var["starter"]["page_title"] = "Login";
|
||||||
|
StarterUser users(context);
|
||||||
DTree result;
|
DTree result;
|
||||||
if(context.params["REQUEST_METHOD"] == "POST")
|
if(context.params["REQUEST_METHOD"] == "POST")
|
||||||
{
|
{
|
||||||
result = starter_user_sign_in(context.post["email"], context.post["password"], context);
|
result = users.sign_in(context.post["email"], context.post["password"]);
|
||||||
if(result["result"].to_string() != "")
|
if(result["result"].to_string() != "")
|
||||||
{
|
{
|
||||||
starter_redirect("account/profile", context);
|
starter_redirect("account/profile", context);
|
||||||
|
|||||||
@ -3,6 +3,6 @@
|
|||||||
RENDER(Request& context)
|
RENDER(Request& context)
|
||||||
{
|
{
|
||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
starter_user_logout(context);
|
StarterUser(context).logout();
|
||||||
starter_redirect("account/login", context);
|
starter_redirect("account/login", context);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,14 @@
|
|||||||
RENDER(Request& context)
|
RENDER(Request& context)
|
||||||
{
|
{
|
||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
if(!starter_is_signed_in(context))
|
StarterUser users(context);
|
||||||
|
if(!users.is_signed_in())
|
||||||
{
|
{
|
||||||
starter_redirect("account/login", context);
|
starter_redirect("account/login", context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.var["starter"]["page_title"] = "Profile";
|
context.var["starter"]["page_title"] = "Profile";
|
||||||
DTree user = starter_current_user(context);
|
DTree user = users.current();
|
||||||
String roles = "";
|
String roles = "";
|
||||||
user["roles"].each([&](DTree role, String key) {
|
user["roles"].each([&](DTree role, String key) {
|
||||||
if(roles != "")
|
if(roles != "")
|
||||||
|
|||||||
@ -4,9 +4,10 @@ RENDER(Request& context)
|
|||||||
{
|
{
|
||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
context.var["starter"]["page_title"] = "Register";
|
context.var["starter"]["page_title"] = "Register";
|
||||||
|
StarterUser users(context);
|
||||||
DTree result;
|
DTree result;
|
||||||
if(context.params["REQUEST_METHOD"] == "POST")
|
if(context.params["REQUEST_METHOD"] == "POST")
|
||||||
result = starter_user_create(context.post["email"], context.post["password"], context);
|
result = users.create(context.post["email"], context.post["password"]);
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<h1>Register</h1>
|
<h1>Register</h1>
|
||||||
|
|||||||
@ -24,10 +24,10 @@ RENDER(Request& context)
|
|||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Current Session Status</h2>
|
<h2>Current Session Status</h2>
|
||||||
<? if(context.session["starter_user_id"] != "") { ?>
|
<? if(context.session[StarterUser::session_key()] != "") { ?>
|
||||||
<div class="success-card">
|
<div class="success-card">
|
||||||
<h3>Logged In</h3>
|
<h3>Logged In</h3>
|
||||||
<p>User ID: <?= context.session["starter_user_id"] ?></p>
|
<p>User ID: <?= context.session[StarterUser::session_key()] ?></p>
|
||||||
</div>
|
</div>
|
||||||
<? } else { ?>
|
<? } else { ?>
|
||||||
<div class="component-card">
|
<div class="component-card">
|
||||||
|
|||||||
@ -14,7 +14,7 @@ RENDER(Request& context)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
starter_set_status(context, 400, "Bad Request");
|
context.set_status(400, "Bad Request");
|
||||||
context.var["starter"]["json"]["status"] = "error";
|
context.var["starter"]["json"]["status"] = "error";
|
||||||
context.var["starter"]["json"]["message"] = "Invalid input";
|
context.var["starter"]["json"]["message"] = "Invalid input";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@ RENDER(Request& context)
|
|||||||
<h2>Theme Families</h2>
|
<h2>Theme Families</h2>
|
||||||
<p>Compare the starter’s built-in theme families and jump to the live gallery.</p>
|
<p>Compare the starter’s built-in theme families and jump to the live gallery.</p>
|
||||||
<div class="theme-grid">
|
<div class="theme-grid">
|
||||||
<? starter_cfg("theme/options", context).each([&](DTree theme, String key) {
|
<? context.cfg.get_by_path("theme/options").each([&](DTree theme, String key) {
|
||||||
if(key != "portal-light" && key != "portal-dark" && key != "localfirst")
|
if(key != "portal-light" && key != "portal-dark" && key != "localfirst")
|
||||||
return;
|
return;
|
||||||
StringMap theme_params;
|
StringMap theme_params;
|
||||||
|
|||||||
@ -18,7 +18,7 @@ RENDER(Request& context)
|
|||||||
<div class="window-dot green"></div>
|
<div class="window-dot green"></div>
|
||||||
<span class="mono-text">marketing_blocks.uce</span>
|
<span class="mono-text">marketing_blocks.uce</span>
|
||||||
</div>
|
</div>
|
||||||
<pre class="code-block"><code>RENDER:HERO_SECTION(Request& context)
|
<pre class="code-block"><code>COMPONENT:HERO_SECTION(Request& context)
|
||||||
{
|
{
|
||||||
String title = first(context.call["title"].to_string(), "Welcome");
|
String title = first(context.call["title"].to_string(), "Welcome");
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -10,7 +10,16 @@ RENDER(Request& context)
|
|||||||
<p>This is the content of Page 2. You can add more information here.</p>
|
<p>This is the content of Page 2. You can add more information here.</p>
|
||||||
<p>Sed at dolor leo. Morbi a tellus sed nisl dictum ultricies sit amet at purus. Nam mattis metus sed nunc egestas convallis.</p>
|
<p>Sed at dolor leo. Morbi a tellus sed nisl dictum ultricies sit amet at purus. Nam mattis metus sed nunc egestas convallis.</p>
|
||||||
<br/>
|
<br/>
|
||||||
<button onclick="$('#page2-section1').load('<?= starter_link("page2-section1", context) ?>')">Load new text</button>
|
<button id="page2-load-button" data-load-url="<?= starter_link("page2-section1", context) ?>" type="button">Load new text</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const button = document.getElementById('page2-load-button');
|
||||||
|
if (!button) return;
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
$('#page2-section1').load(button.dataset.loadUrl);
|
||||||
|
});
|
||||||
|
}());
|
||||||
|
</script>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,9 +5,9 @@ RENDER(Request& context)
|
|||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
context.var["starter"]["page_title"] = "Theme Preview";
|
context.var["starter"]["page_title"] = "Theme Preview";
|
||||||
starter_register_css("views/themes.css", context);
|
starter_register_css("views/themes.css", context);
|
||||||
String current_theme_key = starter_theme_value("key", context);
|
String current_theme_key = context.cfg.get_by_path("theme/key").to_string();
|
||||||
String theme_label = first(starter_theme_value("label", context), current_theme_key);
|
String theme_label = first(context.cfg.get_by_path("theme/label").to_string(), current_theme_key);
|
||||||
String theme_mode = starter_theme_value("mode", context);
|
String theme_mode = context.cfg.get_by_path("theme/mode").to_string();
|
||||||
StringMap theme_params;
|
StringMap theme_params;
|
||||||
theme_params["theme"] = current_theme_key;
|
theme_params["theme"] = current_theme_key;
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ RENDER(Request& context)
|
|||||||
starter_boot(context);
|
starter_boot(context);
|
||||||
context.var["starter"]["page_title"] = "Themes";
|
context.var["starter"]["page_title"] = "Themes";
|
||||||
starter_register_css("views/themes.css", context);
|
starter_register_css("views/themes.css", context);
|
||||||
String current_theme = starter_theme_value("key", context);
|
String current_theme = context.cfg.get_by_path("theme/key").to_string();
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<div class="theme-gallery-shell">
|
<div class="theme-gallery-shell">
|
||||||
@ -15,7 +15,7 @@ RENDER(Request& context)
|
|||||||
<p>The gallery renders the same preview content in each theme family. This makes it easier to judge shell fit, typography, token balance, and embed behavior.</p>
|
<p>The gallery renders the same preview content in each theme family. This makes it easier to judge shell fit, typography, token balance, and embed behavior.</p>
|
||||||
</section>
|
</section>
|
||||||
<div class="theme-gallery-grid">
|
<div class="theme-gallery-grid">
|
||||||
<? starter_cfg("theme/options", context).each([&](DTree theme_info, String theme_key) {
|
<? context.cfg.get_by_path("theme/options").each([&](DTree theme_info, String theme_key) {
|
||||||
StringMap params;
|
StringMap params;
|
||||||
params["theme"] = theme_key;
|
params["theme"] = theme_key;
|
||||||
String preview_href = starter_link("theme-preview", params, context);
|
String preview_href = starter_link("theme-preview", params, context);
|
||||||
|
|||||||
@ -7,7 +7,7 @@ RENDER(Request& context)
|
|||||||
|
|
||||||
DTree named_props;
|
DTree named_props;
|
||||||
named_props["title"] = "Named Render Example";
|
named_props["title"] = "Named Render Example";
|
||||||
named_props["body"] = "This content is rendered through the RENDER:BODY(Request& context) entry point.";
|
named_props["body"] = "This content is rendered through the COMPONENT:BODY(Request& context) entry point.";
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
|
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
|
||||||
@ -23,11 +23,11 @@ RENDER(Request& context)
|
|||||||
component_resolve("components/card"):
|
component_resolve("components/card"):
|
||||||
<code><?= component_resolve("components/card") ?></code>
|
<code><?= component_resolve("components/card") ?></code>
|
||||||
</p>
|
</p>
|
||||||
<h2>Default Render</h2>
|
<h2>Default Component</h2>
|
||||||
<div>
|
<div>
|
||||||
<?: component("components/card", card_props, context) ?>
|
<?: component("components/card", card_props, context) ?>
|
||||||
</div>
|
</div>
|
||||||
<h2>Named Render</h2>
|
<h2>Named Component</h2>
|
||||||
<? render_component("components/card:BODY", named_props, context); ?>
|
<? render_component("components/card:BODY", named_props, context); ?>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
|
|
||||||
RENDER(Request& context)
|
COMPONENT(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<section style="border:1px solid #ccc;padding:1em;margin:1em 0;">
|
<section style="border:1px solid #ccc;padding:1em;margin:1em 0;">
|
||||||
<? render_component("card:TITLE", context.call, context); ?>
|
<? render_component(":TITLE", context.call, context); ?>
|
||||||
<? render_component("card:BODY", context.call, context); ?>
|
<? render_component(":BODY", context.call, context); ?>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:TITLE(Request& context)
|
COMPONENT:TITLE(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<h3><?= first(context.call["title"].to_string(), "Component Title") ?></h3>
|
<h3><?= first(context.call["title"].to_string(), "Component Title") ?></h3>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER:BODY(Request& context)
|
COMPONENT:BODY(Request& context)
|
||||||
{
|
{
|
||||||
<>
|
<>
|
||||||
<p><?= first(context.call["body"].to_string(), "Component Body") ?></p>
|
<p><?= first(context.call["body"].to_string(), "Component Body") ?></p>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
RENDER(Request& context)
|
COMPONENT(Request& context)
|
||||||
{
|
{
|
||||||
String lang = first(context.call["lang"].to_string(), "plain");
|
String lang = first(context.call["lang"].to_string(), "plain");
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
RENDER(Request& context)
|
COMPONENT(Request& context)
|
||||||
{
|
{
|
||||||
String title = first(
|
String title = first(
|
||||||
context.call["node"]["attrs"]["title"].to_string(),
|
context.call["node"]["attrs"]["title"].to_string(),
|
||||||
|
|||||||
@ -146,6 +146,18 @@ String preprocess_named_render_syntax(String content)
|
|||||||
line = indent + "EXPORT void render_" + safe_name(render_name) + render_signature;
|
line = indent + "EXPORT void render_" + safe_name(render_name) + render_signature;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if(trimmed.rfind("COMPONENT:", 0) == 0)
|
||||||
|
{
|
||||||
|
String signature = trimmed.substr(10);
|
||||||
|
auto open_paren_pos = signature.find("(");
|
||||||
|
if(open_paren_pos != String::npos)
|
||||||
|
{
|
||||||
|
String render_name = trim(signature.substr(0, open_paren_pos));
|
||||||
|
String render_signature = signature.substr(open_paren_pos);
|
||||||
|
if(render_name != "")
|
||||||
|
line = indent + "EXPORT void component_render_" + safe_name(render_name) + render_signature;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result += line + line_break;
|
result += line + line_break;
|
||||||
current_line = "";
|
current_line = "";
|
||||||
@ -285,6 +297,7 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name)
|
|||||||
//setup_unit_paths(context, su, file_name);
|
//setup_unit_paths(context, su, file_name);
|
||||||
|
|
||||||
su->on_render = 0;
|
su->on_render = 0;
|
||||||
|
su->on_component = 0;
|
||||||
su->on_websocket = 0;
|
su->on_websocket = 0;
|
||||||
su->on_setup = 0;
|
su->on_setup = 0;
|
||||||
su->compiler_messages = "";
|
su->compiler_messages = "";
|
||||||
@ -308,6 +321,8 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name)
|
|||||||
printf("Error - %s in %s\n", error, su->file_name.c_str());
|
printf("Error - %s in %s\n", error, su->file_name.c_str());
|
||||||
su->on_render = (request_ref_handler)dlsym(su->so_handle, "render");
|
su->on_render = (request_ref_handler)dlsym(su->so_handle, "render");
|
||||||
dlerror();
|
dlerror();
|
||||||
|
su->on_component = (request_ref_handler)dlsym(su->so_handle, "component_render");
|
||||||
|
dlerror();
|
||||||
su->on_websocket = (request_ref_handler)dlsym(su->so_handle, "websocket");
|
su->on_websocket = (request_ref_handler)dlsym(su->so_handle, "websocket");
|
||||||
dlerror();
|
dlerror();
|
||||||
su->api_declarations = split(file_get_contents(su->api_file_name), "\n");
|
su->api_declarations = split(file_get_contents(su->api_file_name), "\n");
|
||||||
@ -466,6 +481,34 @@ SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct UnitInvocationScope
|
||||||
|
{
|
||||||
|
Request* context = 0;
|
||||||
|
String previous_working_directory;
|
||||||
|
String previous_unit_file;
|
||||||
|
|
||||||
|
UnitInvocationScope(Request* context, SharedUnit* su)
|
||||||
|
{
|
||||||
|
this->context = context;
|
||||||
|
previous_working_directory = get_cwd();
|
||||||
|
previous_unit_file = context->resources.current_unit_file;
|
||||||
|
set_cwd(su->src_path);
|
||||||
|
context->resources.current_unit_file = su->file_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
~UnitInvocationScope()
|
||||||
|
{
|
||||||
|
if(!context)
|
||||||
|
return;
|
||||||
|
context->resources.current_unit_file = previous_unit_file;
|
||||||
|
set_cwd(previous_working_directory);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
String component_normalize_path(String name)
|
String component_normalize_path(String name)
|
||||||
{
|
{
|
||||||
name = trim(name);
|
name = trim(name);
|
||||||
@ -477,26 +520,30 @@ String component_normalize_path(String name)
|
|||||||
void component_parse_target(String target, String& file_name, String& render_name)
|
void component_parse_target(String target, String& file_name, String& render_name)
|
||||||
{
|
{
|
||||||
target = trim(target);
|
target = trim(target);
|
||||||
render_name = "render";
|
render_name = "";
|
||||||
auto render_split_pos = target.find(":");
|
auto render_split_pos = target.find(":");
|
||||||
if(render_split_pos != String::npos)
|
if(render_split_pos != String::npos)
|
||||||
{
|
{
|
||||||
render_name = trim(target.substr(render_split_pos + 1));
|
render_name = trim(target.substr(render_split_pos + 1));
|
||||||
target = trim(target.substr(0, render_split_pos));
|
target = trim(target.substr(0, render_split_pos));
|
||||||
if(render_name == "")
|
|
||||||
render_name = "render";
|
|
||||||
}
|
}
|
||||||
file_name = target;
|
file_name = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
String component_resolve_path(String name)
|
String component_resolve_path(String name, Request* request_context = 0)
|
||||||
{
|
{
|
||||||
|
String target_name = trim(name);
|
||||||
String file_name;
|
String file_name;
|
||||||
String render_name;
|
String render_name;
|
||||||
component_parse_target(name, file_name, render_name);
|
component_parse_target(target_name, file_name, render_name);
|
||||||
|
|
||||||
if(file_name == "")
|
if(file_name == "")
|
||||||
return("");
|
{
|
||||||
|
if(target_name.rfind(":", 0) == 0 && request_context && request_context->resources.current_unit_file != "")
|
||||||
|
file_name = request_context->resources.current_unit_file;
|
||||||
|
else
|
||||||
|
return("");
|
||||||
|
}
|
||||||
|
|
||||||
StringList candidates;
|
StringList candidates;
|
||||||
auto push_candidate = [&] (String candidate) {
|
auto push_candidate = [&] (String candidate) {
|
||||||
@ -530,7 +577,7 @@ String component_resolve_path(String name)
|
|||||||
return("");
|
return("");
|
||||||
}
|
}
|
||||||
|
|
||||||
String render_handler_symbol(String render_name)
|
String page_render_handler_symbol(String render_name)
|
||||||
{
|
{
|
||||||
render_name = trim(render_name);
|
render_name = trim(render_name);
|
||||||
if(render_name == "" || render_name == "render")
|
if(render_name == "" || render_name == "render")
|
||||||
@ -538,9 +585,17 @@ String render_handler_symbol(String render_name)
|
|||||||
return("render_" + safe_name(render_name));
|
return("render_" + safe_name(render_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
request_ref_handler get_render_handler(SharedUnit* su, String render_name)
|
String component_handler_symbol(String render_name)
|
||||||
{
|
{
|
||||||
String symbol = render_handler_symbol(render_name);
|
render_name = trim(render_name);
|
||||||
|
if(render_name == "")
|
||||||
|
return("component_render");
|
||||||
|
return("component_render_" + safe_name(render_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
request_ref_handler get_page_render_handler(SharedUnit* su, String render_name)
|
||||||
|
{
|
||||||
|
String symbol = page_render_handler_symbol(render_name);
|
||||||
if(symbol == "render")
|
if(symbol == "render")
|
||||||
return(su->on_render);
|
return(su->on_render);
|
||||||
|
|
||||||
@ -554,6 +609,22 @@ request_ref_handler get_render_handler(SharedUnit* su, String render_name)
|
|||||||
return(handler);
|
return(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
request_ref_handler get_component_handler(SharedUnit* su, String render_name)
|
||||||
|
{
|
||||||
|
String symbol = component_handler_symbol(render_name);
|
||||||
|
if(symbol == "component_render")
|
||||||
|
return(su->on_component);
|
||||||
|
|
||||||
|
auto it = su->api_functions.find(symbol);
|
||||||
|
if(it != su->api_functions.end())
|
||||||
|
return((request_ref_handler)it->second);
|
||||||
|
|
||||||
|
auto handler = (request_ref_handler)dlsym(su->so_handle, symbol.c_str());
|
||||||
|
dlerror();
|
||||||
|
su->api_functions[symbol] = (void*)handler;
|
||||||
|
return(handler);
|
||||||
|
}
|
||||||
|
|
||||||
bool compiler_invoke_render(Request* context, String file_name, String render_name, String* error_out = 0)
|
bool compiler_invoke_render(Request* context, String file_name, String render_name, String* error_out = 0)
|
||||||
{
|
{
|
||||||
auto su = compiler_load_shared_unit(context, file_name, "", false);
|
auto su = compiler_load_shared_unit(context, file_name, "", false);
|
||||||
@ -567,7 +638,7 @@ bool compiler_invoke_render(Request* context, String file_name, String render_na
|
|||||||
return(false);
|
return(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto handler = get_render_handler(su, render_name);
|
auto handler = get_page_render_handler(su, render_name);
|
||||||
if(!handler)
|
if(!handler)
|
||||||
{
|
{
|
||||||
if(error_out)
|
if(error_out)
|
||||||
@ -580,11 +651,41 @@ bool compiler_invoke_render(Request* context, String file_name, String render_na
|
|||||||
return(false);
|
return(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
String prev_wd = get_cwd();
|
UnitInvocationScope invoke_scope(context, su);
|
||||||
set_cwd(su->src_path);
|
su->on_setup(context);
|
||||||
|
handler(*context);
|
||||||
|
return(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool compiler_invoke_component(Request* context, String file_name, String render_name, String* error_out = 0)
|
||||||
|
{
|
||||||
|
auto su = compiler_load_shared_unit(context, file_name, "", false);
|
||||||
|
if(!su)
|
||||||
|
return(false);
|
||||||
|
|
||||||
|
if(!su->on_setup)
|
||||||
|
{
|
||||||
|
if(error_out)
|
||||||
|
*error_out = "internal error: set_current_request() not defined in " + file_name;
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
return(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
UnitInvocationScope invoke_scope(context, su);
|
||||||
su->on_setup(context);
|
su->on_setup(context);
|
||||||
handler(*context);
|
handler(*context);
|
||||||
set_cwd(prev_wd);
|
|
||||||
return(true);
|
return(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,11 +719,9 @@ void compiler_invoke_websocket(Request* context, String file_name)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String prev_wd = get_cwd();
|
UnitInvocationScope invoke_scope(context, su);
|
||||||
set_cwd(su->src_path);
|
|
||||||
su->on_setup(context);
|
su->on_setup(context);
|
||||||
su->on_websocket(*context);
|
su->on_websocket(*context);
|
||||||
set_cwd(prev_wd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void render_file(String file_name)
|
void render_file(String file_name)
|
||||||
@ -638,7 +737,7 @@ void render_file(String file_name, Request& context)
|
|||||||
|
|
||||||
String component_resolve(String name)
|
String component_resolve(String name)
|
||||||
{
|
{
|
||||||
return(component_resolve_path(name));
|
return(component_resolve_path(name, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool component_exists(String name)
|
bool component_exists(String name)
|
||||||
@ -674,10 +773,10 @@ void render_component(String name, DTree props, Request& context)
|
|||||||
String render_name;
|
String render_name;
|
||||||
component_parse_target(name, file_name, render_name);
|
component_parse_target(name, file_name, render_name);
|
||||||
|
|
||||||
String resolved_name = component_resolve_path(file_name);
|
String resolved_name = component_resolve_path(name, &context);
|
||||||
if(resolved_name == "")
|
if(resolved_name == "")
|
||||||
{
|
{
|
||||||
print(component_error_banner("component not found: " + file_name));
|
print(component_error_banner("component not found: " + trim(name)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,7 +784,7 @@ void render_component(String name, DTree props, Request& context)
|
|||||||
context.call = props;
|
context.call = props;
|
||||||
|
|
||||||
String error_message = "";
|
String error_message = "";
|
||||||
if(!compiler_invoke_render(&context, resolved_name, render_name, &error_message) && error_message != "")
|
if(!compiler_invoke_component(&context, resolved_name, render_name, &error_message) && error_message != "")
|
||||||
print(component_error_banner(error_message));
|
print(component_error_banner(error_message));
|
||||||
|
|
||||||
context.call = previous_call;
|
context.call = previous_call;
|
||||||
@ -747,11 +846,9 @@ DTree* call_file(String file_name, String function_name, DTree* call_param)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
String prev_wd = get_cwd();
|
UnitInvocationScope invoke_scope(context, su);
|
||||||
set_cwd(su->src_path);
|
|
||||||
su->on_setup(context);
|
su->on_setup(context);
|
||||||
result = f(call_param);
|
result = f(call_param);
|
||||||
set_cwd(prev_wd);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define RENDER(X) extern "C" void render(Request& context)
|
#define RENDER(X) extern "C" void render(Request& context)
|
||||||
|
#define COMPONENT(X) extern "C" void component_render(Request& context)
|
||||||
#define WS(X) extern "C" void websocket(Request& context)
|
#define WS(X) extern "C" void websocket(Request& context)
|
||||||
#define EXPORT extern "C"
|
#define EXPORT extern "C"
|
||||||
|
|
||||||
|
|||||||
@ -67,13 +67,13 @@ String DTree::to_string()
|
|||||||
return("");
|
return("");
|
||||||
}
|
}
|
||||||
|
|
||||||
String DTree::to_json()
|
String DTree::to_json(char quote_char)
|
||||||
{
|
{
|
||||||
const DTree& target = deref();
|
const DTree& target = deref();
|
||||||
switch(target.type)
|
switch(target.type)
|
||||||
{
|
{
|
||||||
case('S'):
|
case('S'):
|
||||||
return(json_escape(target._String));
|
return(json_escape(target._String, quote_char));
|
||||||
break;
|
break;
|
||||||
case('F'):
|
case('F'):
|
||||||
return(std::to_string(target._float));
|
return(std::to_string(target._float));
|
||||||
@ -121,6 +121,41 @@ String DTree::get_type_name()
|
|||||||
return("unknown");
|
return("unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DTree DTree::get_by_path(String path, String delim)
|
||||||
|
{
|
||||||
|
const DTree* current = &deref();
|
||||||
|
if(path == "")
|
||||||
|
return(*current);
|
||||||
|
size_t start = 0;
|
||||||
|
while(start <= path.length())
|
||||||
|
{
|
||||||
|
size_t end = path.find(delim, start);
|
||||||
|
String segment;
|
||||||
|
if(end == String::npos)
|
||||||
|
segment = path.substr(start);
|
||||||
|
else
|
||||||
|
segment = path.substr(start, end - start);
|
||||||
|
if(segment == "")
|
||||||
|
{
|
||||||
|
if(end == String::npos)
|
||||||
|
break;
|
||||||
|
start = end + delim.length();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current = ¤t->deref();
|
||||||
|
if(current->type != 'M')
|
||||||
|
return(DTree());
|
||||||
|
auto it = current->_map.find(segment);
|
||||||
|
if(it == current->_map.end())
|
||||||
|
return(DTree());
|
||||||
|
current = &it->second;
|
||||||
|
if(end == String::npos)
|
||||||
|
break;
|
||||||
|
start = end + delim.length();
|
||||||
|
}
|
||||||
|
return(current->deref());
|
||||||
|
}
|
||||||
|
|
||||||
bool DTree::is_reference()
|
bool DTree::is_reference()
|
||||||
{
|
{
|
||||||
return(type == 'R');
|
return(type == 'R');
|
||||||
@ -374,16 +409,21 @@ String var_dump(DTree map, String prefix, String postfix)
|
|||||||
return(result);
|
return(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
String json_escape(String s)
|
String json_escape(String s, char quote_char)
|
||||||
{
|
{
|
||||||
//return(String("\"")+s+"\"");
|
//return(String("\"")+s+"\"");
|
||||||
String result;
|
String result;
|
||||||
u32 i = 0;
|
u32 i = 0;
|
||||||
result.append(1, '"');
|
result.append(1, quote_char);
|
||||||
while(i < s.length())
|
while(i < s.length())
|
||||||
{
|
{
|
||||||
char c = s[i];
|
char c = s[i];
|
||||||
switch(c)
|
if(c == quote_char)
|
||||||
|
{
|
||||||
|
result.append(1, '\\');
|
||||||
|
result.append(1, quote_char);
|
||||||
|
}
|
||||||
|
else switch(c)
|
||||||
{
|
{
|
||||||
case('\t'):
|
case('\t'):
|
||||||
result.append("\\t");
|
result.append("\\t");
|
||||||
@ -412,6 +452,6 @@ String json_escape(String s)
|
|||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
result.append(1, '"');
|
result.append(1, quote_char);
|
||||||
return(result);
|
return(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
String json_escape(String s);
|
String json_escape(String s, char quote_char = '"');
|
||||||
|
|
||||||
struct DTree {
|
struct DTree {
|
||||||
|
|
||||||
@ -16,8 +16,9 @@ struct DTree {
|
|||||||
void each(std::function <void (DTree t, String key)> f);
|
void each(std::function <void (DTree t, String key)> f);
|
||||||
bool is_array();
|
bool is_array();
|
||||||
String to_string();
|
String to_string();
|
||||||
String to_json();
|
String to_json(char quote_char = '"');
|
||||||
String get_type_name();
|
String get_type_name();
|
||||||
|
DTree get_by_path(String path, String delim = "/");
|
||||||
bool is_reference();
|
bool is_reference();
|
||||||
DTree* reference_target();
|
DTree* reference_target();
|
||||||
const DTree* reference_target() const;
|
const DTree* reference_target() const;
|
||||||
|
|||||||
@ -359,6 +359,11 @@ String html_escape(f64 a)
|
|||||||
return(std::to_string(a));
|
return(std::to_string(a));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String json_encode(String s, char quote_char)
|
||||||
|
{
|
||||||
|
return(json_escape(s, quote_char));
|
||||||
|
}
|
||||||
|
|
||||||
u64 int_val(String s, u32 base)
|
u64 int_val(String s, u32 base)
|
||||||
{
|
{
|
||||||
return(strtol(s.c_str(), 0, base));
|
return(strtol(s.c_str(), 0, base));
|
||||||
@ -386,7 +391,7 @@ String nibble(String& haystack, String delim)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String json_encode(DTree t)
|
String json_encode(DTree t, char quote_char)
|
||||||
{
|
{
|
||||||
String result = "";
|
String result = "";
|
||||||
if(t.is_array())
|
if(t.is_array())
|
||||||
@ -397,7 +402,7 @@ String json_encode(DTree t)
|
|||||||
if(count > 0)
|
if(count > 0)
|
||||||
result += ", ";
|
result += ", ";
|
||||||
count += 1;
|
count += 1;
|
||||||
result += json_escape(key) + ": " + json_encode(item);
|
result += json_escape(key, quote_char) + ": " + json_encode(item, quote_char);
|
||||||
});
|
});
|
||||||
result += "}";
|
result += "}";
|
||||||
}
|
}
|
||||||
@ -632,6 +637,11 @@ String ob_get_close()
|
|||||||
}
|
}
|
||||||
|
|
||||||
String safe_name(String raw)
|
String safe_name(String raw)
|
||||||
|
{
|
||||||
|
return(ascii_safe_name(raw));
|
||||||
|
}
|
||||||
|
|
||||||
|
String ascii_safe_name(String raw)
|
||||||
{
|
{
|
||||||
String result = "";
|
String result = "";
|
||||||
for(auto c : raw)
|
for(auto c : raw)
|
||||||
|
|||||||
@ -80,7 +80,8 @@ String html_escape(String s);
|
|||||||
String html_escape(u64 a);
|
String html_escape(u64 a);
|
||||||
String html_escape(f64 a);
|
String html_escape(f64 a);
|
||||||
|
|
||||||
String json_encode(DTree t);
|
String json_encode(String s, char quote_char = '"');
|
||||||
|
String json_encode(DTree t, char quote_char = '"');
|
||||||
DTree json_decode(String s);
|
DTree json_decode(String s);
|
||||||
|
|
||||||
String var_dump(StringMap map, String prefix = "", String postfix = "\n");
|
String var_dump(StringMap map, String prefix = "", String postfix = "\n");
|
||||||
@ -92,5 +93,6 @@ String ob_get();
|
|||||||
String ob_get_close();
|
String ob_get_close();
|
||||||
|
|
||||||
String safe_name(String raw);
|
String safe_name(String raw);
|
||||||
|
String ascii_safe_name(String raw);
|
||||||
|
|
||||||
#define is_bit_set(var,pos) ((var) & (1<<(pos)))
|
#define is_bit_set(var,pos) ((var) & (1<<(pos)))
|
||||||
|
|||||||
@ -129,6 +129,19 @@ String dirname(String fn)
|
|||||||
return(result);
|
return(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String path_join(String base, String child)
|
||||||
|
{
|
||||||
|
if(base == "")
|
||||||
|
return(child);
|
||||||
|
if(child == "")
|
||||||
|
return(base);
|
||||||
|
if(child[0] == '/')
|
||||||
|
return(child);
|
||||||
|
if(base[base.length() - 1] == '/')
|
||||||
|
return(base + child);
|
||||||
|
return(base + "/" + child);
|
||||||
|
}
|
||||||
|
|
||||||
bool mkdir(String path)
|
bool mkdir(String path)
|
||||||
{
|
{
|
||||||
shell_exec(String("mkdir -p ")+" "+shell_escape(path));
|
shell_exec(String("mkdir -p ")+" "+shell_escape(path));
|
||||||
|
|||||||
@ -6,6 +6,7 @@ String shell_exec(String cmd);
|
|||||||
String shell_escape(String raw);
|
String shell_escape(String raw);
|
||||||
String basename(String fn);
|
String basename(String fn);
|
||||||
String dirname(String fn);
|
String dirname(String fn);
|
||||||
|
String path_join(String base, String child);
|
||||||
bool mkdir(String path);
|
bool mkdir(String path);
|
||||||
bool file_exists(String path);
|
bool file_exists(String path);
|
||||||
String file_get_contents(String file_name);
|
String file_get_contents(String file_name);
|
||||||
|
|||||||
@ -14,6 +14,41 @@
|
|||||||
|
|
||||||
#include "types.h"
|
#include "types.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
String http_status_reason(s32 code)
|
||||||
|
{
|
||||||
|
switch(code)
|
||||||
|
{
|
||||||
|
case 200: return("OK");
|
||||||
|
case 201: return("Created");
|
||||||
|
case 202: return("Accepted");
|
||||||
|
case 204: return("No Content");
|
||||||
|
case 301: return("Moved Permanently");
|
||||||
|
case 302: return("Found");
|
||||||
|
case 303: return("See Other");
|
||||||
|
case 304: return("Not Modified");
|
||||||
|
case 307: return("Temporary Redirect");
|
||||||
|
case 308: return("Permanent Redirect");
|
||||||
|
case 400: return("Bad Request");
|
||||||
|
case 401: return("Unauthorized");
|
||||||
|
case 403: return("Forbidden");
|
||||||
|
case 404: return("Not Found");
|
||||||
|
case 405: return("Method Not Allowed");
|
||||||
|
case 409: return("Conflict");
|
||||||
|
case 422: return("Unprocessable Content");
|
||||||
|
case 429: return("Too Many Requests");
|
||||||
|
case 500: return("Internal Server Error");
|
||||||
|
case 501: return("Not Implemented");
|
||||||
|
case 502: return("Bad Gateway");
|
||||||
|
case 503: return("Service Unavailable");
|
||||||
|
case 504: return("Gateway Timeout");
|
||||||
|
default: return("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
SharedUnit::~SharedUnit()
|
SharedUnit::~SharedUnit()
|
||||||
{
|
{
|
||||||
if(so_handle)
|
if(so_handle)
|
||||||
@ -45,6 +80,17 @@ void Request::ob_start()
|
|||||||
ob = ob_stack.back();
|
ob = ob_stack.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Request::set_status(s32 code, String reason)
|
||||||
|
{
|
||||||
|
if(reason == "")
|
||||||
|
reason = http_status_reason(code);
|
||||||
|
String prefix = params["GATEWAY_INTERFACE"] != "" ? "Status: " : "HTTP/1.1 ";
|
||||||
|
response_code = prefix + std::to_string(code);
|
||||||
|
if(reason != "")
|
||||||
|
response_code += " " + reason;
|
||||||
|
flags.status = code;
|
||||||
|
}
|
||||||
|
|
||||||
Request::~Request()
|
Request::~Request()
|
||||||
{
|
{
|
||||||
for(auto* stream : ob_stack)
|
for(auto* stream : ob_stack)
|
||||||
|
|||||||
@ -80,6 +80,7 @@ struct SharedUnit {
|
|||||||
|
|
||||||
request_handler on_setup;
|
request_handler on_setup;
|
||||||
request_ref_handler on_render;
|
request_ref_handler on_render;
|
||||||
|
request_ref_handler on_component;
|
||||||
request_ref_handler on_websocket;
|
request_ref_handler on_websocket;
|
||||||
|
|
||||||
String compiler_messages;
|
String compiler_messages;
|
||||||
@ -128,6 +129,7 @@ struct Request {
|
|||||||
StringMap session;
|
StringMap session;
|
||||||
|
|
||||||
DTree var;
|
DTree var;
|
||||||
|
DTree cfg;
|
||||||
DTree call;
|
DTree call;
|
||||||
DTree connection;
|
DTree connection;
|
||||||
|
|
||||||
@ -181,10 +183,12 @@ struct Request {
|
|||||||
u8 websocket_opcode = 0;
|
u8 websocket_opcode = 0;
|
||||||
bool websocket_is_binary = false;
|
bool websocket_is_binary = false;
|
||||||
bool websocket_is_text = false;
|
bool websocket_is_text = false;
|
||||||
|
String current_unit_file = "";
|
||||||
std::string params_buffer;
|
std::string params_buffer;
|
||||||
} resources;
|
} resources;
|
||||||
|
|
||||||
void ob_start();
|
void ob_start();
|
||||||
|
void set_status(s32 code, String reason = "");
|
||||||
|
|
||||||
~Request();
|
~Request();
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user