Refactor SX primitives: modular, isomorphic, general-purpose
Spec modularization: - Add (define-module :name) markers to primitives.sx creating 11 modules (7 core, 4 stdlib). Bootstrappers can now selectively include modules. - Add parse_primitives_by_module() to boundary_parser.py. - Remove split-ids primitive; inline at 4 call sites in blog/market queries. Python file split: - primitives.py: slimmed to registry + core primitives only (~350 lines) - primitives_stdlib.py: NEW — stdlib primitives (format, text, style, debug) - primitives_ctx.py: NEW — extracted 12 page context builders from IO - primitives_io.py: add register_io_handler decorator, auto-derive IO_PRIMITIVES from registry, move sync IO bridges here JS parity fixes: - = uses === (strict equality), != uses !== - round supports optional ndigits parameter - concat uses nil-check not falsy-check (preserves 0, "", false) - escape adds single quote entity (') matching Python/markupsafe - assert added (was missing from JS entirely) Bootstrapper modularization: - PRIMITIVES_JS_MODULES / PRIMITIVES_PY_MODULES dicts keyed by module - --modules CLI flag for selective inclusion (core.* always included) - Regenerated sx-ref.js and sx_ref.py with all fixes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,8 @@
|
|||||||
|
|
||||||
(defquery posts-by-ids (&key ids)
|
(defquery posts-by-ids (&key ids)
|
||||||
"Fetch multiple blog posts by comma-separated IDs."
|
"Fetch multiple blog posts by comma-separated IDs."
|
||||||
(service "blog" "get-posts-by-ids" :ids (split-ids ids)))
|
(service "blog" "get-posts-by-ids"
|
||||||
|
:ids (map parse-int (filter (fn (s) (not (empty? s))) (split (str ids) ",")))))
|
||||||
|
|
||||||
(defquery search-posts (&key query page per-page)
|
(defquery search-posts (&key query page per-page)
|
||||||
"Search blog posts by text query, paginated."
|
"Search blog posts by text query, paginated."
|
||||||
@@ -35,4 +36,5 @@
|
|||||||
(defquery page-configs-batch (&key container-type ids)
|
(defquery page-configs-batch (&key container-type ids)
|
||||||
"Return PageConfigs for multiple container IDs (comma-separated)."
|
"Return PageConfigs for multiple container IDs (comma-separated)."
|
||||||
(service "page-config" "get-batch"
|
(service "page-config" "get-batch"
|
||||||
:container-type container-type :ids (split-ids ids)))
|
:container-type container-type
|
||||||
|
:ids (map parse-int (filter (fn (s) (not (empty? s))) (split (str ids) ",")))))
|
||||||
|
|||||||
@@ -7,8 +7,10 @@
|
|||||||
|
|
||||||
(defquery products-by-ids (&key ids)
|
(defquery products-by-ids (&key ids)
|
||||||
"Return product details for comma-separated IDs."
|
"Return product details for comma-separated IDs."
|
||||||
(service "market-data" "products-by-ids" :ids (split-ids ids)))
|
(service "market-data" "products-by-ids"
|
||||||
|
:ids (map parse-int (filter (fn (s) (not (empty? s))) (split (str ids) ",")))))
|
||||||
|
|
||||||
(defquery marketplaces-by-ids (&key ids)
|
(defquery marketplaces-by-ids (&key ids)
|
||||||
"Return marketplace data for comma-separated IDs."
|
"Return marketplace data for comma-separated IDs."
|
||||||
(service "market-data" "marketplaces-by-ids" :ids (split-ids ids)))
|
(service "market-data" "marketplaces-by-ids"
|
||||||
|
:ids (map parse-int (filter (fn (s) (not (empty? s))) (split (str ids) ",")))))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,7 @@ from .primitives import (
|
|||||||
get_primitive,
|
get_primitive,
|
||||||
register_primitive,
|
register_primitive,
|
||||||
)
|
)
|
||||||
|
from . import primitives_stdlib # noqa: F401 — registers stdlib primitives
|
||||||
from .env import Env
|
from .env import Env
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|||||||
import math
|
import math
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from .types import Keyword, Lambda, NIL
|
from .types import Keyword, NIL
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -265,12 +265,6 @@ def prim_join(sep: str, coll: list) -> str:
|
|||||||
def prim_replace(s: str, old: str, new: str) -> str:
|
def prim_replace(s: str, old: str, new: str) -> str:
|
||||||
return s.replace(old, new)
|
return s.replace(old, new)
|
||||||
|
|
||||||
@register_primitive("strip-tags")
|
|
||||||
def prim_strip_tags(s: str) -> str:
|
|
||||||
"""Strip HTML tags from a string."""
|
|
||||||
import re
|
|
||||||
return re.sub(r"<[^>]+>", "", s)
|
|
||||||
|
|
||||||
@register_primitive("slice")
|
@register_primitive("slice")
|
||||||
def prim_slice(coll: Any, start: int, end: Any = None) -> Any:
|
def prim_slice(coll: Any, start: int, end: Any = None) -> Any:
|
||||||
"""Slice a string or list: (slice coll start end?)."""
|
"""Slice a string or list: (slice coll start end?)."""
|
||||||
@@ -432,181 +426,3 @@ def prim_into(target: Any, coll: Any) -> Any:
|
|||||||
return result
|
return result
|
||||||
raise ValueError(f"into: unsupported target type {type(target).__name__}")
|
raise ValueError(f"into: unsupported target type {type(target).__name__}")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Format helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@register_primitive("format-date")
|
|
||||||
def prim_format_date(date_str: Any, fmt: str) -> str:
|
|
||||||
"""``(format-date date-str fmt)`` → formatted date string."""
|
|
||||||
from datetime import datetime
|
|
||||||
try:
|
|
||||||
dt = datetime.fromisoformat(str(date_str))
|
|
||||||
return dt.strftime(fmt)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return str(date_str) if date_str else ""
|
|
||||||
|
|
||||||
|
|
||||||
@register_primitive("format-decimal")
|
|
||||||
def prim_format_decimal(val: Any, places: Any = 2) -> str:
|
|
||||||
"""``(format-decimal val places)`` → formatted decimal string."""
|
|
||||||
try:
|
|
||||||
return f"{float(val):.{int(places)}f}"
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return "0." + "0" * int(places)
|
|
||||||
|
|
||||||
|
|
||||||
@register_primitive("parse-int")
|
|
||||||
def prim_parse_int(val: Any, default: Any = 0) -> int | Any:
|
|
||||||
"""``(parse-int val default?)`` → int(val) with fallback."""
|
|
||||||
try:
|
|
||||||
return int(val)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
@register_primitive("parse-datetime")
|
|
||||||
def prim_parse_datetime(val: Any) -> Any:
|
|
||||||
"""``(parse-datetime "2024-01-15T10:00:00")`` → ISO string or nil."""
|
|
||||||
from datetime import datetime
|
|
||||||
if not val or val is NIL:
|
|
||||||
return NIL
|
|
||||||
try:
|
|
||||||
dt = datetime.fromisoformat(str(val))
|
|
||||||
return dt.isoformat()
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return NIL
|
|
||||||
|
|
||||||
|
|
||||||
@register_primitive("split-ids")
|
|
||||||
def prim_split_ids(val: Any) -> list[int]:
|
|
||||||
"""``(split-ids "1,2,3")`` → [1, 2, 3]. Parse comma-separated int IDs."""
|
|
||||||
if not val or val is NIL:
|
|
||||||
return []
|
|
||||||
return [int(x.strip()) for x in str(val).split(",") if x.strip()]
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Assertions
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@register_primitive("assert")
|
|
||||||
def prim_assert(condition: Any, message: str = "Assertion failed") -> bool:
|
|
||||||
if not condition:
|
|
||||||
raise RuntimeError(f"Assertion error: {message}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Text helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@register_primitive("pluralize")
|
|
||||||
def prim_pluralize(count: Any, singular: str = "", plural: str = "s") -> str:
|
|
||||||
"""``(pluralize count)`` → "s" if count != 1, else "".
|
|
||||||
``(pluralize count "item" "items")`` → "item" or "items"."""
|
|
||||||
try:
|
|
||||||
n = int(count)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
n = 0
|
|
||||||
if singular or plural != "s":
|
|
||||||
return singular if n == 1 else plural
|
|
||||||
return "" if n == 1 else "s"
|
|
||||||
|
|
||||||
|
|
||||||
@register_primitive("escape")
|
|
||||||
def prim_escape(s: Any) -> str:
|
|
||||||
"""``(escape val)`` → HTML-escaped string."""
|
|
||||||
from markupsafe import escape as _escape
|
|
||||||
return str(_escape(str(s) if s is not None and s is not NIL else ""))
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Style primitives
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@register_primitive("css")
|
|
||||||
def prim_css(*args: Any) -> Any:
|
|
||||||
"""``(css :flex :gap-4 :hover:bg-sky-200)`` → StyleValue.
|
|
||||||
|
|
||||||
Accepts keyword atoms (strings without colon prefix) and runtime
|
|
||||||
strings. Returns a StyleValue with a content-addressed class name
|
|
||||||
and all resolved CSS declarations.
|
|
||||||
"""
|
|
||||||
from .style_resolver import resolve_style
|
|
||||||
atoms = tuple(
|
|
||||||
(a.name if isinstance(a, Keyword) else str(a))
|
|
||||||
for a in args if a is not None and a is not NIL and a is not False
|
|
||||||
)
|
|
||||||
if not atoms:
|
|
||||||
return NIL
|
|
||||||
return resolve_style(atoms)
|
|
||||||
|
|
||||||
|
|
||||||
@register_primitive("merge-styles")
|
|
||||||
def prim_merge_styles(*styles: Any) -> Any:
|
|
||||||
"""``(merge-styles style1 style2)`` → merged StyleValue.
|
|
||||||
|
|
||||||
Merges multiple StyleValues; later declarations win.
|
|
||||||
"""
|
|
||||||
from .types import StyleValue
|
|
||||||
from .style_resolver import merge_styles
|
|
||||||
valid = [s for s in styles if isinstance(s, StyleValue)]
|
|
||||||
if not valid:
|
|
||||||
return NIL
|
|
||||||
if len(valid) == 1:
|
|
||||||
return valid[0]
|
|
||||||
return merge_styles(valid)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Sync IO bridge primitives
|
|
||||||
#
|
|
||||||
# These are declared in boundary.sx (I/O tier), NOT primitives.sx.
|
|
||||||
# They bypass @register_primitive validation because they aren't pure.
|
|
||||||
# But they must be evaluator-visible because they're called inline in .sx
|
|
||||||
# code (inside let, filter, etc.) where the async IO interceptor can't
|
|
||||||
# reach them — particularly in async_eval_ref.py which only intercepts
|
|
||||||
# IO at the top level.
|
|
||||||
#
|
|
||||||
# The async evaluators also intercept these via IO_PRIMITIVES, so the
|
|
||||||
# async path works too. This registration ensures the sync fallback works.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _bridge_app_url(service, *path_parts):
|
|
||||||
from shared.infrastructure.urls import app_url
|
|
||||||
path = str(path_parts[0]) if path_parts else "/"
|
|
||||||
return app_url(str(service), path)
|
|
||||||
|
|
||||||
def _bridge_asset_url(*path_parts):
|
|
||||||
from shared.infrastructure.urls import asset_url
|
|
||||||
path = str(path_parts[0]) if path_parts else ""
|
|
||||||
return asset_url(path)
|
|
||||||
|
|
||||||
def _bridge_config(key):
|
|
||||||
from shared.config import config
|
|
||||||
cfg = config()
|
|
||||||
return cfg.get(str(key))
|
|
||||||
|
|
||||||
def _bridge_jinja_global(key, *default):
|
|
||||||
from quart import current_app
|
|
||||||
d = default[0] if default else None
|
|
||||||
return current_app.jinja_env.globals.get(str(key), d)
|
|
||||||
|
|
||||||
def _bridge_relations_from(entity_type):
|
|
||||||
from shared.sx.relations import relations_from
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"name": d.name, "from_type": d.from_type, "to_type": d.to_type,
|
|
||||||
"cardinality": d.cardinality, "nav": d.nav,
|
|
||||||
"nav_icon": d.nav_icon, "nav_label": d.nav_label,
|
|
||||||
}
|
|
||||||
for d in relations_from(str(entity_type))
|
|
||||||
]
|
|
||||||
|
|
||||||
_PRIMITIVES["app-url"] = _bridge_app_url
|
|
||||||
_PRIMITIVES["asset-url"] = _bridge_asset_url
|
|
||||||
_PRIMITIVES["config"] = _bridge_config
|
|
||||||
_PRIMITIVES["jinja-global"] = _bridge_jinja_global
|
|
||||||
_PRIMITIVES["relations-from"] = _bridge_relations_from
|
|
||||||
|
|||||||
544
shared/sx/primitives_ctx.py
Normal file
544
shared/sx/primitives_ctx.py
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
"""
|
||||||
|
Service-specific page context IO handlers.
|
||||||
|
|
||||||
|
These are application-specific (rose-ash), not part of the generic SX
|
||||||
|
framework. Each handler builds a dict of template data from Quart request
|
||||||
|
context for use by .sx page components.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .primitives_io import register_io_handler
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Root / post headers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_io_handler("root-header-ctx")
|
||||||
|
async def _io_root_header_ctx(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""``(root-header-ctx)`` → dict with all root header values.
|
||||||
|
|
||||||
|
Fetches cart-mini, auth-menu, nav-tree fragments and computes
|
||||||
|
settings-url / is-admin from rights. Result is cached on ``g``
|
||||||
|
per request so multiple calls (e.g. header + mobile) are free.
|
||||||
|
"""
|
||||||
|
from quart import g, current_app, request
|
||||||
|
cached = getattr(g, "_root_header_ctx", None)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
from shared.infrastructure.fragments import fetch_fragments
|
||||||
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
|
from shared.infrastructure.urls import app_url
|
||||||
|
from shared.config import config
|
||||||
|
from .types import NIL
|
||||||
|
|
||||||
|
user = getattr(g, "user", None)
|
||||||
|
ident = current_cart_identity()
|
||||||
|
|
||||||
|
cart_params: dict[str, Any] = {}
|
||||||
|
if ident["user_id"] is not None:
|
||||||
|
cart_params["user_id"] = ident["user_id"]
|
||||||
|
if ident["session_id"] is not None:
|
||||||
|
cart_params["session_id"] = ident["session_id"]
|
||||||
|
|
||||||
|
auth_params: dict[str, Any] = {}
|
||||||
|
if user and getattr(user, "email", None):
|
||||||
|
auth_params["email"] = user.email
|
||||||
|
|
||||||
|
nav_params = {"app_name": current_app.name, "path": request.path}
|
||||||
|
|
||||||
|
cart_mini, auth_menu, nav_tree = await fetch_fragments([
|
||||||
|
("cart", "cart-mini", cart_params or None),
|
||||||
|
("account", "auth-menu", auth_params or None),
|
||||||
|
("blog", "nav-tree", nav_params),
|
||||||
|
])
|
||||||
|
|
||||||
|
rights = getattr(g, "rights", None) or {}
|
||||||
|
is_admin = (
|
||||||
|
rights.get("admin", False)
|
||||||
|
if isinstance(rights, dict)
|
||||||
|
else getattr(rights, "admin", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"cart-mini": cart_mini or NIL,
|
||||||
|
"blog-url": app_url("blog", ""),
|
||||||
|
"site-title": config()["title"],
|
||||||
|
"app-label": current_app.name,
|
||||||
|
"nav-tree": nav_tree or NIL,
|
||||||
|
"auth-menu": auth_menu or NIL,
|
||||||
|
"nav-panel": NIL,
|
||||||
|
"settings-url": app_url("blog", "/settings/") if is_admin else "",
|
||||||
|
"is-admin": is_admin,
|
||||||
|
}
|
||||||
|
g._root_header_ctx = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("post-header-ctx")
|
||||||
|
async def _io_post_header_ctx(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""``(post-header-ctx)`` → dict with post-level header values."""
|
||||||
|
from quart import g, request
|
||||||
|
cached = getattr(g, "_post_header_ctx", None)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
from shared.infrastructure.urls import app_url
|
||||||
|
from .types import NIL
|
||||||
|
from .parser import SxExpr
|
||||||
|
|
||||||
|
dctx = getattr(g, "_defpage_ctx", None) or {}
|
||||||
|
post = dctx.get("post") or {}
|
||||||
|
slug = post.get("slug", "")
|
||||||
|
if not slug:
|
||||||
|
result: dict[str, Any] = {"slug": ""}
|
||||||
|
g._post_header_ctx = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
title = (post.get("title") or "")[:160]
|
||||||
|
feature_image = post.get("feature_image") or NIL
|
||||||
|
|
||||||
|
# Container nav (pre-fetched by page helper into defpage ctx)
|
||||||
|
raw_nav = dctx.get("container_nav") or ""
|
||||||
|
container_nav: Any = NIL
|
||||||
|
nav_str = str(raw_nav).strip()
|
||||||
|
if nav_str and nav_str.replace("(<>", "").replace(")", "").strip():
|
||||||
|
if isinstance(raw_nav, SxExpr):
|
||||||
|
container_nav = raw_nav
|
||||||
|
else:
|
||||||
|
container_nav = SxExpr(nav_str)
|
||||||
|
|
||||||
|
page_cart_count = dctx.get("page_cart_count", 0) or 0
|
||||||
|
|
||||||
|
rights = getattr(g, "rights", None) or {}
|
||||||
|
is_admin = (
|
||||||
|
rights.get("admin", False)
|
||||||
|
if isinstance(rights, dict)
|
||||||
|
else getattr(rights, "admin", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
is_admin_page = dctx.get("is_admin_section") or "/admin" in request.path
|
||||||
|
|
||||||
|
from quart import current_app
|
||||||
|
select_colours = current_app.jinja_env.globals.get("select_colours", "")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"slug": slug,
|
||||||
|
"title": title,
|
||||||
|
"feature-image": feature_image,
|
||||||
|
"link-href": app_url("blog", f"/{slug}/"),
|
||||||
|
"container-nav": container_nav,
|
||||||
|
"page-cart-count": page_cart_count,
|
||||||
|
"cart-href": app_url("cart", f"/{slug}/") if page_cart_count else "",
|
||||||
|
"admin-href": app_url("blog", f"/{slug}/admin/"),
|
||||||
|
"is-admin": is_admin,
|
||||||
|
"is-admin-page": is_admin_page or NIL,
|
||||||
|
"select-colours": select_colours,
|
||||||
|
}
|
||||||
|
g._post_header_ctx = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cart
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_io_handler("cart-page-ctx")
|
||||||
|
async def _io_cart_page_ctx(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""``(cart-page-ctx)`` → dict with cart page header values."""
|
||||||
|
from quart import g
|
||||||
|
from .types import NIL
|
||||||
|
from shared.infrastructure.urls import app_url
|
||||||
|
|
||||||
|
page_post = getattr(g, "page_post", None)
|
||||||
|
if not page_post:
|
||||||
|
return {"slug": "", "title": "", "feature-image": NIL, "cart-url": "/"}
|
||||||
|
|
||||||
|
slug = getattr(page_post, "slug", "") or ""
|
||||||
|
title = (getattr(page_post, "title", "") or "")[:160]
|
||||||
|
feature_image = getattr(page_post, "feature_image", None) or NIL
|
||||||
|
|
||||||
|
return {
|
||||||
|
"slug": slug,
|
||||||
|
"title": title,
|
||||||
|
"feature-image": feature_image,
|
||||||
|
"page-cart-url": app_url("cart", f"/{slug}/"),
|
||||||
|
"cart-url": app_url("cart", "/"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Events
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_io_handler("events-calendar-ctx")
|
||||||
|
async def _io_events_calendar_ctx(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""``(events-calendar-ctx)`` → dict with events calendar header values."""
|
||||||
|
from quart import g
|
||||||
|
cal = getattr(g, "calendar", None)
|
||||||
|
if not cal:
|
||||||
|
dctx = getattr(g, "_defpage_ctx", None) or {}
|
||||||
|
cal = dctx.get("calendar")
|
||||||
|
if not cal:
|
||||||
|
return {"slug": ""}
|
||||||
|
return {
|
||||||
|
"slug": getattr(cal, "slug", "") or "",
|
||||||
|
"name": getattr(cal, "name", "") or "",
|
||||||
|
"description": getattr(cal, "description", "") or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("events-day-ctx")
|
||||||
|
async def _io_events_day_ctx(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""``(events-day-ctx)`` → dict with events day header values."""
|
||||||
|
from quart import g, url_for
|
||||||
|
from .types import NIL
|
||||||
|
from .parser import SxExpr
|
||||||
|
|
||||||
|
dctx = getattr(g, "_defpage_ctx", None) or {}
|
||||||
|
cal = getattr(g, "calendar", None) or dctx.get("calendar")
|
||||||
|
day_date = dctx.get("day_date") or getattr(g, "day_date", None)
|
||||||
|
if not cal or not day_date:
|
||||||
|
return {"date-str": ""}
|
||||||
|
|
||||||
|
cal_slug = getattr(cal, "slug", "") or ""
|
||||||
|
|
||||||
|
# Build confirmed entries nav
|
||||||
|
confirmed = dctx.get("confirmed_entries") or []
|
||||||
|
rights = getattr(g, "rights", None) or {}
|
||||||
|
is_admin = (
|
||||||
|
rights.get("admin", False)
|
||||||
|
if isinstance(rights, dict)
|
||||||
|
else getattr(rights, "admin", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
from .helpers import sx_call
|
||||||
|
nav_parts: list[str] = []
|
||||||
|
if confirmed:
|
||||||
|
entry_links = []
|
||||||
|
for entry in confirmed:
|
||||||
|
href = url_for(
|
||||||
|
"calendar.day.calendar_entries.calendar_entry.get",
|
||||||
|
calendar_slug=cal_slug,
|
||||||
|
year=day_date.year, month=day_date.month, day=day_date.day,
|
||||||
|
entry_id=entry.id,
|
||||||
|
)
|
||||||
|
start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||||
|
end = (
|
||||||
|
f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
||||||
|
if entry.end_at else ""
|
||||||
|
)
|
||||||
|
entry_links.append(sx_call(
|
||||||
|
"events-day-entry-link",
|
||||||
|
href=href, name=entry.name, time_str=f"{start}{end}",
|
||||||
|
))
|
||||||
|
inner = "".join(entry_links)
|
||||||
|
nav_parts.append(sx_call(
|
||||||
|
"events-day-entries-nav", inner=SxExpr(inner),
|
||||||
|
))
|
||||||
|
|
||||||
|
if is_admin and day_date:
|
||||||
|
admin_href = url_for(
|
||||||
|
"defpage_day_admin", calendar_slug=cal_slug,
|
||||||
|
year=day_date.year, month=day_date.month, day=day_date.day,
|
||||||
|
)
|
||||||
|
nav_parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog"))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"date-str": day_date.strftime("%A %d %B %Y"),
|
||||||
|
"year": day_date.year,
|
||||||
|
"month": day_date.month,
|
||||||
|
"day": day_date.day,
|
||||||
|
"nav": SxExpr("".join(nav_parts)) if nav_parts else NIL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("events-entry-ctx")
|
||||||
|
async def _io_events_entry_ctx(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""``(events-entry-ctx)`` → dict with events entry header values."""
|
||||||
|
from quart import g, url_for
|
||||||
|
from .types import NIL
|
||||||
|
from .parser import SxExpr
|
||||||
|
|
||||||
|
dctx = getattr(g, "_defpage_ctx", None) or {}
|
||||||
|
cal = getattr(g, "calendar", None) or dctx.get("calendar")
|
||||||
|
entry = getattr(g, "entry", None) or dctx.get("entry")
|
||||||
|
if not cal or not entry:
|
||||||
|
return {"id": ""}
|
||||||
|
|
||||||
|
cal_slug = getattr(cal, "slug", "") or ""
|
||||||
|
day = dctx.get("day")
|
||||||
|
month = dctx.get("month")
|
||||||
|
year = dctx.get("year")
|
||||||
|
|
||||||
|
# Times
|
||||||
|
start = entry.start_at
|
||||||
|
end = entry.end_at
|
||||||
|
time_str = ""
|
||||||
|
if start:
|
||||||
|
time_str = start.strftime("%H:%M")
|
||||||
|
if end:
|
||||||
|
time_str += f" \u2192 {end.strftime('%H:%M')}"
|
||||||
|
|
||||||
|
link_href = url_for(
|
||||||
|
"calendar.day.calendar_entries.calendar_entry.get",
|
||||||
|
calendar_slug=cal_slug,
|
||||||
|
year=year, month=month, day=day, entry_id=entry.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build nav: associated posts + admin link
|
||||||
|
entry_posts = dctx.get("entry_posts") or []
|
||||||
|
rights = getattr(g, "rights", None) or {}
|
||||||
|
is_admin = (
|
||||||
|
rights.get("admin", False)
|
||||||
|
if isinstance(rights, dict)
|
||||||
|
else getattr(rights, "admin", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
from .helpers import sx_call
|
||||||
|
from shared.infrastructure.urls import app_url
|
||||||
|
|
||||||
|
nav_parts: list[str] = []
|
||||||
|
if entry_posts:
|
||||||
|
post_links = ""
|
||||||
|
for ep in entry_posts:
|
||||||
|
ep_slug = getattr(ep, "slug", "")
|
||||||
|
ep_title = getattr(ep, "title", "")
|
||||||
|
feat = getattr(ep, "feature_image", None)
|
||||||
|
href = app_url("blog", f"/{ep_slug}/")
|
||||||
|
if feat:
|
||||||
|
img_html = sx_call("events-post-img", src=feat, alt=ep_title)
|
||||||
|
else:
|
||||||
|
img_html = sx_call("events-post-img-placeholder")
|
||||||
|
post_links += sx_call(
|
||||||
|
"events-entry-nav-post-link",
|
||||||
|
href=href, img=SxExpr(img_html), title=ep_title,
|
||||||
|
)
|
||||||
|
nav_parts.append(
|
||||||
|
sx_call("events-entry-posts-nav-oob", items=SxExpr(post_links))
|
||||||
|
.replace(' :hx-swap-oob "true"', '')
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_admin:
|
||||||
|
admin_url = url_for(
|
||||||
|
"calendar.day.calendar_entries.calendar_entry.admin.admin",
|
||||||
|
calendar_slug=cal_slug,
|
||||||
|
day=day, month=month, year=year, entry_id=entry.id,
|
||||||
|
)
|
||||||
|
nav_parts.append(sx_call("events-entry-admin-link", href=admin_url))
|
||||||
|
|
||||||
|
# Entry admin nav (ticket_types link)
|
||||||
|
admin_href = url_for(
|
||||||
|
"calendar.day.calendar_entries.calendar_entry.admin.admin",
|
||||||
|
calendar_slug=cal_slug,
|
||||||
|
day=day, month=month, year=year, entry_id=entry.id,
|
||||||
|
) if is_admin else ""
|
||||||
|
|
||||||
|
ticket_types_href = url_for(
|
||||||
|
"calendar.day.calendar_entries.calendar_entry.ticket_types.get",
|
||||||
|
calendar_slug=cal_slug, entry_id=entry.id,
|
||||||
|
year=year, month=month, day=day,
|
||||||
|
)
|
||||||
|
|
||||||
|
from quart import current_app
|
||||||
|
select_colours = current_app.jinja_env.globals.get("select_colours", "")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": str(entry.id),
|
||||||
|
"name": entry.name or "",
|
||||||
|
"time-str": time_str,
|
||||||
|
"link-href": link_href,
|
||||||
|
"nav": SxExpr("".join(nav_parts)) if nav_parts else NIL,
|
||||||
|
"admin-href": admin_href,
|
||||||
|
"ticket-types-href": ticket_types_href,
|
||||||
|
"is-admin": is_admin,
|
||||||
|
"select-colours": select_colours,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("events-slot-ctx")
|
||||||
|
async def _io_events_slot_ctx(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""``(events-slot-ctx)`` → dict with events slot header values."""
|
||||||
|
from quart import g
|
||||||
|
dctx = getattr(g, "_defpage_ctx", None) or {}
|
||||||
|
slot = getattr(g, "slot", None) or dctx.get("slot")
|
||||||
|
if not slot:
|
||||||
|
return {"name": ""}
|
||||||
|
return {
|
||||||
|
"name": getattr(slot, "name", "") or "",
|
||||||
|
"description": getattr(slot, "description", "") or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("events-ticket-type-ctx")
|
||||||
|
async def _io_events_ticket_type_ctx(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""``(events-ticket-type-ctx)`` → dict with ticket type header values."""
|
||||||
|
from quart import g, url_for
|
||||||
|
|
||||||
|
dctx = getattr(g, "_defpage_ctx", None) or {}
|
||||||
|
cal = getattr(g, "calendar", None) or dctx.get("calendar")
|
||||||
|
entry = getattr(g, "entry", None) or dctx.get("entry")
|
||||||
|
ticket_type = getattr(g, "ticket_type", None) or dctx.get("ticket_type")
|
||||||
|
if not cal or not entry or not ticket_type:
|
||||||
|
return {"id": ""}
|
||||||
|
|
||||||
|
cal_slug = getattr(cal, "slug", "") or ""
|
||||||
|
day = dctx.get("day")
|
||||||
|
month = dctx.get("month")
|
||||||
|
year = dctx.get("year")
|
||||||
|
|
||||||
|
link_href = url_for(
|
||||||
|
"calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get",
|
||||||
|
calendar_slug=cal_slug, year=year, month=month, day=day,
|
||||||
|
entry_id=entry.id, ticket_type_id=ticket_type.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": str(ticket_type.id),
|
||||||
|
"name": getattr(ticket_type, "name", "") or "",
|
||||||
|
"link-href": link_href,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Market
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_io_handler("market-header-ctx")
|
||||||
|
async def _io_market_header_ctx(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""``(market-header-ctx)`` → dict with market header data."""
|
||||||
|
from quart import g, url_for
|
||||||
|
from shared.config import config as get_config
|
||||||
|
from .parser import SxExpr
|
||||||
|
|
||||||
|
cfg = get_config()
|
||||||
|
market_title = cfg.get("market_title", "")
|
||||||
|
link_href = url_for("defpage_market_home")
|
||||||
|
|
||||||
|
# Get categories if market is loaded
|
||||||
|
market = getattr(g, "market", None)
|
||||||
|
categories = {}
|
||||||
|
if market:
|
||||||
|
from bp.browse.services.nav import get_nav
|
||||||
|
nav_data = await get_nav(g.s, market_id=market.id)
|
||||||
|
categories = nav_data.get("cats", {})
|
||||||
|
|
||||||
|
# Build minimal ctx for existing helper functions
|
||||||
|
select_colours = getattr(g, "select_colours", "")
|
||||||
|
if not select_colours:
|
||||||
|
from quart import current_app
|
||||||
|
select_colours = current_app.jinja_env.globals.get("select_colours", "")
|
||||||
|
rights = getattr(g, "rights", None) or {}
|
||||||
|
|
||||||
|
mini_ctx: dict[str, Any] = {
|
||||||
|
"market_title": market_title,
|
||||||
|
"top_slug": "",
|
||||||
|
"sub_slug": "",
|
||||||
|
"categories": categories,
|
||||||
|
"qs": "",
|
||||||
|
"hx_select_search": "#main-panel",
|
||||||
|
"select_colours": select_colours,
|
||||||
|
"rights": rights,
|
||||||
|
"category_label": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build header + mobile nav data via new data-driven helpers
|
||||||
|
from sxc.pages.layouts import _market_header_data, _mobile_nav_panel_sx
|
||||||
|
header_data = _market_header_data(mini_ctx)
|
||||||
|
mobile_nav = _mobile_nav_panel_sx(mini_ctx)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"market-title": market_title,
|
||||||
|
"link-href": link_href,
|
||||||
|
"top-slug": "",
|
||||||
|
"sub-slug": "",
|
||||||
|
"categories": header_data.get("categories", []),
|
||||||
|
"hx-select": header_data.get("hx-select", "#main-panel"),
|
||||||
|
"select-colours": header_data.get("select-colours", ""),
|
||||||
|
"all-href": header_data.get("all-href", ""),
|
||||||
|
"all-active": header_data.get("all-active", False),
|
||||||
|
"admin-href": header_data.get("admin-href", ""),
|
||||||
|
"mobile-nav": SxExpr(mobile_nav) if mobile_nav else "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Federation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_io_handler("federation-actor-ctx")
|
||||||
|
async def _io_federation_actor_ctx(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""``(federation-actor-ctx)`` → serialized actor dict or None."""
|
||||||
|
from quart import g
|
||||||
|
actor = getattr(g, "_social_actor", None)
|
||||||
|
if not actor:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"id": actor.id,
|
||||||
|
"preferred_username": actor.preferred_username,
|
||||||
|
"display_name": getattr(actor, "display_name", None),
|
||||||
|
"icon_url": getattr(actor, "icon_url", None),
|
||||||
|
"actor_url": getattr(actor, "actor_url", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Misc UI contexts
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_io_handler("select-colours")
|
||||||
|
async def _io_select_colours(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> str:
|
||||||
|
"""``(select-colours)`` → the shared select/hover CSS class string."""
|
||||||
|
from quart import current_app
|
||||||
|
return current_app.jinja_env.globals.get("select_colours", "")
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("account-nav-ctx")
|
||||||
|
async def _io_account_nav_ctx(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> Any:
|
||||||
|
"""``(account-nav-ctx)`` → account nav fragments as SxExpr, or NIL."""
|
||||||
|
from quart import g
|
||||||
|
from .types import NIL
|
||||||
|
from .parser import SxExpr
|
||||||
|
from .helpers import sx_call
|
||||||
|
val = getattr(g, "account_nav", None)
|
||||||
|
if not val:
|
||||||
|
return NIL
|
||||||
|
if isinstance(val, SxExpr):
|
||||||
|
return val
|
||||||
|
return sx_call("rich-text", html=str(val))
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("app-rights")
|
||||||
|
async def _io_app_rights(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: Any
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""``(app-rights)`` → user rights dict from ``g.rights``."""
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "rights", None) or {}
|
||||||
File diff suppressed because it is too large
Load Diff
131
shared/sx/primitives_stdlib.py
Normal file
131
shared/sx/primitives_stdlib.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
"""
|
||||||
|
Standard library primitives — isomorphic, opt-in modules.
|
||||||
|
|
||||||
|
Augment core with format, text, style, and debug primitives.
|
||||||
|
These are registered into the same _PRIMITIVES registry as core.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .primitives import register_primitive
|
||||||
|
from .types import NIL
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# stdlib.format
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_primitive("format-date")
|
||||||
|
def prim_format_date(date_str: Any, fmt: str) -> str:
|
||||||
|
"""``(format-date date-str fmt)`` → formatted date string."""
|
||||||
|
from datetime import datetime
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(str(date_str))
|
||||||
|
return dt.strftime(fmt)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return str(date_str) if date_str else ""
|
||||||
|
|
||||||
|
|
||||||
|
@register_primitive("format-decimal")
|
||||||
|
def prim_format_decimal(val: Any, places: Any = 2) -> str:
|
||||||
|
"""``(format-decimal val places)`` → formatted decimal string."""
|
||||||
|
try:
|
||||||
|
return f"{float(val):.{int(places)}f}"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return "0." + "0" * int(places)
|
||||||
|
|
||||||
|
|
||||||
|
@register_primitive("parse-int")
|
||||||
|
def prim_parse_int(val: Any, default: Any = 0) -> int | Any:
|
||||||
|
"""``(parse-int val default?)`` → int(val) with fallback."""
|
||||||
|
try:
|
||||||
|
return int(val)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
@register_primitive("parse-datetime")
|
||||||
|
def prim_parse_datetime(val: Any) -> Any:
|
||||||
|
"""``(parse-datetime "2024-01-15T10:00:00")`` → ISO string or nil."""
|
||||||
|
from datetime import datetime
|
||||||
|
if not val or val is NIL:
|
||||||
|
return NIL
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(str(val))
|
||||||
|
return dt.isoformat()
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return NIL
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# stdlib.text
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_primitive("pluralize")
|
||||||
|
def prim_pluralize(count: Any, singular: str = "", plural: str = "s") -> str:
|
||||||
|
"""``(pluralize count)`` → "s" if count != 1, else "".
|
||||||
|
``(pluralize count "item" "items")`` → "item" or "items"."""
|
||||||
|
try:
|
||||||
|
n = int(count)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
n = 0
|
||||||
|
if singular or plural != "s":
|
||||||
|
return singular if n == 1 else plural
|
||||||
|
return "" if n == 1 else "s"
|
||||||
|
|
||||||
|
|
||||||
|
@register_primitive("escape")
|
||||||
|
def prim_escape(s: Any) -> str:
|
||||||
|
"""``(escape val)`` → HTML-escaped string."""
|
||||||
|
from markupsafe import escape as _escape
|
||||||
|
return str(_escape(str(s) if s is not None and s is not NIL else ""))
|
||||||
|
|
||||||
|
|
||||||
|
@register_primitive("strip-tags")
|
||||||
|
def prim_strip_tags(s: str) -> str:
|
||||||
|
"""Strip HTML tags from a string."""
|
||||||
|
import re
|
||||||
|
return re.sub(r"<[^>]+>", "", s)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# stdlib.style
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_primitive("css")
|
||||||
|
def prim_css(*args: Any) -> Any:
|
||||||
|
"""``(css :flex :gap-4 :hover:bg-sky-200)`` → StyleValue."""
|
||||||
|
from .types import Keyword
|
||||||
|
from .style_resolver import resolve_style
|
||||||
|
atoms = tuple(
|
||||||
|
(a.name if isinstance(a, Keyword) else str(a))
|
||||||
|
for a in args if a is not None and a is not NIL and a is not False
|
||||||
|
)
|
||||||
|
if not atoms:
|
||||||
|
return NIL
|
||||||
|
return resolve_style(atoms)
|
||||||
|
|
||||||
|
|
||||||
|
@register_primitive("merge-styles")
|
||||||
|
def prim_merge_styles(*styles: Any) -> Any:
|
||||||
|
"""``(merge-styles style1 style2)`` → merged StyleValue."""
|
||||||
|
from .types import StyleValue
|
||||||
|
from .style_resolver import merge_styles
|
||||||
|
valid = [s for s in styles if isinstance(s, StyleValue)]
|
||||||
|
if not valid:
|
||||||
|
return NIL
|
||||||
|
if len(valid) == 1:
|
||||||
|
return valid[0]
|
||||||
|
return merge_styles(valid)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# stdlib.debug
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register_primitive("assert")
|
||||||
|
def prim_assert(condition: Any, message: str = "Assertion failed") -> bool:
|
||||||
|
if not condition:
|
||||||
|
raise RuntimeError(f"Assertion error: {message}")
|
||||||
|
return True
|
||||||
@@ -996,13 +996,19 @@ ADAPTER_DEPS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
def compile_ref_to_js(
|
||||||
|
adapters: list[str] | None = None,
|
||||||
|
modules: list[str] | None = None,
|
||||||
|
) -> str:
|
||||||
"""Read reference .sx files and emit JavaScript.
|
"""Read reference .sx files and emit JavaScript.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
adapters: List of adapter names to include.
|
adapters: List of adapter names to include.
|
||||||
Valid names: html, sx, dom, engine.
|
Valid names: html, sx, dom, engine.
|
||||||
None = include all adapters.
|
None = include all adapters.
|
||||||
|
modules: List of primitive module names to include.
|
||||||
|
core.* are always included. stdlib.* are opt-in.
|
||||||
|
None = include all modules (backward compatible).
|
||||||
"""
|
"""
|
||||||
ref_dir = os.path.dirname(os.path.abspath(__file__))
|
ref_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
emitter = JSEmitter()
|
emitter = JSEmitter()
|
||||||
@@ -1060,9 +1066,25 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
|||||||
has_parser = "parser" in adapter_set
|
has_parser = "parser" in adapter_set
|
||||||
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
|
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
|
||||||
|
|
||||||
|
# Determine which primitive modules to include
|
||||||
|
prim_modules = None # None = all
|
||||||
|
if modules is not None:
|
||||||
|
prim_modules = [m for m in _ALL_JS_MODULES if m.startswith("core.")]
|
||||||
|
for m in modules:
|
||||||
|
if m not in prim_modules:
|
||||||
|
if m not in PRIMITIVES_JS_MODULES:
|
||||||
|
raise ValueError(f"Unknown module: {m!r}. Valid: {', '.join(PRIMITIVES_JS_MODULES)}")
|
||||||
|
prim_modules.append(m)
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
parts.append(PREAMBLE)
|
parts.append(PREAMBLE)
|
||||||
parts.append(PLATFORM_JS)
|
parts.append(PLATFORM_JS_PRE)
|
||||||
|
parts.append('\n // =========================================================================')
|
||||||
|
parts.append(' // Primitives')
|
||||||
|
parts.append(' // =========================================================================\n')
|
||||||
|
parts.append(' var PRIMITIVES = {};')
|
||||||
|
parts.append(_assemble_primitives_js(prim_modules))
|
||||||
|
parts.append(PLATFORM_JS_POST)
|
||||||
|
|
||||||
# Parser platform must come before compiled parser.sx
|
# Parser platform must come before compiled parser.sx
|
||||||
if has_parser:
|
if has_parser:
|
||||||
@@ -1181,7 +1203,235 @@ PREAMBLE = '''\
|
|||||||
return arguments.length ? arguments[arguments.length - 1] : false;
|
return arguments.length ? arguments[arguments.length - 1] : false;
|
||||||
}'''
|
}'''
|
||||||
|
|
||||||
PLATFORM_JS = '''
|
# ---------------------------------------------------------------------------
|
||||||
|
# Primitive modules — JS implementations keyed by spec module name.
|
||||||
|
# core.* modules are always included; stdlib.* are opt-in.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||||
|
"core.arithmetic": '''
|
||||||
|
// core.arithmetic
|
||||||
|
PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; };
|
||||||
|
PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; };
|
||||||
|
PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; };
|
||||||
|
PRIMITIVES["/"] = function(a, b) { return a / b; };
|
||||||
|
PRIMITIVES["mod"] = function(a, b) { return a % b; };
|
||||||
|
PRIMITIVES["inc"] = function(n) { return n + 1; };
|
||||||
|
PRIMITIVES["dec"] = function(n) { return n - 1; };
|
||||||
|
PRIMITIVES["abs"] = Math.abs;
|
||||||
|
PRIMITIVES["floor"] = Math.floor;
|
||||||
|
PRIMITIVES["ceil"] = Math.ceil;
|
||||||
|
PRIMITIVES["round"] = function(x, n) {
|
||||||
|
if (n === undefined || n === 0) return Math.round(x);
|
||||||
|
var f = Math.pow(10, n); return Math.round(x * f) / f;
|
||||||
|
};
|
||||||
|
PRIMITIVES["min"] = Math.min;
|
||||||
|
PRIMITIVES["max"] = Math.max;
|
||||||
|
PRIMITIVES["sqrt"] = Math.sqrt;
|
||||||
|
PRIMITIVES["pow"] = Math.pow;
|
||||||
|
PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); };
|
||||||
|
''',
|
||||||
|
|
||||||
|
"core.comparison": '''
|
||||||
|
// core.comparison
|
||||||
|
PRIMITIVES["="] = function(a, b) { return a === b; };
|
||||||
|
PRIMITIVES["!="] = function(a, b) { return a !== b; };
|
||||||
|
PRIMITIVES["<"] = function(a, b) { return a < b; };
|
||||||
|
PRIMITIVES[">"] = function(a, b) { return a > b; };
|
||||||
|
PRIMITIVES["<="] = function(a, b) { return a <= b; };
|
||||||
|
PRIMITIVES[">="] = function(a, b) { return a >= b; };
|
||||||
|
''',
|
||||||
|
|
||||||
|
"core.logic": '''
|
||||||
|
// core.logic
|
||||||
|
PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); };
|
||||||
|
''',
|
||||||
|
|
||||||
|
"core.predicates": '''
|
||||||
|
// core.predicates
|
||||||
|
PRIMITIVES["nil?"] = isNil;
|
||||||
|
PRIMITIVES["number?"] = function(x) { return typeof x === "number"; };
|
||||||
|
PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
|
||||||
|
PRIMITIVES["list?"] = Array.isArray;
|
||||||
|
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
|
||||||
|
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
|
||||||
|
PRIMITIVES["contains?"] = function(c, k) {
|
||||||
|
if (typeof c === "string") return c.indexOf(String(k)) !== -1;
|
||||||
|
if (Array.isArray(c)) return c.indexOf(k) !== -1;
|
||||||
|
return k in c;
|
||||||
|
};
|
||||||
|
PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; };
|
||||||
|
PRIMITIVES["even?"] = function(n) { return n % 2 === 0; };
|
||||||
|
PRIMITIVES["zero?"] = function(n) { return n === 0; };
|
||||||
|
''',
|
||||||
|
|
||||||
|
"core.strings": '''
|
||||||
|
// core.strings
|
||||||
|
PRIMITIVES["str"] = function() {
|
||||||
|
var p = [];
|
||||||
|
for (var i = 0; i < arguments.length; i++) {
|
||||||
|
var v = arguments[i]; if (isNil(v)) continue; p.push(String(v));
|
||||||
|
}
|
||||||
|
return p.join("");
|
||||||
|
};
|
||||||
|
PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); };
|
||||||
|
PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); };
|
||||||
|
PRIMITIVES["trim"] = function(s) { return String(s).trim(); };
|
||||||
|
PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
|
||||||
|
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
|
||||||
|
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
|
||||||
|
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
|
||||||
|
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
|
||||||
|
PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); };
|
||||||
|
PRIMITIVES["concat"] = function() {
|
||||||
|
var out = [];
|
||||||
|
for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
''',
|
||||||
|
|
||||||
|
"core.collections": '''
|
||||||
|
// core.collections
|
||||||
|
PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); };
|
||||||
|
PRIMITIVES["dict"] = function() {
|
||||||
|
var d = {};
|
||||||
|
for (var i = 0; i < arguments.length - 1; i += 2) d[arguments[i]] = arguments[i + 1];
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
PRIMITIVES["range"] = function(a, b, step) {
|
||||||
|
var r = []; step = step || 1;
|
||||||
|
for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i);
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
PRIMITIVES["get"] = function(c, k, def) { var v = (c && c[k]); return v !== undefined ? v : (def !== undefined ? def : NIL); };
|
||||||
|
PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; };
|
||||||
|
PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; };
|
||||||
|
PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; };
|
||||||
|
PRIMITIVES["rest"] = function(c) { return c ? c.slice(1) : []; };
|
||||||
|
PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
|
||||||
|
PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); };
|
||||||
|
PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); };
|
||||||
|
PRIMITIVES["chunk-every"] = function(c, n) {
|
||||||
|
var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r;
|
||||||
|
};
|
||||||
|
PRIMITIVES["zip-pairs"] = function(c) {
|
||||||
|
var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r;
|
||||||
|
};
|
||||||
|
''',
|
||||||
|
|
||||||
|
"core.dict": '''
|
||||||
|
// core.dict
|
||||||
|
PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); };
|
||||||
|
PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; };
|
||||||
|
PRIMITIVES["merge"] = function() {
|
||||||
|
var out = {};
|
||||||
|
for (var i = 0; i < arguments.length; i++) { var d = arguments[i]; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; }
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
PRIMITIVES["assoc"] = function(d) {
|
||||||
|
var out = {}; if (d && !isNil(d)) for (var k in d) out[k] = d[k];
|
||||||
|
for (var i = 1; i < arguments.length - 1; i += 2) out[arguments[i]] = arguments[i + 1];
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
PRIMITIVES["dissoc"] = function(d) {
|
||||||
|
var out = {}; for (var k in d) out[k] = d[k];
|
||||||
|
for (var i = 1; i < arguments.length; i++) delete out[arguments[i]];
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
PRIMITIVES["into"] = function(target, coll) {
|
||||||
|
if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
|
||||||
|
var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
''',
|
||||||
|
|
||||||
|
"stdlib.format": '''
|
||||||
|
// stdlib.format
|
||||||
|
PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); };
|
||||||
|
PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; };
|
||||||
|
PRIMITIVES["format-date"] = function(s, fmt) {
|
||||||
|
if (!s) return "";
|
||||||
|
try {
|
||||||
|
var d = new Date(s);
|
||||||
|
if (isNaN(d.getTime())) return String(s);
|
||||||
|
var months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
||||||
|
var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
||||||
|
return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2))
|
||||||
|
.replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()])
|
||||||
|
.replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2))
|
||||||
|
.replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2));
|
||||||
|
} catch (e) { return String(s); }
|
||||||
|
};
|
||||||
|
PRIMITIVES["parse-datetime"] = function(s) { return s ? String(s) : NIL; };
|
||||||
|
''',
|
||||||
|
|
||||||
|
"stdlib.text": '''
|
||||||
|
// stdlib.text
|
||||||
|
PRIMITIVES["pluralize"] = function(n, s, p) {
|
||||||
|
if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s");
|
||||||
|
return n == 1 ? "" : "s";
|
||||||
|
};
|
||||||
|
PRIMITIVES["escape"] = function(s) {
|
||||||
|
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");
|
||||||
|
};
|
||||||
|
PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
|
||||||
|
''',
|
||||||
|
|
||||||
|
"stdlib.style": '''
|
||||||
|
// stdlib.style
|
||||||
|
PRIMITIVES["css"] = function() {
|
||||||
|
var atoms = [];
|
||||||
|
for (var i = 0; i < arguments.length; i++) {
|
||||||
|
var a = arguments[i];
|
||||||
|
if (isNil(a) || a === false) continue;
|
||||||
|
atoms.push(isKw(a) ? a.name : String(a));
|
||||||
|
}
|
||||||
|
if (!atoms.length) return NIL;
|
||||||
|
return new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
|
||||||
|
};
|
||||||
|
PRIMITIVES["merge-styles"] = function() {
|
||||||
|
var valid = [];
|
||||||
|
for (var i = 0; i < arguments.length; i++) {
|
||||||
|
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
|
||||||
|
}
|
||||||
|
if (!valid.length) return NIL;
|
||||||
|
if (valid.length === 1) return valid[0];
|
||||||
|
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
|
||||||
|
return new StyleValue("sx-merged", allDecls, [], [], []);
|
||||||
|
};
|
||||||
|
''',
|
||||||
|
|
||||||
|
"stdlib.debug": '''
|
||||||
|
// stdlib.debug
|
||||||
|
PRIMITIVES["assert"] = function(cond, msg) {
|
||||||
|
if (!isSxTruthy(cond)) throw new Error("Assertion error: " + (msg || "Assertion failed"));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
''',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Modules to include by default (all)
|
||||||
|
_ALL_JS_MODULES = list(PRIMITIVES_JS_MODULES.keys())
|
||||||
|
|
||||||
|
# Selected primitive modules for current compilation (None = all)
|
||||||
|
|
||||||
|
|
||||||
|
def _assemble_primitives_js(modules: list[str] | None = None) -> str:
|
||||||
|
"""Assemble JS primitive code from selected modules.
|
||||||
|
|
||||||
|
If modules is None, all modules are included.
|
||||||
|
Core modules are always included regardless of the list.
|
||||||
|
"""
|
||||||
|
if modules is None:
|
||||||
|
modules = _ALL_JS_MODULES
|
||||||
|
parts = []
|
||||||
|
for mod in modules:
|
||||||
|
if mod in PRIMITIVES_JS_MODULES:
|
||||||
|
parts.append(PRIMITIVES_JS_MODULES[mod])
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
PLATFORM_JS_PRE = '''
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Platform interface — JS implementation
|
// Platform interface — JS implementation
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -1305,180 +1555,9 @@ PLATFORM_JS = '''
|
|||||||
function error(msg) { throw new Error(msg); }
|
function error(msg) { throw new Error(msg); }
|
||||||
function inspect(x) { return JSON.stringify(x); }
|
function inspect(x) { return JSON.stringify(x); }
|
||||||
|
|
||||||
// =========================================================================
|
'''
|
||||||
// Primitives
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
var PRIMITIVES = {};
|
|
||||||
|
|
||||||
// Arithmetic
|
|
||||||
PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; };
|
|
||||||
PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; };
|
|
||||||
PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; };
|
|
||||||
PRIMITIVES["/"] = function(a, b) { return a / b; };
|
|
||||||
PRIMITIVES["mod"] = function(a, b) { return a % b; };
|
|
||||||
PRIMITIVES["inc"] = function(n) { return n + 1; };
|
|
||||||
PRIMITIVES["dec"] = function(n) { return n - 1; };
|
|
||||||
PRIMITIVES["abs"] = Math.abs;
|
|
||||||
PRIMITIVES["floor"] = Math.floor;
|
|
||||||
PRIMITIVES["ceil"] = Math.ceil;
|
|
||||||
PRIMITIVES["round"] = Math.round;
|
|
||||||
PRIMITIVES["min"] = Math.min;
|
|
||||||
PRIMITIVES["max"] = Math.max;
|
|
||||||
PRIMITIVES["sqrt"] = Math.sqrt;
|
|
||||||
PRIMITIVES["pow"] = Math.pow;
|
|
||||||
PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); };
|
|
||||||
|
|
||||||
// Comparison
|
|
||||||
PRIMITIVES["="] = function(a, b) { return a == b; };
|
|
||||||
PRIMITIVES["!="] = function(a, b) { return a != b; };
|
|
||||||
PRIMITIVES["<"] = function(a, b) { return a < b; };
|
|
||||||
PRIMITIVES[">"] = function(a, b) { return a > b; };
|
|
||||||
PRIMITIVES["<="] = function(a, b) { return a <= b; };
|
|
||||||
PRIMITIVES[">="] = function(a, b) { return a >= b; };
|
|
||||||
|
|
||||||
// Logic
|
|
||||||
PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); };
|
|
||||||
|
|
||||||
// String
|
|
||||||
PRIMITIVES["str"] = function() {
|
|
||||||
var p = [];
|
|
||||||
for (var i = 0; i < arguments.length; i++) {
|
|
||||||
var v = arguments[i]; if (isNil(v)) continue; p.push(String(v));
|
|
||||||
}
|
|
||||||
return p.join("");
|
|
||||||
};
|
|
||||||
PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); };
|
|
||||||
PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); };
|
|
||||||
PRIMITIVES["trim"] = function(s) { return String(s).trim(); };
|
|
||||||
PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
|
|
||||||
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
|
|
||||||
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
|
|
||||||
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
|
|
||||||
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
|
|
||||||
PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); };
|
|
||||||
PRIMITIVES["concat"] = function() {
|
|
||||||
var out = [];
|
|
||||||
for (var i = 0; i < arguments.length; i++) if (arguments[i]) out = out.concat(arguments[i]);
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
|
|
||||||
|
|
||||||
// Predicates
|
|
||||||
PRIMITIVES["nil?"] = isNil;
|
|
||||||
PRIMITIVES["number?"] = function(x) { return typeof x === "number"; };
|
|
||||||
PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
|
|
||||||
PRIMITIVES["list?"] = Array.isArray;
|
|
||||||
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
|
|
||||||
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
|
|
||||||
PRIMITIVES["contains?"] = function(c, k) {
|
|
||||||
if (typeof c === "string") return c.indexOf(String(k)) !== -1;
|
|
||||||
if (Array.isArray(c)) return c.indexOf(k) !== -1;
|
|
||||||
return k in c;
|
|
||||||
};
|
|
||||||
PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; };
|
|
||||||
PRIMITIVES["even?"] = function(n) { return n % 2 === 0; };
|
|
||||||
PRIMITIVES["zero?"] = function(n) { return n === 0; };
|
|
||||||
|
|
||||||
// Collections
|
|
||||||
PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); };
|
|
||||||
PRIMITIVES["dict"] = function() {
|
|
||||||
var d = {};
|
|
||||||
for (var i = 0; i < arguments.length - 1; i += 2) d[arguments[i]] = arguments[i + 1];
|
|
||||||
return d;
|
|
||||||
};
|
|
||||||
PRIMITIVES["range"] = function(a, b, step) {
|
|
||||||
var r = []; step = step || 1;
|
|
||||||
for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i);
|
|
||||||
return r;
|
|
||||||
};
|
|
||||||
PRIMITIVES["get"] = function(c, k, def) { var v = (c && c[k]); return v !== undefined ? v : (def !== undefined ? def : NIL); };
|
|
||||||
PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; };
|
|
||||||
PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; };
|
|
||||||
PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; };
|
|
||||||
PRIMITIVES["rest"] = function(c) { return c ? c.slice(1) : []; };
|
|
||||||
PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
|
|
||||||
PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); };
|
|
||||||
PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); };
|
|
||||||
PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); };
|
|
||||||
PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; };
|
|
||||||
PRIMITIVES["merge"] = function() {
|
|
||||||
var out = {};
|
|
||||||
for (var i = 0; i < arguments.length; i++) { var d = arguments[i]; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; }
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
PRIMITIVES["assoc"] = function(d) {
|
|
||||||
var out = {}; if (d && !isNil(d)) for (var k in d) out[k] = d[k];
|
|
||||||
for (var i = 1; i < arguments.length - 1; i += 2) out[arguments[i]] = arguments[i + 1];
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
PRIMITIVES["dissoc"] = function(d) {
|
|
||||||
var out = {}; for (var k in d) out[k] = d[k];
|
|
||||||
for (var i = 1; i < arguments.length; i++) delete out[arguments[i]];
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
PRIMITIVES["chunk-every"] = function(c, n) {
|
|
||||||
var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r;
|
|
||||||
};
|
|
||||||
PRIMITIVES["zip-pairs"] = function(c) {
|
|
||||||
var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r;
|
|
||||||
};
|
|
||||||
PRIMITIVES["into"] = function(target, coll) {
|
|
||||||
if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
|
|
||||||
var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
|
|
||||||
return r;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format
|
|
||||||
PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); };
|
|
||||||
PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; };
|
|
||||||
PRIMITIVES["pluralize"] = function(n, s, p) {
|
|
||||||
if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s");
|
|
||||||
return n == 1 ? "" : "s";
|
|
||||||
};
|
|
||||||
PRIMITIVES["escape"] = function(s) {
|
|
||||||
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
|
||||||
};
|
|
||||||
PRIMITIVES["format-date"] = function(s, fmt) {
|
|
||||||
if (!s) return "";
|
|
||||||
try {
|
|
||||||
var d = new Date(s);
|
|
||||||
if (isNaN(d.getTime())) return String(s);
|
|
||||||
var months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
|
||||||
var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
|
||||||
return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2))
|
|
||||||
.replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()])
|
|
||||||
.replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2))
|
|
||||||
.replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2));
|
|
||||||
} catch (e) { return String(s); }
|
|
||||||
};
|
|
||||||
PRIMITIVES["parse-datetime"] = function(s) { return s ? String(s) : NIL; };
|
|
||||||
PRIMITIVES["split-ids"] = function(s) {
|
|
||||||
if (!s) return [];
|
|
||||||
return String(s).split(",").map(function(x) { return x.trim(); }).filter(function(x) { return x; });
|
|
||||||
};
|
|
||||||
PRIMITIVES["css"] = function() {
|
|
||||||
// Stub — CSSX requires style dictionary which is browser-only
|
|
||||||
var atoms = [];
|
|
||||||
for (var i = 0; i < arguments.length; i++) {
|
|
||||||
var a = arguments[i];
|
|
||||||
if (isNil(a) || a === false) continue;
|
|
||||||
atoms.push(isKw(a) ? a.name : String(a));
|
|
||||||
}
|
|
||||||
if (!atoms.length) return NIL;
|
|
||||||
return new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
|
|
||||||
};
|
|
||||||
PRIMITIVES["merge-styles"] = function() {
|
|
||||||
var valid = [];
|
|
||||||
for (var i = 0; i < arguments.length; i++) {
|
|
||||||
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
|
|
||||||
}
|
|
||||||
if (!valid.length) return NIL;
|
|
||||||
if (valid.length === 1) return valid[0];
|
|
||||||
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
|
|
||||||
return new StyleValue("sx-merged", allDecls, [], [], []);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
PLATFORM_JS_POST = '''
|
||||||
function isPrimitive(name) { return name in PRIMITIVES; }
|
function isPrimitive(name) { return name in PRIMITIVES; }
|
||||||
function getPrimitive(name) { return PRIMITIVES[name]; }
|
function getPrimitive(name) { return PRIMITIVES[name]; }
|
||||||
|
|
||||||
@@ -2825,18 +2904,22 @@ if __name__ == "__main__":
|
|||||||
p = argparse.ArgumentParser(description="Bootstrap-compile SX reference spec to JavaScript")
|
p = argparse.ArgumentParser(description="Bootstrap-compile SX reference spec to JavaScript")
|
||||||
p.add_argument("--adapters", "-a",
|
p.add_argument("--adapters", "-a",
|
||||||
help="Comma-separated adapter list (html,sx,dom,engine). Default: all")
|
help="Comma-separated adapter list (html,sx,dom,engine). Default: all")
|
||||||
|
p.add_argument("--modules", "-m",
|
||||||
|
help="Comma-separated primitive modules (core.* always included). Default: all")
|
||||||
p.add_argument("--output", "-o",
|
p.add_argument("--output", "-o",
|
||||||
help="Output file (default: stdout)")
|
help="Output file (default: stdout)")
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
||||||
adapters = args.adapters.split(",") if args.adapters else None
|
adapters = args.adapters.split(",") if args.adapters else None
|
||||||
js = compile_ref_to_js(adapters)
|
modules = args.modules.split(",") if args.modules else None
|
||||||
|
js = compile_ref_to_js(adapters, modules)
|
||||||
|
|
||||||
if args.output:
|
if args.output:
|
||||||
with open(args.output, "w") as f:
|
with open(args.output, "w") as f:
|
||||||
f.write(js)
|
f.write(js)
|
||||||
included = ", ".join(adapters) if adapters else "all"
|
included = ", ".join(adapters) if adapters else "all"
|
||||||
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included})",
|
mods = ", ".join(modules) if modules else "all"
|
||||||
|
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included}, modules: {mods})",
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(js)
|
print(js)
|
||||||
|
|||||||
@@ -801,14 +801,30 @@ ADAPTER_FILES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def compile_ref_to_py(adapters: list[str] | None = None) -> str:
|
def compile_ref_to_py(
|
||||||
|
adapters: list[str] | None = None,
|
||||||
|
modules: list[str] | None = None,
|
||||||
|
) -> str:
|
||||||
"""Read reference .sx files and emit Python.
|
"""Read reference .sx files and emit Python.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
adapters: List of adapter names to include.
|
adapters: List of adapter names to include.
|
||||||
Valid names: html, sx.
|
Valid names: html, sx.
|
||||||
None = include all server-side adapters.
|
None = include all server-side adapters.
|
||||||
|
modules: List of primitive module names to include.
|
||||||
|
core.* are always included. stdlib.* are opt-in.
|
||||||
|
None = include all modules (backward compatible).
|
||||||
"""
|
"""
|
||||||
|
# Determine which primitive modules to include
|
||||||
|
prim_modules = None # None = all
|
||||||
|
if modules is not None:
|
||||||
|
prim_modules = [m for m in _ALL_PY_MODULES if m.startswith("core.")]
|
||||||
|
for m in modules:
|
||||||
|
if m not in prim_modules:
|
||||||
|
if m not in PRIMITIVES_PY_MODULES:
|
||||||
|
raise ValueError(f"Unknown module: {m!r}. Valid: {', '.join(PRIMITIVES_PY_MODULES)}")
|
||||||
|
prim_modules.append(m)
|
||||||
|
|
||||||
ref_dir = os.path.dirname(os.path.abspath(__file__))
|
ref_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
emitter = PyEmitter()
|
emitter = PyEmitter()
|
||||||
|
|
||||||
@@ -849,7 +865,9 @@ def compile_ref_to_py(adapters: list[str] | None = None) -> str:
|
|||||||
parts = []
|
parts = []
|
||||||
parts.append(PREAMBLE)
|
parts.append(PREAMBLE)
|
||||||
parts.append(PLATFORM_PY)
|
parts.append(PLATFORM_PY)
|
||||||
parts.append(PRIMITIVES_PY)
|
parts.append(PRIMITIVES_PY_PRE)
|
||||||
|
parts.append(_assemble_primitives_py(prim_modules))
|
||||||
|
parts.append(PRIMITIVES_PY_POST)
|
||||||
|
|
||||||
for label, defines in all_sections:
|
for label, defines in all_sections:
|
||||||
parts.append(f"\n# === Transpiled from {label} ===\n")
|
parts.append(f"\n# === Transpiled from {label} ===\n")
|
||||||
@@ -1506,29 +1524,14 @@ def aser_special(name, expr, env):
|
|||||||
return trampoline(result)
|
return trampoline(result)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
PRIMITIVES_PY = '''
|
# ---------------------------------------------------------------------------
|
||||||
# =========================================================================
|
# Primitive modules — Python implementations keyed by spec module name.
|
||||||
# Primitives
|
# core.* modules are always included; stdlib.* are opt-in.
|
||||||
# =========================================================================
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# Save builtins before shadowing
|
PRIMITIVES_PY_MODULES: dict[str, str] = {
|
||||||
_b_len = len
|
"core.arithmetic": '''
|
||||||
_b_map = map
|
# core.arithmetic
|
||||||
_b_filter = filter
|
|
||||||
_b_range = range
|
|
||||||
_b_list = list
|
|
||||||
_b_dict = dict
|
|
||||||
_b_max = max
|
|
||||||
_b_min = min
|
|
||||||
_b_round = round
|
|
||||||
_b_abs = abs
|
|
||||||
_b_sum = sum
|
|
||||||
_b_zip = zip
|
|
||||||
_b_int = int
|
|
||||||
|
|
||||||
PRIMITIVES = {}
|
|
||||||
|
|
||||||
# Arithmetic
|
|
||||||
PRIMITIVES["+"] = lambda *args: _b_sum(args)
|
PRIMITIVES["+"] = lambda *args: _b_sum(args)
|
||||||
PRIMITIVES["-"] = lambda a, b=None: -a if b is None else a - b
|
PRIMITIVES["-"] = lambda a, b=None: -a if b is None else a - b
|
||||||
PRIMITIVES["*"] = lambda *args: _sx_mul(*args)
|
PRIMITIVES["*"] = lambda *args: _sx_mul(*args)
|
||||||
@@ -1551,37 +1554,25 @@ def _sx_mul(*args):
|
|||||||
for a in args:
|
for a in args:
|
||||||
r *= a
|
r *= a
|
||||||
return r
|
return r
|
||||||
|
''',
|
||||||
|
|
||||||
# Comparison
|
"core.comparison": '''
|
||||||
|
# core.comparison
|
||||||
PRIMITIVES["="] = lambda a, b: a == b
|
PRIMITIVES["="] = lambda a, b: a == b
|
||||||
PRIMITIVES["!="] = lambda a, b: a != b
|
PRIMITIVES["!="] = lambda a, b: a != b
|
||||||
PRIMITIVES["<"] = lambda a, b: a < b
|
PRIMITIVES["<"] = lambda a, b: a < b
|
||||||
PRIMITIVES[">"] = lambda a, b: a > b
|
PRIMITIVES[">"] = lambda a, b: a > b
|
||||||
PRIMITIVES["<="] = lambda a, b: a <= b
|
PRIMITIVES["<="] = lambda a, b: a <= b
|
||||||
PRIMITIVES[">="] = lambda a, b: a >= b
|
PRIMITIVES[">="] = lambda a, b: a >= b
|
||||||
|
''',
|
||||||
|
|
||||||
# Logic
|
"core.logic": '''
|
||||||
|
# core.logic
|
||||||
PRIMITIVES["not"] = lambda x: not sx_truthy(x)
|
PRIMITIVES["not"] = lambda x: not sx_truthy(x)
|
||||||
|
''',
|
||||||
|
|
||||||
# String
|
"core.predicates": '''
|
||||||
PRIMITIVES["str"] = sx_str
|
# core.predicates
|
||||||
PRIMITIVES["upper"] = lambda s: str(s).upper()
|
|
||||||
PRIMITIVES["lower"] = lambda s: str(s).lower()
|
|
||||||
PRIMITIVES["trim"] = lambda s: str(s).strip()
|
|
||||||
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
|
|
||||||
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
|
|
||||||
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
|
|
||||||
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
|
|
||||||
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
|
|
||||||
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
|
|
||||||
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
|
|
||||||
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
|
|
||||||
|
|
||||||
import re as _re
|
|
||||||
def _strip_tags(s):
|
|
||||||
return _re.sub(r"<[^>]+>", "", s)
|
|
||||||
|
|
||||||
# Predicates
|
|
||||||
PRIMITIVES["nil?"] = lambda x: x is None or x is NIL
|
PRIMITIVES["nil?"] = lambda x: x is None or x is NIL
|
||||||
PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)
|
PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)
|
||||||
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
|
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
|
||||||
@@ -1598,8 +1589,25 @@ PRIMITIVES["contains?"] = lambda c, k: (
|
|||||||
PRIMITIVES["odd?"] = lambda n: n % 2 != 0
|
PRIMITIVES["odd?"] = lambda n: n % 2 != 0
|
||||||
PRIMITIVES["even?"] = lambda n: n % 2 == 0
|
PRIMITIVES["even?"] = lambda n: n % 2 == 0
|
||||||
PRIMITIVES["zero?"] = lambda n: n == 0
|
PRIMITIVES["zero?"] = lambda n: n == 0
|
||||||
|
''',
|
||||||
|
|
||||||
# Collections
|
"core.strings": '''
|
||||||
|
# core.strings
|
||||||
|
PRIMITIVES["str"] = sx_str
|
||||||
|
PRIMITIVES["upper"] = lambda s: str(s).upper()
|
||||||
|
PRIMITIVES["lower"] = lambda s: str(s).lower()
|
||||||
|
PRIMITIVES["trim"] = lambda s: str(s).strip()
|
||||||
|
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
|
||||||
|
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
|
||||||
|
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
|
||||||
|
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
|
||||||
|
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
|
||||||
|
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
|
||||||
|
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
|
||||||
|
''',
|
||||||
|
|
||||||
|
"core.collections": '''
|
||||||
|
# core.collections
|
||||||
PRIMITIVES["list"] = lambda *args: _b_list(args)
|
PRIMITIVES["list"] = lambda *args: _b_list(args)
|
||||||
PRIMITIVES["dict"] = lambda *args: {args[i]: args[i+1] for i in _b_range(0, _b_len(args)-1, 2)}
|
PRIMITIVES["dict"] = lambda *args: {args[i]: args[i+1] for i in _b_range(0, _b_len(args)-1, 2)}
|
||||||
PRIMITIVES["range"] = lambda a, b, step=1: _b_list(_b_range(_b_int(a), _b_int(b), _b_int(step)))
|
PRIMITIVES["range"] = lambda a, b, step=1: _b_list(_b_range(_b_int(a), _b_int(b), _b_int(step)))
|
||||||
@@ -1611,22 +1619,20 @@ PRIMITIVES["rest"] = lambda c: c[1:] if c else []
|
|||||||
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
|
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
|
||||||
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
|
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
|
||||||
PRIMITIVES["append"] = lambda c, x: (c or []) + [x]
|
PRIMITIVES["append"] = lambda c, x: (c or []) + [x]
|
||||||
|
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
|
||||||
|
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
|
||||||
|
''',
|
||||||
|
|
||||||
|
"core.dict": '''
|
||||||
|
# core.dict
|
||||||
PRIMITIVES["keys"] = lambda d: _b_list((d or {}).keys())
|
PRIMITIVES["keys"] = lambda d: _b_list((d or {}).keys())
|
||||||
PRIMITIVES["vals"] = lambda d: _b_list((d or {}).values())
|
PRIMITIVES["vals"] = lambda d: _b_list((d or {}).values())
|
||||||
PRIMITIVES["merge"] = lambda *args: _sx_merge_dicts(*args)
|
PRIMITIVES["merge"] = lambda *args: _sx_merge_dicts(*args)
|
||||||
PRIMITIVES["assoc"] = lambda d, *kvs: _sx_assoc(d, *kvs)
|
PRIMITIVES["assoc"] = lambda d, *kvs: _sx_assoc(d, *kvs)
|
||||||
PRIMITIVES["dissoc"] = lambda d, *ks: {k: v for k, v in d.items() if k not in ks}
|
PRIMITIVES["dissoc"] = lambda d, *ks: {k: v for k, v in d.items() if k not in ks}
|
||||||
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
|
|
||||||
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
|
|
||||||
PRIMITIVES["into"] = lambda target, coll: (_b_list(coll) if isinstance(target, _b_list) else {p[0]: p[1] for p in coll if isinstance(p, _b_list) and _b_len(p) >= 2})
|
PRIMITIVES["into"] = lambda target, coll: (_b_list(coll) if isinstance(target, _b_list) else {p[0]: p[1] for p in coll if isinstance(p, _b_list) and _b_len(p) >= 2})
|
||||||
PRIMITIVES["zip"] = lambda *colls: [_b_list(t) for t in _b_zip(*colls)]
|
PRIMITIVES["zip"] = lambda *colls: [_b_list(t) for t in _b_zip(*colls)]
|
||||||
|
|
||||||
# Format
|
|
||||||
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
|
|
||||||
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
|
|
||||||
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
|
|
||||||
PRIMITIVES["escape"] = escape_html
|
|
||||||
|
|
||||||
def _sx_merge_dicts(*args):
|
def _sx_merge_dicts(*args):
|
||||||
out = {}
|
out = {}
|
||||||
for d in args:
|
for d in args:
|
||||||
@@ -1639,13 +1645,80 @@ def _sx_assoc(d, *kvs):
|
|||||||
for i in _b_range(0, _b_len(kvs) - 1, 2):
|
for i in _b_range(0, _b_len(kvs) - 1, 2):
|
||||||
out[kvs[i]] = kvs[i + 1]
|
out[kvs[i]] = kvs[i + 1]
|
||||||
return out
|
return out
|
||||||
|
''',
|
||||||
|
|
||||||
|
"stdlib.format": '''
|
||||||
|
# stdlib.format
|
||||||
|
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
|
||||||
|
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
|
||||||
|
PRIMITIVES["parse-datetime"] = lambda s: str(s) if s else NIL
|
||||||
|
|
||||||
def _sx_parse_int(v, default=0):
|
def _sx_parse_int(v, default=0):
|
||||||
try:
|
try:
|
||||||
return _b_int(v)
|
return _b_int(v)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return default
|
return default
|
||||||
|
''',
|
||||||
|
|
||||||
|
"stdlib.text": '''
|
||||||
|
# stdlib.text
|
||||||
|
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
|
||||||
|
PRIMITIVES["escape"] = escape_html
|
||||||
|
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
|
||||||
|
|
||||||
|
import re as _re
|
||||||
|
def _strip_tags(s):
|
||||||
|
return _re.sub(r"<[^>]+>", "", s)
|
||||||
|
''',
|
||||||
|
|
||||||
|
"stdlib.style": '''
|
||||||
|
# stdlib.style — stubs (CSSX needs full runtime)
|
||||||
|
''',
|
||||||
|
|
||||||
|
"stdlib.debug": '''
|
||||||
|
# stdlib.debug
|
||||||
|
PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).throw(RuntimeError(f"Assertion error: {msg}")) if not sx_truthy(cond) else True
|
||||||
|
''',
|
||||||
|
}
|
||||||
|
|
||||||
|
_ALL_PY_MODULES = list(PRIMITIVES_PY_MODULES.keys())
|
||||||
|
|
||||||
|
|
||||||
|
def _assemble_primitives_py(modules: list[str] | None = None) -> str:
|
||||||
|
"""Assemble Python primitive code from selected modules."""
|
||||||
|
if modules is None:
|
||||||
|
modules = _ALL_PY_MODULES
|
||||||
|
parts = []
|
||||||
|
for mod in modules:
|
||||||
|
if mod in PRIMITIVES_PY_MODULES:
|
||||||
|
parts.append(PRIMITIVES_PY_MODULES[mod])
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
PRIMITIVES_PY_PRE = '''
|
||||||
|
# =========================================================================
|
||||||
|
# Primitives
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
# Save builtins before shadowing
|
||||||
|
_b_len = len
|
||||||
|
_b_map = map
|
||||||
|
_b_filter = filter
|
||||||
|
_b_range = range
|
||||||
|
_b_list = list
|
||||||
|
_b_dict = dict
|
||||||
|
_b_max = max
|
||||||
|
_b_min = min
|
||||||
|
_b_round = round
|
||||||
|
_b_abs = abs
|
||||||
|
_b_sum = sum
|
||||||
|
_b_zip = zip
|
||||||
|
_b_int = int
|
||||||
|
|
||||||
|
PRIMITIVES = {}
|
||||||
|
'''
|
||||||
|
|
||||||
|
PRIMITIVES_PY_POST = '''
|
||||||
def is_primitive(name):
|
def is_primitive(name):
|
||||||
if name in PRIMITIVES:
|
if name in PRIMITIVES:
|
||||||
return True
|
return True
|
||||||
@@ -1811,9 +1884,15 @@ def main():
|
|||||||
default=None,
|
default=None,
|
||||||
help="Comma-separated adapter names (html,sx). Default: all server-side.",
|
help="Comma-separated adapter names (html,sx). Default: all server-side.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--modules",
|
||||||
|
default=None,
|
||||||
|
help="Comma-separated primitive modules (core.* always included). Default: all.",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
adapters = args.adapters.split(",") if args.adapters else None
|
adapters = args.adapters.split(",") if args.adapters else None
|
||||||
print(compile_ref_to_py(adapters))
|
modules = args.modules.split(",") if args.modules else None
|
||||||
|
print(compile_ref_to_py(adapters, modules))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -42,17 +42,44 @@ def _extract_keyword_arg(expr: list, key: str) -> Any:
|
|||||||
|
|
||||||
def parse_primitives_sx() -> frozenset[str]:
|
def parse_primitives_sx() -> frozenset[str]:
|
||||||
"""Parse primitives.sx and return frozenset of declared pure primitive names."""
|
"""Parse primitives.sx and return frozenset of declared pure primitive names."""
|
||||||
|
by_module = parse_primitives_by_module()
|
||||||
|
all_names: set[str] = set()
|
||||||
|
for names in by_module.values():
|
||||||
|
all_names.update(names)
|
||||||
|
return frozenset(all_names)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_primitives_by_module() -> dict[str, frozenset[str]]:
|
||||||
|
"""Parse primitives.sx and return primitives grouped by module.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping module name (e.g. "core.arithmetic") to frozenset of
|
||||||
|
primitive names declared under that module.
|
||||||
|
"""
|
||||||
source = _read_file("primitives.sx")
|
source = _read_file("primitives.sx")
|
||||||
exprs = parse_all(source)
|
exprs = parse_all(source)
|
||||||
names: set[str] = set()
|
modules: dict[str, set[str]] = {}
|
||||||
|
current_module = "_unscoped"
|
||||||
|
|
||||||
for expr in exprs:
|
for expr in exprs:
|
||||||
if (isinstance(expr, list) and len(expr) >= 2
|
if not isinstance(expr, list) or len(expr) < 2:
|
||||||
and isinstance(expr[0], Symbol)
|
continue
|
||||||
and expr[0].name == "define-primitive"):
|
if not isinstance(expr[0], Symbol):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if expr[0].name == "define-module":
|
||||||
|
mod_name = expr[1]
|
||||||
|
if isinstance(mod_name, Keyword):
|
||||||
|
current_module = mod_name.name
|
||||||
|
elif isinstance(mod_name, str):
|
||||||
|
current_module = mod_name
|
||||||
|
|
||||||
|
elif expr[0].name == "define-primitive":
|
||||||
name = expr[1]
|
name = expr[1]
|
||||||
if isinstance(name, str):
|
if isinstance(name, str):
|
||||||
names.add(name)
|
modules.setdefault(current_module, set()).add(name)
|
||||||
return frozenset(names)
|
|
||||||
|
return {mod: frozenset(names) for mod, names in modules.items()}
|
||||||
|
|
||||||
|
|
||||||
def parse_boundary_sx() -> tuple[frozenset[str], dict[str, frozenset[str]]]:
|
def parse_boundary_sx() -> tuple[frozenset[str], dict[str, frozenset[str]]]:
|
||||||
|
|||||||
@@ -18,13 +18,19 @@
|
|||||||
;; The :body is optional — when provided, it gives a reference
|
;; The :body is optional — when provided, it gives a reference
|
||||||
;; implementation in SX that bootstrap compilers MAY use for testing
|
;; implementation in SX that bootstrap compilers MAY use for testing
|
||||||
;; or as a fallback. Most targets will implement natively for performance.
|
;; or as a fallback. Most targets will implement natively for performance.
|
||||||
|
;;
|
||||||
|
;; Modules: (define-module :name) scopes subsequent define-primitive
|
||||||
|
;; entries until the next define-module. Bootstrappers use this to
|
||||||
|
;; selectively include primitive groups.
|
||||||
;; ==========================================================================
|
;; ==========================================================================
|
||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Arithmetic
|
;; Core — Arithmetic
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define-module :core.arithmetic)
|
||||||
|
|
||||||
(define-primitive "+"
|
(define-primitive "+"
|
||||||
:params (&rest args)
|
:params (&rest args)
|
||||||
:returns "number"
|
:returns "number"
|
||||||
@@ -115,9 +121,11 @@
|
|||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Comparison
|
;; Core — Comparison
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define-module :core.comparison)
|
||||||
|
|
||||||
(define-primitive "="
|
(define-primitive "="
|
||||||
:params (a b)
|
:params (a b)
|
||||||
:returns "boolean"
|
:returns "boolean"
|
||||||
@@ -151,9 +159,11 @@
|
|||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Predicates
|
;; Core — Predicates
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define-module :core.predicates)
|
||||||
|
|
||||||
(define-primitive "odd?"
|
(define-primitive "odd?"
|
||||||
:params (n)
|
:params (n)
|
||||||
:returns "boolean"
|
:returns "boolean"
|
||||||
@@ -209,9 +219,11 @@
|
|||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Logic
|
;; Core — Logic
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define-module :core.logic)
|
||||||
|
|
||||||
(define-primitive "not"
|
(define-primitive "not"
|
||||||
:params (x)
|
:params (x)
|
||||||
:returns "boolean"
|
:returns "boolean"
|
||||||
@@ -219,9 +231,11 @@
|
|||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Strings
|
;; Core — Strings
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define-module :core.strings)
|
||||||
|
|
||||||
(define-primitive "str"
|
(define-primitive "str"
|
||||||
:params (&rest args)
|
:params (&rest args)
|
||||||
:returns "string"
|
:returns "string"
|
||||||
@@ -279,9 +293,11 @@
|
|||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Collections — construction
|
;; Core — Collections
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define-module :core.collections)
|
||||||
|
|
||||||
(define-primitive "list"
|
(define-primitive "list"
|
||||||
:params (&rest args)
|
:params (&rest args)
|
||||||
:returns "list"
|
:returns "list"
|
||||||
@@ -297,11 +313,6 @@
|
|||||||
:returns "list"
|
:returns "list"
|
||||||
:doc "Integer range [start, end) with optional step.")
|
:doc "Integer range [start, end) with optional step.")
|
||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
|
||||||
;; Collections — access
|
|
||||||
;; --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
(define-primitive "get"
|
(define-primitive "get"
|
||||||
:params (coll key &rest default)
|
:params (coll key &rest default)
|
||||||
:returns "any"
|
:returns "any"
|
||||||
@@ -354,9 +365,11 @@
|
|||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Collections — dict operations
|
;; Core — Dict operations
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define-module :core.dict)
|
||||||
|
|
||||||
(define-primitive "keys"
|
(define-primitive "keys"
|
||||||
:params (d)
|
:params (d)
|
||||||
:returns "list"
|
:returns "list"
|
||||||
@@ -389,9 +402,11 @@
|
|||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Format helpers
|
;; Stdlib — Format
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define-module :stdlib.format)
|
||||||
|
|
||||||
(define-primitive "format-date"
|
(define-primitive "format-date"
|
||||||
:params (date-str fmt)
|
:params (date-str fmt)
|
||||||
:returns "string"
|
:returns "string"
|
||||||
@@ -407,11 +422,18 @@
|
|||||||
:returns "number"
|
:returns "number"
|
||||||
:doc "Parse string to integer with optional default on failure.")
|
:doc "Parse string to integer with optional default on failure.")
|
||||||
|
|
||||||
|
(define-primitive "parse-datetime"
|
||||||
|
:params (s)
|
||||||
|
:returns "string"
|
||||||
|
:doc "Parse datetime string — identity passthrough (returns string or nil).")
|
||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Text helpers
|
;; Stdlib — Text
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define-module :stdlib.text)
|
||||||
|
|
||||||
(define-primitive "pluralize"
|
(define-primitive "pluralize"
|
||||||
:params (count &rest forms)
|
:params (count &rest forms)
|
||||||
:returns "string"
|
:returns "string"
|
||||||
@@ -429,33 +451,10 @@
|
|||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Date & parsing helpers
|
;; Stdlib — Style
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
(define-primitive "parse-datetime"
|
(define-module :stdlib.style)
|
||||||
:params (s)
|
|
||||||
:returns "string"
|
|
||||||
:doc "Parse datetime string — identity passthrough (returns string or nil).")
|
|
||||||
|
|
||||||
(define-primitive "split-ids"
|
|
||||||
:params (s)
|
|
||||||
:returns "list"
|
|
||||||
:doc "Split comma-separated ID string into list of trimmed non-empty strings.")
|
|
||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
|
||||||
;; Assertions
|
|
||||||
;; --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
(define-primitive "assert"
|
|
||||||
:params (condition &rest message)
|
|
||||||
:returns "boolean"
|
|
||||||
:doc "Assert condition is truthy; raise error with message if not.")
|
|
||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
|
||||||
;; CSSX — style system primitives
|
|
||||||
;; --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
(define-primitive "css"
|
(define-primitive "css"
|
||||||
:params (&rest atoms)
|
:params (&rest atoms)
|
||||||
@@ -467,3 +466,15 @@
|
|||||||
:params (&rest styles)
|
:params (&rest styles)
|
||||||
:returns "style-value"
|
:returns "style-value"
|
||||||
:doc "Merge multiple StyleValues into one combined StyleValue.")
|
:doc "Merge multiple StyleValues into one combined StyleValue.")
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Stdlib — Debug
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define-module :stdlib.debug)
|
||||||
|
|
||||||
|
(define-primitive "assert"
|
||||||
|
:params (condition &rest message)
|
||||||
|
:returns "boolean"
|
||||||
|
:doc "Assert condition is truthy; raise error with message if not.")
|
||||||
|
|||||||
@@ -656,7 +656,8 @@ _b_int = int
|
|||||||
|
|
||||||
PRIMITIVES = {}
|
PRIMITIVES = {}
|
||||||
|
|
||||||
# Arithmetic
|
|
||||||
|
# core.arithmetic
|
||||||
PRIMITIVES["+"] = lambda *args: _b_sum(args)
|
PRIMITIVES["+"] = lambda *args: _b_sum(args)
|
||||||
PRIMITIVES["-"] = lambda a, b=None: -a if b is None else a - b
|
PRIMITIVES["-"] = lambda a, b=None: -a if b is None else a - b
|
||||||
PRIMITIVES["*"] = lambda *args: _sx_mul(*args)
|
PRIMITIVES["*"] = lambda *args: _sx_mul(*args)
|
||||||
@@ -680,7 +681,8 @@ def _sx_mul(*args):
|
|||||||
r *= a
|
r *= a
|
||||||
return r
|
return r
|
||||||
|
|
||||||
# Comparison
|
|
||||||
|
# core.comparison
|
||||||
PRIMITIVES["="] = lambda a, b: a == b
|
PRIMITIVES["="] = lambda a, b: a == b
|
||||||
PRIMITIVES["!="] = lambda a, b: a != b
|
PRIMITIVES["!="] = lambda a, b: a != b
|
||||||
PRIMITIVES["<"] = lambda a, b: a < b
|
PRIMITIVES["<"] = lambda a, b: a < b
|
||||||
@@ -688,28 +690,12 @@ PRIMITIVES[">"] = lambda a, b: a > b
|
|||||||
PRIMITIVES["<="] = lambda a, b: a <= b
|
PRIMITIVES["<="] = lambda a, b: a <= b
|
||||||
PRIMITIVES[">="] = lambda a, b: a >= b
|
PRIMITIVES[">="] = lambda a, b: a >= b
|
||||||
|
|
||||||
# Logic
|
|
||||||
|
# core.logic
|
||||||
PRIMITIVES["not"] = lambda x: not sx_truthy(x)
|
PRIMITIVES["not"] = lambda x: not sx_truthy(x)
|
||||||
|
|
||||||
# String
|
|
||||||
PRIMITIVES["str"] = sx_str
|
|
||||||
PRIMITIVES["upper"] = lambda s: str(s).upper()
|
|
||||||
PRIMITIVES["lower"] = lambda s: str(s).lower()
|
|
||||||
PRIMITIVES["trim"] = lambda s: str(s).strip()
|
|
||||||
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
|
|
||||||
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
|
|
||||||
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
|
|
||||||
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
|
|
||||||
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
|
|
||||||
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
|
|
||||||
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
|
|
||||||
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
|
|
||||||
|
|
||||||
import re as _re
|
# core.predicates
|
||||||
def _strip_tags(s):
|
|
||||||
return _re.sub(r"<[^>]+>", "", s)
|
|
||||||
|
|
||||||
# Predicates
|
|
||||||
PRIMITIVES["nil?"] = lambda x: x is None or x is NIL
|
PRIMITIVES["nil?"] = lambda x: x is None or x is NIL
|
||||||
PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)
|
PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)
|
||||||
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
|
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
|
||||||
@@ -727,7 +713,22 @@ PRIMITIVES["odd?"] = lambda n: n % 2 != 0
|
|||||||
PRIMITIVES["even?"] = lambda n: n % 2 == 0
|
PRIMITIVES["even?"] = lambda n: n % 2 == 0
|
||||||
PRIMITIVES["zero?"] = lambda n: n == 0
|
PRIMITIVES["zero?"] = lambda n: n == 0
|
||||||
|
|
||||||
# Collections
|
|
||||||
|
# core.strings
|
||||||
|
PRIMITIVES["str"] = sx_str
|
||||||
|
PRIMITIVES["upper"] = lambda s: str(s).upper()
|
||||||
|
PRIMITIVES["lower"] = lambda s: str(s).lower()
|
||||||
|
PRIMITIVES["trim"] = lambda s: str(s).strip()
|
||||||
|
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
|
||||||
|
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
|
||||||
|
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
|
||||||
|
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
|
||||||
|
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
|
||||||
|
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
|
||||||
|
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
|
||||||
|
|
||||||
|
|
||||||
|
# core.collections
|
||||||
PRIMITIVES["list"] = lambda *args: _b_list(args)
|
PRIMITIVES["list"] = lambda *args: _b_list(args)
|
||||||
PRIMITIVES["dict"] = lambda *args: {args[i]: args[i+1] for i in _b_range(0, _b_len(args)-1, 2)}
|
PRIMITIVES["dict"] = lambda *args: {args[i]: args[i+1] for i in _b_range(0, _b_len(args)-1, 2)}
|
||||||
PRIMITIVES["range"] = lambda a, b, step=1: _b_list(_b_range(_b_int(a), _b_int(b), _b_int(step)))
|
PRIMITIVES["range"] = lambda a, b, step=1: _b_list(_b_range(_b_int(a), _b_int(b), _b_int(step)))
|
||||||
@@ -739,22 +740,19 @@ PRIMITIVES["rest"] = lambda c: c[1:] if c else []
|
|||||||
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
|
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
|
||||||
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
|
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
|
||||||
PRIMITIVES["append"] = lambda c, x: (c or []) + [x]
|
PRIMITIVES["append"] = lambda c, x: (c or []) + [x]
|
||||||
|
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
|
||||||
|
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
|
||||||
|
|
||||||
|
|
||||||
|
# core.dict
|
||||||
PRIMITIVES["keys"] = lambda d: _b_list((d or {}).keys())
|
PRIMITIVES["keys"] = lambda d: _b_list((d or {}).keys())
|
||||||
PRIMITIVES["vals"] = lambda d: _b_list((d or {}).values())
|
PRIMITIVES["vals"] = lambda d: _b_list((d or {}).values())
|
||||||
PRIMITIVES["merge"] = lambda *args: _sx_merge_dicts(*args)
|
PRIMITIVES["merge"] = lambda *args: _sx_merge_dicts(*args)
|
||||||
PRIMITIVES["assoc"] = lambda d, *kvs: _sx_assoc(d, *kvs)
|
PRIMITIVES["assoc"] = lambda d, *kvs: _sx_assoc(d, *kvs)
|
||||||
PRIMITIVES["dissoc"] = lambda d, *ks: {k: v for k, v in d.items() if k not in ks}
|
PRIMITIVES["dissoc"] = lambda d, *ks: {k: v for k, v in d.items() if k not in ks}
|
||||||
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
|
|
||||||
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
|
|
||||||
PRIMITIVES["into"] = lambda target, coll: (_b_list(coll) if isinstance(target, _b_list) else {p[0]: p[1] for p in coll if isinstance(p, _b_list) and _b_len(p) >= 2})
|
PRIMITIVES["into"] = lambda target, coll: (_b_list(coll) if isinstance(target, _b_list) else {p[0]: p[1] for p in coll if isinstance(p, _b_list) and _b_len(p) >= 2})
|
||||||
PRIMITIVES["zip"] = lambda *colls: [_b_list(t) for t in _b_zip(*colls)]
|
PRIMITIVES["zip"] = lambda *colls: [_b_list(t) for t in _b_zip(*colls)]
|
||||||
|
|
||||||
# Format
|
|
||||||
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
|
|
||||||
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
|
|
||||||
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
|
|
||||||
PRIMITIVES["escape"] = escape_html
|
|
||||||
|
|
||||||
def _sx_merge_dicts(*args):
|
def _sx_merge_dicts(*args):
|
||||||
out = {}
|
out = {}
|
||||||
for d in args:
|
for d in args:
|
||||||
@@ -768,12 +766,36 @@ def _sx_assoc(d, *kvs):
|
|||||||
out[kvs[i]] = kvs[i + 1]
|
out[kvs[i]] = kvs[i + 1]
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# stdlib.format
|
||||||
|
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
|
||||||
|
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
|
||||||
|
PRIMITIVES["parse-datetime"] = lambda s: str(s) if s else NIL
|
||||||
|
|
||||||
def _sx_parse_int(v, default=0):
|
def _sx_parse_int(v, default=0):
|
||||||
try:
|
try:
|
||||||
return _b_int(v)
|
return _b_int(v)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
# stdlib.text
|
||||||
|
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
|
||||||
|
PRIMITIVES["escape"] = escape_html
|
||||||
|
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
|
||||||
|
|
||||||
|
import re as _re
|
||||||
|
def _strip_tags(s):
|
||||||
|
return _re.sub(r"<[^>]+>", "", s)
|
||||||
|
|
||||||
|
|
||||||
|
# stdlib.style — stubs (CSSX needs full runtime)
|
||||||
|
|
||||||
|
|
||||||
|
# stdlib.debug
|
||||||
|
PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).throw(RuntimeError(f"Assertion error: {msg}")) if not sx_truthy(cond) else True
|
||||||
|
|
||||||
|
|
||||||
def is_primitive(name):
|
def is_primitive(name):
|
||||||
if name in PRIMITIVES:
|
if name in PRIMITIVES:
|
||||||
return True
|
return True
|
||||||
@@ -1161,4 +1183,4 @@ def render(expr, env=None):
|
|||||||
|
|
||||||
def make_env(**kwargs):
|
def make_env(**kwargs):
|
||||||
"""Create an environment dict with initial bindings."""
|
"""Create an environment dict with initial bindings."""
|
||||||
return dict(kwargs)
|
return dict(kwargs)
|
||||||
Reference in New Issue
Block a user