diff --git a/README.md b/README.md index 08dcbd2..a24249d 100644 --- a/README.md +++ b/README.md @@ -247,11 +247,11 @@ The runtime reads its server settings from: The shipped example contains the important filesystem and FastCGI settings: ```ini -BIN_DIRECTORY=/tmp/uce/work -TMP_UPLOAD_PATH=/tmp/uce/uploads -SESSION_PATH=/tmp/uce/sessions +BIN_DIRECTORY=/var/cache/uce/work +TMP_UPLOAD_PATH=/var/lib/uce/uploads +SESSION_PATH=/var/lib/uce/sessions -FCGI_SOCKET_PATH=/run/uce.sock +FCGI_SOCKET_PATH=/run/uce/fastcgi.sock FCGI_PORT=9993 PRECOMPILE_FILES_IN= @@ -265,7 +265,7 @@ SESSION_TIME=2592000 For nginx deployments, the most important setting is: -- `FCGI_SOCKET_PATH=/run/uce.sock` +- `FCGI_SOCKET_PATH=/run/uce/fastcgi.sock` That is the Unix socket nginx should use for normal `.uce` requests. @@ -317,11 +317,35 @@ scripts/systemd/manage-uce-service.sh logs 200 The unit currently: -- creates `/tmp/uce/work`, `/tmp/uce/uploads`, and `/tmp/uce/sessions` -- removes any stale `/run/uce.sock` +- uses systemd-managed runtime/state/cache roots under: + - `/run/uce` + - `/var/lib/uce` + - `/var/cache/uce` +- prepares: + - `/var/cache/uce/work` + - `/var/lib/uce/uploads` + - `/var/lib/uce/sessions` +- removes any stale `/run/uce/fastcgi.sock` - rebuilds the runtime before start - runs the binary from the repo root so `COMPILER_SYS_PATH` resolves correctly +### Debian package build + +To build a Debian package from the repository root: + +```bash +bash scripts/make_deb.sh 0.1.2 +``` + +That script: + +- rebuilds the runtime first +- stages the current runtime tree under `/usr/lib/uce` +- installs `/etc/uce/settings.cfg` as a package conffile +- installs a packaged `uce.service` under `/lib/systemd/system/` +- writes Debian maintainer scripts for systemd reload/enable handling +- follows a more PHP-like/FHS deployment shape with immutable runtime files under `/usr/lib`, config under `/etc`, cache/state under `/var`, and the FastCGI socket under `/run/uce/` + ### 5. Configure nginx for `.uce` and `.ws.uce` You need two nginx paths for `.ws.uce` endpoints: @@ -359,7 +383,7 @@ server { fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_param DOCUMENT_URI $uri; fastcgi_param REQUEST_URI $request_uri; - fastcgi_pass unix:/run/uce.sock; + fastcgi_pass unix:/run/uce/fastcgi.sock; } location ~ \.ws\.uce$ { @@ -374,7 +398,7 @@ server { fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_param DOCUMENT_URI $uri; fastcgi_param REQUEST_URI $request_uri; - fastcgi_pass unix:/run/uce.sock; + fastcgi_pass unix:/run/uce/fastcgi.sock; } location @uce_websocket { diff --git a/dist/uce_0.1.2-1_amd64.deb b/dist/uce_0.1.2-1_amd64.deb new file mode 100644 index 0000000..df54d7d Binary files /dev/null and b/dist/uce_0.1.2-1_amd64.deb differ diff --git a/dist/uce_0.1.3-1_amd64.deb b/dist/uce_0.1.3-1_amd64.deb new file mode 100644 index 0000000..c02e12e Binary files /dev/null and b/dist/uce_0.1.3-1_amd64.deb differ diff --git a/dist/uce_0.1.4-1_amd64.deb b/dist/uce_0.1.4-1_amd64.deb new file mode 100644 index 0000000..6886b42 Binary files /dev/null and b/dist/uce_0.1.4-1_amd64.deb differ diff --git a/etc/uce/settings.cfg b/etc/uce/settings.cfg index 12c3cb6..41dfb26 100644 --- a/etc/uce/settings.cfg +++ b/etc/uce/settings.cfg @@ -1,10 +1,10 @@ # UCE WORKING DIRECTORIES -BIN_DIRECTORY=/tmp/uce/work -TMP_UPLOAD_PATH=/tmp/uce/uploads -SESSION_PATH=/tmp/uce/sessions +BIN_DIRECTORY=/var/cache/uce/work +TMP_UPLOAD_PATH=/var/lib/uce/uploads +SESSION_PATH=/var/lib/uce/sessions # LISTEN ON SOCKETS -FCGI_SOCKET_PATH=/run/uce.sock +FCGI_SOCKET_PATH=/run/uce/fastcgi.sock FCGI_PORT=9993 # OPTIONAL PROACTIVE COMPILE ROOT diff --git a/scripts/control b/scripts/control deleted file mode 100644 index fa9ece3..0000000 --- a/scripts/control +++ /dev/null @@ -1,7 +0,0 @@ -Package: uce -Section: web -Priority: optional -Architecture: amd64 -Depends: clang (>= 1:13.0) -Maintainer: Udo Schroeter -Description: Write server-side web stuff in C++ diff --git a/scripts/deb/conffiles b/scripts/deb/conffiles new file mode 100644 index 0000000..2b97c8d --- /dev/null +++ b/scripts/deb/conffiles @@ -0,0 +1 @@ +/etc/uce/settings.cfg diff --git a/scripts/deb/control.in b/scripts/deb/control.in new file mode 100644 index 0000000..7c1a31a --- /dev/null +++ b/scripts/deb/control.in @@ -0,0 +1,11 @@ +Package: @PACKAGE_NAME@ +Version: @VERSION@ +Section: web +Priority: optional +Architecture: @ARCH@ +Depends: bash, clang, libmariadb-dev | default-libmysqlclient-dev, systemd +Maintainer: Udo Schroeter +Installed-Size: @INSTALLED_SIZE@ +Description: UCE FastCGI runtime and live-compiling C++ web environment + UCE is an experimental C/C++ web runtime with FastCGI request handling, + on-demand page compilation, and a published site/doc/test tree. diff --git a/scripts/deb/postinst b/scripts/deb/postinst new file mode 100644 index 0000000..b825238 --- /dev/null +++ b/scripts/deb/postinst @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +if command -v systemctl >/dev/null 2>&1; then + systemctl daemon-reload >/dev/null 2>&1 || true + if [ "$1" = "configure" ]; then + systemctl enable uce.service >/dev/null 2>&1 || true + fi +fi + +exit 0 diff --git a/scripts/deb/postrm b/scripts/deb/postrm new file mode 100644 index 0000000..c78c423 --- /dev/null +++ b/scripts/deb/postrm @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +if command -v systemctl >/dev/null 2>&1; then + systemctl daemon-reload >/dev/null 2>&1 || true +fi + +exit 0 diff --git a/scripts/deb/prerm b/scripts/deb/prerm new file mode 100644 index 0000000..109b144 --- /dev/null +++ b/scripts/deb/prerm @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +if [ "$1" = "remove" ] || [ "$1" = "deconfigure" ]; then + if command -v systemctl >/dev/null 2>&1; then + systemctl disable --now uce.service >/dev/null 2>&1 || true + fi +fi + +exit 0 diff --git a/scripts/deb/uce.service b/scripts/deb/uce.service new file mode 100644 index 0000000..f17d30d --- /dev/null +++ b/scripts/deb/uce.service @@ -0,0 +1,25 @@ +[Unit] +Description=UCE FastCGI Runtime +After=network-online.target mariadb.service memcached.service +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=/usr/lib/uce +RuntimeDirectory=uce +StateDirectory=uce +CacheDirectory=uce +ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions +ExecStartPre=/usr/bin/rm -f /run/uce/fastcgi.sock +ExecStartPre=/usr/bin/bash /usr/lib/uce/scripts/build_linux.sh +ExecStart=/usr/lib/uce/bin/uce_fastcgi.linux.bin +ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock +Restart=always +RestartSec=2 +TimeoutStopSec=15 +KillMode=mixed +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/scripts/make_deb.sh b/scripts/make_deb.sh index 417d146..f7da601 100755 --- a/scripts/make_deb.sh +++ b/scripts/make_deb.sh @@ -1,46 +1,137 @@ -#!/bin/bash -cd "$(dirname "$0")" -cd .. +#!/usr/bin/env bash +set -euo pipefail -if [ -z "$1" ] - then - echo "Usage: make_deb.sh VERSION" - exit +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +DEB_ASSET_DIR="$SCRIPT_DIR/deb" +PACKAGE_NAME="uce" +REVISION="${UCE_DEB_REVISION:-1}" + +usage() { + cat <<'EOF' +Usage: + scripts/make_deb.sh VERSION + +Environment: + UCE_DEB_REVISION Debian package revision suffix (default: 1) + UCE_DEB_ARCH Override package architecture +EOF +} + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Required command not found: $1" >&2 + exit 1 + fi +} + +resolve_arch() { + if [[ -n "${UCE_DEB_ARCH:-}" ]]; then + printf '%s\n' "$UCE_DEB_ARCH" + return + fi + if command -v dpkg-architecture >/dev/null 2>&1; then + dpkg-architecture -qDEB_HOST_ARCH + return + fi + dpkg --print-architecture +} + +validate_version() { + local version="$1" + if [[ ! "$version" =~ ^[0-9][A-Za-z0-9.+:~]*$ ]]; then + echo "Invalid Debian version string: $version" >&2 + exit 1 + fi +} + +copy_payload() { + local destination="$1" + local path + for path in LICENSE README.md codesearch bin scripts site src; do + cp -a "$REPO_ROOT/$path" "$destination/" + done + mkdir -p "$destination/etc" + cp -a "$REPO_ROOT/etc/uce" "$destination/etc/" +} + +write_control_file() { + local output_file="$1" + local package_version="$2" + local arch="$3" + local installed_size="$4" + + sed \ + -e "s/@PACKAGE_NAME@/$PACKAGE_NAME/g" \ + -e "s/@VERSION@/$package_version/g" \ + -e "s/@ARCH@/$arch/g" \ + -e "s/@INSTALLED_SIZE@/$installed_size/g" \ + "$DEB_ASSET_DIR/control.in" > "$output_file" +} + +write_md5sums() { + local stage_dir="$1" + ( + cd "$stage_dir" + find usr etc lib -type f -print0 | sort -z | xargs -0 md5sum > DEBIAN/md5sums + ) +} + +if [[ $# -ne 1 ]]; then + usage >&2 + exit 1 fi -VERSION=$1 -REV=1 -PROJECT="uce" +VERSION="$1" +validate_version "$VERSION" -PKG_NAME="${PROJECT}_$VERSION.$REV" +require_command bash +require_command clang++ +require_command dpkg-deb +require_command dpkg +require_command install +require_command find +require_command xargs +require_command md5sum +require_command mysql_config +require_command du +require_command sed +require_command awk +require_command cp +require_command sort -echo "Making package $PKG_NAME" +ARCH="$(resolve_arch)" +PACKAGE_VERSION="${VERSION}-${REVISION}" +PACKAGE_BASENAME="${PACKAGE_NAME}_${PACKAGE_VERSION}_${ARCH}" +STAGE_DIR="$REPO_ROOT/pkg/$PACKAGE_BASENAME" +DEBIAN_DIR="$STAGE_DIR/DEBIAN" +INSTALL_ROOT="$STAGE_DIR/usr/lib/uce" +DIST_DIR="$REPO_ROOT/dist" +OUTPUT_DEB="$DIST_DIR/$PACKAGE_BASENAME.deb" + +echo "Making package $PACKAGE_BASENAME" echo "===================================" -scripts/build_linux.sh +bash "$REPO_ROOT/scripts/build_linux.sh" -rm -r "pkg/$PKG_NAME" -mkdir -p "pkg/$PKG_NAME/opt/uce/" -mkdir -p "pkg/$PKG_NAME/DEBIAN/" -mkdir -p "pkg/$PKG_NAME/etc/uce/" -mkdir -p "dist" +rm -rf -- "$STAGE_DIR" +mkdir -p "$DEBIAN_DIR" "$INSTALL_ROOT" "$STAGE_DIR/etc/uce" "$STAGE_DIR/lib/systemd/system" "$DIST_DIR" -cp scripts/control "pkg/$PKG_NAME/DEBIAN/" -echo "Version: $VERSION" >> "pkg/$PKG_NAME/DEBIAN/control" -echo "" >> "pkg/$PKG_NAME/DEBIAN/control" +copy_payload "$INSTALL_ROOT" -cp -r bin "pkg/$PKG_NAME/opt/uce/" -cp -r doc "pkg/$PKG_NAME/opt/uce/" -cp -r examples "pkg/$PKG_NAME/opt/uce/" -cp -r scripts "pkg/$PKG_NAME/opt/uce/" -cp -r test "pkg/$PKG_NAME/opt/uce/" -cp -r etc "pkg/$PKG_NAME/" +install -m 0644 "$REPO_ROOT/etc/uce/settings.cfg" "$STAGE_DIR/etc/uce/settings.cfg" +install -m 0644 "$DEB_ASSET_DIR/uce.service" "$STAGE_DIR/lib/systemd/system/uce.service" +install -m 0644 "$DEB_ASSET_DIR/conffiles" "$DEBIAN_DIR/conffiles" +install -m 0755 "$DEB_ASSET_DIR/postinst" "$DEBIAN_DIR/postinst" +install -m 0755 "$DEB_ASSET_DIR/prerm" "$DEBIAN_DIR/prerm" +install -m 0755 "$DEB_ASSET_DIR/postrm" "$DEBIAN_DIR/postrm" -du -sh "pkg/$PKG_NAME/"* +INSTALLED_SIZE="$(du -sk "$STAGE_DIR" | awk '{print $1}')" +write_control_file "$DEBIAN_DIR/control" "$PACKAGE_VERSION" "$ARCH" "$INSTALLED_SIZE" +write_md5sums "$STAGE_DIR" -cd pkg -dpkg-deb --build "$PKG_NAME" -mv "$PKG_NAME.deb" "../dist/" -cd .. +dpkg-deb --root-owner-group --build "$STAGE_DIR" "$OUTPUT_DEB" -ls -lh "dist/" | grep "$PKG_NAME" +dpkg-deb -I "$OUTPUT_DEB" | sed -n '1,80p' +echo +dpkg-deb -c "$OUTPUT_DEB" | sed -n '1,60p' diff --git a/scripts/systemd/manage-uce-service.sh b/scripts/systemd/manage-uce-service.sh index 3badab6..3ff653c 100755 --- a/scripts/systemd/manage-uce-service.sh +++ b/scripts/systemd/manage-uce-service.sh @@ -10,6 +10,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" UNIT_NAME="uce.service" UNIT_SOURCE="$SCRIPT_DIR/uce.service" +if [[ ( "$REPO_ROOT" == "/usr/lib/uce" || "$REPO_ROOT" == "/opt/uce" ) && -f "$REPO_ROOT/scripts/deb/uce.service" ]]; then + UNIT_SOURCE="$REPO_ROOT/scripts/deb/uce.service" +fi UNIT_DEST="/etc/systemd/system/$UNIT_NAME" CONFIG_SOURCE="$REPO_ROOT/etc/uce/settings.cfg" CONFIG_DEST="/etc/uce/settings.cfg" diff --git a/scripts/systemd/uce.service b/scripts/systemd/uce.service index 59a91d2..53cc91c 100644 --- a/scripts/systemd/uce.service +++ b/scripts/systemd/uce.service @@ -6,11 +6,14 @@ Wants=network-online.target [Service] Type=simple WorkingDirectory=/Code/uce.openfu.com/uce -ExecStartPre=/usr/bin/mkdir -p /tmp/uce/work /tmp/uce/uploads /tmp/uce/sessions -ExecStartPre=/usr/bin/rm -f /run/uce.sock +RuntimeDirectory=uce +StateDirectory=uce +CacheDirectory=uce +ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions +ExecStartPre=/usr/bin/rm -f /run/uce/fastcgi.sock ExecStartPre=/usr/bin/bash /Code/uce.openfu.com/uce/scripts/build_linux.sh ExecStart=/Code/uce.openfu.com/uce/bin/uce_fastcgi.linux.bin -ExecStopPost=/usr/bin/rm -f /run/uce.sock +ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock Restart=always RestartSec=2 TimeoutStopSec=15 diff --git a/site/doc/index.uce b/site/doc/index.uce index 2d472eb..70cb1d4 100644 --- a/site/doc/index.uce +++ b/site/doc/index.uce @@ -188,6 +188,10 @@ RENDER(Request& context) nibble(s, "-"); ?>
  • :
    types + +:related +**PHP:** `array_merge()` +**JavaScript / Node.js:** Object spread, `Object.assign()`, or array concatenation depending on whether the data is map-like or list-like \ No newline at end of file diff --git a/site/doc/pages/contains.txt b/site/doc/pages/contains.txt new file mode 100644 index 0000000..dc00be6 --- /dev/null +++ b/site/doc/pages/contains.txt @@ -0,0 +1,19 @@ +:sig +bool contains(String haystack, String needle) + +:params +haystack : string to search in +needle : substring to look for +return value : `true` when `needle` appears in `haystack`, otherwise `false` + +:desc +Returns whether `haystack` contains `needle`. + +An empty `needle` always returns `true`. + +:see +>string + +:related +**PHP:** `str_contains()` +**JavaScript / Node.js:** `String.prototype.includes()` \ No newline at end of file diff --git a/site/doc/pages/redirect.txt b/site/doc/pages/redirect.txt new file mode 100644 index 0000000..bfae26d --- /dev/null +++ b/site/doc/pages/redirect.txt @@ -0,0 +1,19 @@ +:sig +void redirect(String url, s32 code = 302) + +:params +url : target URL for the redirect +code : optional HTTP redirect status, defaults to `302` + +:desc +Sets the `Location` response header and updates the current HTTP status code. + +Use this helper instead of manually assigning `context.header["Location"]` and `context.set_status(...)` when you want to redirect the current response. + +:see +set_status +0_context + +:related +**PHP:** `header("Location: ...")` together with `http_response_code()` or implicit redirect semantics +**JavaScript / Node.js:** `res.redirect(...)`, setting `Location`, or returning a redirect `Response` \ No newline at end of file diff --git a/site/doc/pages/str_ends_with.txt b/site/doc/pages/str_ends_with.txt new file mode 100644 index 0000000..fe6ddc4 --- /dev/null +++ b/site/doc/pages/str_ends_with.txt @@ -0,0 +1,17 @@ +:sig +bool str_ends_with(String haystack, String needle) + +:params +haystack : string to inspect +needle : suffix to compare against the end of `haystack` +return value : `true` when `haystack` ends with `needle`, otherwise `false` + +:desc +Checks whether `haystack` ends with the given suffix. + +:see +>string + +:related +**PHP:** `str_ends_with()` +**JavaScript / Node.js:** `String.prototype.endsWith()` \ No newline at end of file diff --git a/site/doc/pages/str_starts_with.txt b/site/doc/pages/str_starts_with.txt new file mode 100644 index 0000000..3c2a0aa --- /dev/null +++ b/site/doc/pages/str_starts_with.txt @@ -0,0 +1,17 @@ +:sig +bool str_starts_with(String haystack, String needle) + +:params +haystack : string to inspect +needle : prefix to compare against the start of `haystack` +return value : `true` when `haystack` begins with `needle`, otherwise `false` + +:desc +Checks whether `haystack` starts with the given prefix. + +:see +>string + +:related +**PHP:** `str_starts_with()` +**JavaScript / Node.js:** `String.prototype.startsWith()` \ No newline at end of file diff --git a/site/doc/pages/strpos.txt b/site/doc/pages/strpos.txt new file mode 100644 index 0000000..3bcddab --- /dev/null +++ b/site/doc/pages/strpos.txt @@ -0,0 +1,22 @@ +:sig +s64 strpos(String haystack, String needle, s64 offset = 0) + +:params +haystack : string to search in +needle : substring to search for +offset : optional start offset; negative values count from the end of the string +return value : zero-based position of the first match, or `-1` if `needle` is not found + +:desc +Finds the first occurrence of `needle` inside `haystack`. + +This is the closest UCE equivalent to PHP `strpos()`, but it returns `-1` instead of `false` when no match is found. + +If `needle` is an empty string, `strpos()` returns the normalized start offset. + +:see +>string + +:related +**PHP:** `strpos()` +**JavaScript / Node.js:** `String.prototype.indexOf()` \ No newline at end of file diff --git a/site/doc/pages/substr.txt b/site/doc/pages/substr.txt new file mode 100644 index 0000000..fef930d --- /dev/null +++ b/site/doc/pages/substr.txt @@ -0,0 +1,25 @@ +:sig +String substr(String s, s64 start_pos) +String substr(String s, s64 start_pos, s64 length) + +:params +s : source string +start_pos : zero-based start position; negative values count from the end of the string +length : optional substring length; negative values trim bytes from the end of the result range +return value : extracted substring, or an empty string if the requested range is outside the source string + +:desc +Extracts part of a string using PHP-style start and length semantics. + +Use the two-argument form to return everything from `start_pos` to the end of the string. + +If `start_pos` is negative, counting starts from the end of the string. + +If `length` is negative, the returned range stops that many bytes before the end of the string. + +:see +>string + +:related +**PHP:** `substr()` +**JavaScript / Node.js:** `String.prototype.slice()` or `substring()` \ No newline at end of file diff --git a/site/doc/search.uce b/site/doc/search.uce index 7d93091..fb32413 100644 --- a/site/doc/search.uce +++ b/site/doc/search.uce @@ -1,10 +1,4 @@ -// Returns whether haystack contains needle (case-sensitive, both should be pre-lowercased) -bool str_contains(String haystack, String needle) -{ - return replace(haystack, needle, "\x01") != haystack; -} - void render_hit(String ft, String name, String badge, String snippet) { <>
    @@ -51,8 +45,8 @@ RENDER(Request& context) } String content = file_get_contents("pages/" + ft + ".txt"); - bool name_match = str_contains(to_lower(name), q); - bool content_match = str_contains(to_lower(content), q); + bool name_match = contains(to_lower(name), q); + bool content_match = contains(to_lower(content), q); if(!name_match && !content_match) continue; @@ -64,7 +58,7 @@ RENDER(Request& context) for(auto line : split(content, "\n")) { String t = trim(line); - if(t.length() > 4 && t.substr(0, 1) != ":" && str_contains(to_lower(t), q)) + if(t.length() > 4 && t.substr(0, 1) != ":" && contains(to_lower(t), q)) { snippet = t; if(snippet.length() > 120) diff --git a/site/doc/singlepage.uce b/site/doc/singlepage.uce index a0a739d..9b392d6 100644 --- a/site/doc/singlepage.uce +++ b/site/doc/singlepage.uce @@ -111,13 +111,14 @@ void render_see_section(String name) RENDER(Request& context) { - already_shown_items = new StringMap(); + StringMap shown_items; + already_shown_items = &shown_items; String page = first(context.get["p"], "index"); <> - +

    diff --git a/site/doc/style.css b/site/doc/style.css index 8eefed1..5d2b492 100644 --- a/site/doc/style.css +++ b/site/doc/style.css @@ -1,24 +1,29 @@ /* UCE Documentation — Theme - Color palette: #139 deep-blue base · white text · yellow accents */ + Color palette: deep blue gradient · warm gold accents · frosted glass surfaces */ :root { - --bg: #113399; - --bg-surface: rgba(255, 255, 255, 0.065); - --bg-surface-hover: rgba(255, 255, 255, 0.11); - --bg-inset: rgba(0, 0, 0, 0.22); - --bg-code: rgba(0, 0, 0, 0.28); - --text: #e8ecf4; - --text-dim: rgba(255, 255, 255, 0.55); - --text-muted: rgba(255, 255, 255, 0.35); - --accent: #ffd644; - --accent-dim: rgba(255, 214, 68, 0.1); - --accent-hover: #ffe680; - --border: rgba(255, 255, 255, 0.08); - --border-strong: rgba(255, 255, 255, 0.18); - --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; - --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace; - --radius: 8px; - --radius-lg: 14px; + --bg: #0b1d4e; + --bg-grad: linear-gradient(168deg, #0f2462 0%, #0b1d4e 40%, #091840 100%); + --bg-surface: rgba(255, 255, 255, 0.05); + --bg-surface-hover: rgba(255, 255, 255, 0.09); + --bg-inset: rgba(0, 0, 0, 0.3); + --bg-code: rgba(0, 0, 0, 0.32); + --text: #dfe5f2; + --text-dim: rgba(200, 210, 235, 0.6); + --text-muted: rgba(200, 210, 235, 0.35); + --accent: #f0c430; + --accent-dim: rgba(240, 196, 48, 0.08); + --accent-hover: #ffd95c; + --accent-glow: rgba(240, 196, 48, 0.15); + --border: rgba(255, 255, 255, 0.06); + --border-strong: rgba(255, 255, 255, 0.14); + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2); + --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.25); + --shadow-glow: 0 0 20px rgba(240, 196, 48, 0.06); + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + --font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace; + --radius: 10px; + --radius-lg: 16px; --ease: cubic-bezier(0.4, 0, 0.2, 1); } @@ -52,8 +57,10 @@ body { font-family: var(--font-sans); font-size: 1rem; line-height: 1.65; - background: var(--bg); + background: var(--bg-grad); + background-attachment: fixed; color: var(--text); + min-height: 100vh; } /* ── Typography ── */ @@ -63,8 +70,9 @@ h1 { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; - padding: 28px 0 20px; - border-bottom: 1px solid var(--border); + padding: 32px 0 20px; + border-bottom: 2px solid transparent; + border-image: linear-gradient(90deg, var(--accent) 0%, rgba(240, 196, 48, 0.15) 100%) 1; margin-bottom: 28px; } @@ -131,15 +139,18 @@ a:hover { .category { background: var(--bg-surface); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 18px; margin-bottom: 10px; - transition: border-color 150ms var(--ease); + transition: border-color 200ms var(--ease), box-shadow 200ms var(--ease); } .category:hover { border-color: var(--border-strong); + box-shadow: var(--shadow-sm); } .category h3 { @@ -190,23 +201,25 @@ a:hover { display: block; padding: 7px 12px; border-radius: 6px; - transition: background 150ms var(--ease); + transition: background 150ms var(--ease), transform 150ms var(--ease); } .func-item a:hover { background: var(--bg-surface-hover); text-decoration: none; + transform: translateX(2px); } .badge { - font-size: 0.68rem; - font-weight: 500; - padding: 1px 7px; - border-radius: 4px; - margin-left: 6px; + font-size: 0.65rem; + font-weight: 600; + padding: 2px 8px; + border-radius: 10px; + margin-left: 8px; vertical-align: middle; - background: rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.06); color: var(--text-dim); + letter-spacing: 0.02em; } .dim { @@ -221,10 +234,13 @@ a:hover { .doc-section { background: var(--bg-surface); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 24px; margin-bottom: 16px; + box-shadow: var(--shadow-sm); } .doc-section h3 { @@ -240,12 +256,22 @@ a:hover { line-height: 1.5; background: var(--bg-code); border-color: var(--border-strong); + box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.15); } .sig div { font-family: var(--font-mono); } +.sig pre { + background: none; + border: none; + padding: 0; + margin: 0; + font-size: inherit; + line-height: inherit; +} + .params div { padding: 8px 0; border-bottom: 1px solid var(--border); @@ -261,6 +287,23 @@ a:hover { font-weight: 500; } +.related { + background: var(--accent-dim); + border: 1px solid rgba(240, 196, 48, 0.12); + border-radius: var(--radius); + padding: 14px 20px; + font-size: 0.9rem; + color: var(--text-dim); +} + +.related div { + padding: 2px 0; +} + +.related strong { + color: var(--text); +} + .see { background: transparent; border: none; @@ -333,9 +376,10 @@ pre { border: 1px solid var(--border); border-radius: var(--radius); font-family: var(--font-mono); - font-size: 0.88rem; - line-height: 1.55; + font-size: 0.86rem; + line-height: 1.6; white-space: pre-wrap; + box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.12); } code { @@ -365,9 +409,12 @@ li { details { background: var(--bg-surface); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 16px; + box-shadow: var(--shadow-sm); } summary { @@ -421,7 +468,7 @@ details pre { #doc-search:focus { outline: none; border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(255, 214, 68, 0.12); + box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-glow); } .search-count { @@ -445,13 +492,14 @@ details pre { display: flex; align-items: baseline; gap: 10px; - padding: 5px 12px; + padding: 6px 12px; border-radius: 6px; - transition: background 120ms var(--ease); + transition: background 120ms var(--ease), transform 120ms var(--ease); } .search-hit:hover { background: var(--bg-surface-hover); + transform: translateX(2px); } .search-hit a { diff --git a/site/test/dtree.uce b/site/test/dtree.uce index b8e7bb7..e8b41d7 100644 --- a/site/test/dtree.uce +++ b/site/test/dtree.uce @@ -3,12 +3,6 @@ RENDER(Request& context) { DTree t; - DTree assoc_left; - DTree assoc_right; - DTree assoc_merged; - DTree list_left; - DTree list_right; - DTree list_merged; <> @@ -17,25 +11,25 @@ RENDER(Request& context) DTree

    - Value Types: +

    Value Types

    - Tree: +

    Nested Tree

    - Array Merge +

    Array Merge — Associative

    - Params -
    +

    Array Merge — List

    + +
    + +
    + Request Parameters +
    +
    } diff --git a/site/test/string.uce b/site/test/string.uce index 9283598..5e0b154 100644 --- a/site/test/string.uce +++ b/site/test/string.uce @@ -3,32 +3,100 @@ +void test_row(String label, String result) +{ + <> +} + +void test_row(String label, String result, String expect) +{ + bool pass = (result == expect); + <> + + + "> + +} + +void test_num(String label, s64 result, s64 expect) +{ + bool pass = (result == expect); + <> + + + "> + +} + RENDER(Request& context) { String sample = " Hello UCE World "; <> +

    UCE Test: Strings

    -
    sample = ""

    - print("sample: [", sample, "]\n"); - print("trim(sample): [", trim(sample), "]\n"); - print("substr(sample, 2, 5): [", substr(sample, 2, 5), "]\n"); - print("substr(sample, -7): [", substr(sample, -7), "]\n"); - print("strpos(sample, \"UCE\"): ", strpos(sample, "UCE"), "\n"); - print("contains(sample, \"World\"): ", contains(sample, "World") ? "true" : "false", "\n"); - print("str_starts_with(trim(sample), \"Hello\"): ", str_starts_with(trim(sample), "Hello") ? "true" : "false", "\n"); - print("str_ends_with(trim(sample), \"World\"): ", str_ends_with(trim(sample), "World") ? "true" : "false", "\n"); +

    trim

    +
    - ?>
    +

    substr

    +
    - Params -
    +

    strpos

    +
    + +

    contains

    +
    + +

    str_starts_with / str_ends_with

    +
    + +

    replace / split / join

    +
    + +

    to_upper / to_lower

    +
    + +
    + Request Parameters +
    +
    } diff --git a/site/test/style.css b/site/test/style.css index 9b27be3..51966a4 100644 --- a/site/test/style.css +++ b/site/test/style.css @@ -1,24 +1,28 @@ /* UCE Test Suite — Theme - Color palette: #139 deep-blue base · white text · yellow accents */ + Color palette: deep blue gradient · warm gold accents · frosted glass surfaces */ :root { - --bg: #113399; - --bg-surface: rgba(255, 255, 255, 0.065); - --bg-surface-hover: rgba(255, 255, 255, 0.11); - --bg-inset: rgba(0, 0, 0, 0.22); - --bg-code: rgba(0, 0, 0, 0.28); - --text: #e8ecf4; - --text-dim: rgba(255, 255, 255, 0.55); - --text-muted: rgba(255, 255, 255, 0.35); - --accent: #ffd644; - --accent-dim: rgba(255, 214, 68, 0.1); - --accent-hover: #ffe680; - --border: rgba(255, 255, 255, 0.08); - --border-strong: rgba(255, 255, 255, 0.18); - --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; - --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace; - --radius: 8px; - --radius-lg: 14px; + --bg: #0b1d4e; + --bg-grad: linear-gradient(168deg, #0f2462 0%, #0b1d4e 40%, #091840 100%); + --bg-surface: rgba(255, 255, 255, 0.05); + --bg-surface-hover: rgba(255, 255, 255, 0.09); + --bg-inset: rgba(0, 0, 0, 0.3); + --bg-code: rgba(0, 0, 0, 0.32); + --text: #dfe5f2; + --text-dim: rgba(200, 210, 235, 0.6); + --text-muted: rgba(200, 210, 235, 0.35); + --accent: #f0c430; + --accent-dim: rgba(240, 196, 48, 0.08); + --accent-hover: #ffd95c; + --accent-glow: rgba(240, 196, 48, 0.15); + --border: rgba(255, 255, 255, 0.06); + --border-strong: rgba(255, 255, 255, 0.14); + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2); + --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.25); + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + --font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace; + --radius: 10px; + --radius-lg: 16px; --ease: cubic-bezier(0.4, 0, 0.2, 1); } @@ -52,8 +56,10 @@ body { font-family: var(--font-sans); font-size: 1rem; line-height: 1.65; - background: var(--bg); + background: var(--bg-grad); + background-attachment: fixed; color: var(--text); + min-height: 100vh; } /* ── Typography ── */ @@ -63,8 +69,9 @@ h1 { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; - padding: 28px 0 20px; - border-bottom: 1px solid var(--border); + padding: 32px 0 20px; + border-bottom: 2px solid transparent; + border-image: linear-gradient(90deg, var(--accent) 0%, rgba(240, 196, 48, 0.15) 100%) 1; margin-bottom: 28px; } @@ -117,10 +124,13 @@ body > section, body > div:not(.test-grid):not(.system-info):not(.grid-heading), body > p { background: var(--bg-surface); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 24px; margin-bottom: 16px; + box-shadow: var(--shadow-sm); } /* ── Test Card Grid ── */ @@ -138,18 +148,22 @@ body > p { gap: 4px; padding: 16px 20px; background: var(--bg-surface); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: var(--radius); text-decoration: none; color: var(--text); - transition: all 150ms var(--ease); + box-shadow: var(--shadow-sm); + transition: all 200ms var(--ease); } .test-card:hover { background: var(--bg-surface-hover); border-color: var(--border-strong); text-decoration: none; - transform: translateY(-1px); + transform: translateY(-2px); + box-shadow: var(--shadow-md); } .test-card strong { @@ -263,8 +277,9 @@ pre { border-radius: var(--radius); font-family: var(--font-mono); font-size: 0.85rem; - line-height: 1.55; + line-height: 1.6; white-space: pre-wrap; + box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.12); } code { @@ -302,8 +317,11 @@ li a { margin-top: 32px; padding: 16px 20px; background: var(--bg-code); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: var(--radius); + box-shadow: var(--shadow-sm); } .system-info h3 { @@ -322,9 +340,12 @@ li a { details { background: var(--bg-surface); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 16px; + box-shadow: var(--shadow-sm); } summary {