Merge branch 'worktree-refactor-primitives' into macros
# Conflicts: # shared/sx/ref/bootstrap_js.py # shared/sx/ref/bootstrap_py.py
This commit is contained in:
@@ -10,7 +10,8 @@
|
||||
|
||||
(defquery posts-by-ids (&key 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)
|
||||
"Search blog posts by text query, paginated."
|
||||
@@ -35,4 +36,5 @@
|
||||
(defquery page-configs-batch (&key container-type ids)
|
||||
"Return PageConfigs for multiple container IDs (comma-separated)."
|
||||
(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)
|
||||
"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)
|
||||
"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,
|
||||
register_primitive,
|
||||
)
|
||||
from . import primitives_stdlib # noqa: F401 — registers stdlib primitives
|
||||
from .env import Env
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
import math
|
||||
from typing import Any, Callable
|
||||
|
||||
from .types import Keyword, Lambda, NIL
|
||||
from .types import Keyword, NIL
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -291,12 +291,6 @@ def prim_join(sep: str, coll: list) -> str:
|
||||
def prim_replace(s: str, old: str, new: str) -> str:
|
||||
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")
|
||||
def prim_slice(coll: Any, start: int, end: Any = None) -> Any:
|
||||
"""Slice a string or list: (slice coll start end?)."""
|
||||
@@ -458,181 +452,3 @@ def prim_into(target: Any, coll: Any) -> Any:
|
||||
return result
|
||||
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
|
||||
@@ -1087,14 +1087,20 @@ CONTINUATIONS_JS = '''
|
||||
'''
|
||||
|
||||
|
||||
def compile_ref_to_js(adapters: list[str] | None = None,
|
||||
extensions: list[str] | None = None) -> str:
|
||||
def compile_ref_to_js(
|
||||
adapters: list[str] | None = None,
|
||||
modules: list[str] | None = None,
|
||||
extensions: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Read reference .sx files and emit JavaScript.
|
||||
|
||||
Args:
|
||||
adapters: List of adapter names to include.
|
||||
Valid names: html, sx, dom, engine.
|
||||
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).
|
||||
extensions: List of optional extensions to include.
|
||||
Valid names: continuations.
|
||||
None = no extensions.
|
||||
@@ -1164,9 +1170,25 @@ def compile_ref_to_js(adapters: list[str] | None = None,
|
||||
has_parser = "parser" in adapter_set
|
||||
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.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
|
||||
if has_parser:
|
||||
@@ -1287,7 +1309,235 @@ PREAMBLE = '''\
|
||||
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
|
||||
// =========================================================================
|
||||
@@ -1411,180 +1661,9 @@ PLATFORM_JS = '''
|
||||
function error(msg) { throw new Error(msg); }
|
||||
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 getPrimitive(name) { return PRIMITIVES[name]; }
|
||||
|
||||
@@ -2931,6 +3010,8 @@ if __name__ == "__main__":
|
||||
p = argparse.ArgumentParser(description="Bootstrap-compile SX reference spec to JavaScript")
|
||||
p.add_argument("--adapters", "-a",
|
||||
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("--extensions",
|
||||
help="Comma-separated extensions (continuations). Default: none.")
|
||||
p.add_argument("--output", "-o",
|
||||
@@ -2938,15 +3019,17 @@ if __name__ == "__main__":
|
||||
args = p.parse_args()
|
||||
|
||||
adapters = args.adapters.split(",") if args.adapters else None
|
||||
modules = args.modules.split(",") if args.modules else None
|
||||
extensions = args.extensions.split(",") if args.extensions else None
|
||||
js = compile_ref_to_js(adapters, extensions)
|
||||
js = compile_ref_to_js(adapters, modules, extensions)
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
f.write(js)
|
||||
included = ", ".join(adapters) if adapters else "all"
|
||||
mods = ", ".join(modules) if modules else "all"
|
||||
ext_label = ", ".join(extensions) if extensions else "none"
|
||||
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included}, extensions: {ext_label})",
|
||||
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included}, modules: {mods}, extensions: {ext_label})",
|
||||
file=sys.stderr)
|
||||
else:
|
||||
print(js)
|
||||
|
||||
@@ -833,11 +833,8 @@ def _extract_eval_dispatch_names(all_sections: list) -> set[str]:
|
||||
names = set()
|
||||
for _label, defines in all_sections:
|
||||
for name, _expr in defines:
|
||||
# sf-* functions correspond to dispatched special forms
|
||||
if name.startswith("sf-"):
|
||||
# sf-if → if, sf-set! → set!, sf-named-let → named-let
|
||||
form = name[3:]
|
||||
# Map back: sf_cond_scheme etc. are internal, skip
|
||||
if form in ("cond-scheme", "cond-clojure", "case-loop"):
|
||||
continue
|
||||
names.add(form)
|
||||
@@ -852,17 +849,13 @@ def _validate_special_forms(ref_dir: str, all_sections: list,
|
||||
"""Cross-check special-forms.sx against eval.sx dispatch. Warn on mismatches."""
|
||||
spec_names = _parse_special_forms_spec(ref_dir)
|
||||
if not spec_names:
|
||||
return # no spec file, skip validation
|
||||
return
|
||||
|
||||
# Collect what eval.sx dispatches
|
||||
dispatch_names = _extract_eval_dispatch_names(all_sections)
|
||||
|
||||
# Add extension forms if enabled
|
||||
if has_continuations:
|
||||
dispatch_names |= EXTENSION_FORMS["continuations"]
|
||||
|
||||
# Normalize: eval.sx sf-* names don't always match form names directly
|
||||
# sf-thread-first → ->, sf-named-let is internal, ho-every → every?
|
||||
name_aliases = {
|
||||
"thread-first": "->",
|
||||
"every": "every?",
|
||||
@@ -872,17 +865,13 @@ def _validate_special_forms(ref_dir: str, all_sections: list,
|
||||
for n in dispatch_names:
|
||||
normalized_dispatch.add(name_aliases.get(n, n))
|
||||
|
||||
# Internal helpers that aren't user-facing forms
|
||||
internal = {"named-let"}
|
||||
normalized_dispatch -= internal
|
||||
|
||||
# Forms in spec but not dispatched
|
||||
undispatched = spec_names - normalized_dispatch
|
||||
# Ignore aliases and domain forms that are handled differently
|
||||
ignore = {"fn", "let*", "do", "defrelation"}
|
||||
undispatched -= ignore
|
||||
|
||||
# Forms dispatched but not in spec
|
||||
unspecced = normalized_dispatch - spec_names
|
||||
unspecced -= ignore
|
||||
|
||||
@@ -896,18 +885,34 @@ def _validate_special_forms(ref_dir: str, all_sections: list,
|
||||
f"{', '.join(sorted(unspecced))}", file=sys.stderr)
|
||||
|
||||
|
||||
def compile_ref_to_py(adapters: list[str] | None = None,
|
||||
extensions: list[str] | None = None) -> str:
|
||||
def compile_ref_to_py(
|
||||
adapters: list[str] | None = None,
|
||||
modules: list[str] | None = None,
|
||||
extensions: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Read reference .sx files and emit Python.
|
||||
|
||||
Args:
|
||||
adapters: List of adapter names to include.
|
||||
Valid names: html, sx.
|
||||
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).
|
||||
extensions: List of optional extensions to include.
|
||||
Valid names: continuations.
|
||||
None = no extensions.
|
||||
"""
|
||||
# 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__))
|
||||
emitter = PyEmitter()
|
||||
|
||||
@@ -960,7 +965,9 @@ def compile_ref_to_py(adapters: list[str] | None = None,
|
||||
parts = []
|
||||
parts.append(PREAMBLE)
|
||||
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:
|
||||
parts.append(f"\n# === Transpiled from {label} ===\n")
|
||||
@@ -1621,29 +1628,14 @@ def aser_special(name, expr, env):
|
||||
return trampoline(result)
|
||||
'''
|
||||
|
||||
PRIMITIVES_PY = '''
|
||||
# =========================================================================
|
||||
# Primitives
|
||||
# =========================================================================
|
||||
# ---------------------------------------------------------------------------
|
||||
# Primitive modules — Python implementations keyed by spec module name.
|
||||
# core.* modules are always included; stdlib.* are opt-in.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 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 = {}
|
||||
|
||||
# Arithmetic
|
||||
PRIMITIVES_PY_MODULES: dict[str, str] = {
|
||||
"core.arithmetic": '''
|
||||
# core.arithmetic
|
||||
PRIMITIVES["+"] = lambda *args: _b_sum(args)
|
||||
PRIMITIVES["-"] = lambda a, b=None: -a if b is None else a - b
|
||||
PRIMITIVES["*"] = lambda *args: _sx_mul(*args)
|
||||
@@ -1666,37 +1658,25 @@ def _sx_mul(*args):
|
||||
for a in args:
|
||||
r *= a
|
||||
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
|
||||
''',
|
||||
|
||||
# Logic
|
||||
"core.logic": '''
|
||||
# core.logic
|
||||
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
|
||||
def _strip_tags(s):
|
||||
return _re.sub(r"<[^>]+>", "", s)
|
||||
|
||||
# Predicates
|
||||
"core.predicates": '''
|
||||
# core.predicates
|
||||
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["string?"] = lambda x: isinstance(x, str)
|
||||
@@ -1714,8 +1694,25 @@ PRIMITIVES["contains?"] = lambda c, k: (
|
||||
PRIMITIVES["odd?"] = lambda n: n % 2 != 0
|
||||
PRIMITIVES["even?"] = lambda n: n % 2 == 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["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)))
|
||||
@@ -1727,22 +1724,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["cons"] = lambda x, c: [x] + (c or [])
|
||||
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["vals"] = lambda d: _b_list((d or {}).values())
|
||||
PRIMITIVES["merge"] = lambda *args: _sx_merge_dicts(*args)
|
||||
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["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["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):
|
||||
out = {}
|
||||
for d in args:
|
||||
@@ -1755,13 +1750,80 @@ def _sx_assoc(d, *kvs):
|
||||
for i in _b_range(0, _b_len(kvs) - 1, 2):
|
||||
out[kvs[i]] = kvs[i + 1]
|
||||
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):
|
||||
try:
|
||||
return _b_int(v)
|
||||
except (ValueError, TypeError):
|
||||
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):
|
||||
if name in PRIMITIVES:
|
||||
return True
|
||||
@@ -1986,6 +2048,11 @@ def main():
|
||||
default=None,
|
||||
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.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--extensions",
|
||||
default=None,
|
||||
@@ -1993,8 +2060,9 @@ def main():
|
||||
)
|
||||
args = parser.parse_args()
|
||||
adapters = args.adapters.split(",") if args.adapters else None
|
||||
modules = args.modules.split(",") if args.modules else None
|
||||
extensions = args.extensions.split(",") if args.extensions else None
|
||||
print(compile_ref_to_py(adapters, extensions))
|
||||
print(compile_ref_to_py(adapters, modules, extensions))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -42,17 +42,44 @@ def _extract_keyword_arg(expr: list, key: str) -> Any:
|
||||
|
||||
def parse_primitives_sx() -> frozenset[str]:
|
||||
"""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")
|
||||
exprs = parse_all(source)
|
||||
names: set[str] = set()
|
||||
modules: dict[str, set[str]] = {}
|
||||
current_module = "_unscoped"
|
||||
|
||||
for expr in exprs:
|
||||
if (isinstance(expr, list) and len(expr) >= 2
|
||||
and isinstance(expr[0], Symbol)
|
||||
and expr[0].name == "define-primitive"):
|
||||
if not isinstance(expr, list) or len(expr) < 2:
|
||||
continue
|
||||
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]
|
||||
if isinstance(name, str):
|
||||
names.add(name)
|
||||
return frozenset(names)
|
||||
modules.setdefault(current_module, set()).add(name)
|
||||
|
||||
return {mod: frozenset(names) for mod, names in modules.items()}
|
||||
|
||||
|
||||
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
|
||||
;; implementation in SX that bootstrap compilers MAY use for testing
|
||||
;; 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 "+"
|
||||
:params (&rest args)
|
||||
:returns "number"
|
||||
@@ -115,9 +121,11 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Comparison
|
||||
;; Core — Comparison
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-module :core.comparison)
|
||||
|
||||
(define-primitive "="
|
||||
:params (a b)
|
||||
:returns "boolean"
|
||||
@@ -172,9 +180,11 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Predicates
|
||||
;; Core — Predicates
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-module :core.predicates)
|
||||
|
||||
(define-primitive "odd?"
|
||||
:params (n)
|
||||
:returns "boolean"
|
||||
@@ -235,9 +245,11 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Logic
|
||||
;; Core — Logic
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-module :core.logic)
|
||||
|
||||
(define-primitive "not"
|
||||
:params (x)
|
||||
:returns "boolean"
|
||||
@@ -245,9 +257,11 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Strings
|
||||
;; Core — Strings
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-module :core.strings)
|
||||
|
||||
(define-primitive "str"
|
||||
:params (&rest args)
|
||||
:returns "string"
|
||||
@@ -305,9 +319,11 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Collections — construction
|
||||
;; Core — Collections
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-module :core.collections)
|
||||
|
||||
(define-primitive "list"
|
||||
:params (&rest args)
|
||||
:returns "list"
|
||||
@@ -323,11 +339,6 @@
|
||||
:returns "list"
|
||||
:doc "Integer range [start, end) with optional step.")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Collections — access
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-primitive "get"
|
||||
:params (coll key &rest default)
|
||||
:returns "any"
|
||||
@@ -380,9 +391,11 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Collections — dict operations
|
||||
;; Core — Dict operations
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-module :core.dict)
|
||||
|
||||
(define-primitive "keys"
|
||||
:params (d)
|
||||
:returns "list"
|
||||
@@ -415,9 +428,11 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Format helpers
|
||||
;; Stdlib — Format
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-module :stdlib.format)
|
||||
|
||||
(define-primitive "format-date"
|
||||
:params (date-str fmt)
|
||||
:returns "string"
|
||||
@@ -433,11 +448,18 @@
|
||||
:returns "number"
|
||||
: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"
|
||||
:params (count &rest forms)
|
||||
:returns "string"
|
||||
@@ -455,33 +477,10 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Date & parsing helpers
|
||||
;; Stdlib — Style
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-primitive "parse-datetime"
|
||||
: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-module :stdlib.style)
|
||||
|
||||
(define-primitive "css"
|
||||
:params (&rest atoms)
|
||||
@@ -493,3 +492,15 @@
|
||||
:params (&rest styles)
|
||||
:returns "style-value"
|
||||
: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.")
|
||||
|
||||
@@ -658,7 +658,8 @@ _b_int = int
|
||||
|
||||
PRIMITIVES = {}
|
||||
|
||||
# Arithmetic
|
||||
|
||||
# core.arithmetic
|
||||
PRIMITIVES["+"] = lambda *args: _b_sum(args)
|
||||
PRIMITIVES["-"] = lambda a, b=None: -a if b is None else a - b
|
||||
PRIMITIVES["*"] = lambda *args: _sx_mul(*args)
|
||||
@@ -682,7 +683,8 @@ def _sx_mul(*args):
|
||||
r *= a
|
||||
return r
|
||||
|
||||
# Comparison
|
||||
|
||||
# core.comparison
|
||||
PRIMITIVES["="] = lambda a, b: a == b
|
||||
PRIMITIVES["!="] = lambda a, b: a != b
|
||||
PRIMITIVES["<"] = lambda a, b: a < b
|
||||
@@ -690,28 +692,12 @@ 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)
|
||||
|
||||
# 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
|
||||
def _strip_tags(s):
|
||||
return _re.sub(r"<[^>]+>", "", s)
|
||||
|
||||
# Predicates
|
||||
# core.predicates
|
||||
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["string?"] = lambda x: isinstance(x, str)
|
||||
@@ -730,7 +716,22 @@ PRIMITIVES["odd?"] = lambda n: n % 2 != 0
|
||||
PRIMITIVES["even?"] = lambda n: n % 2 == 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["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)))
|
||||
@@ -742,22 +743,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["cons"] = lambda x, c: [x] + (c or [])
|
||||
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["vals"] = lambda d: _b_list((d or {}).values())
|
||||
PRIMITIVES["merge"] = lambda *args: _sx_merge_dicts(*args)
|
||||
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["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["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):
|
||||
out = {}
|
||||
for d in args:
|
||||
@@ -771,12 +769,36 @@ def _sx_assoc(d, *kvs):
|
||||
out[kvs[i]] = kvs[i + 1]
|
||||
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):
|
||||
try:
|
||||
return _b_int(v)
|
||||
except (ValueError, TypeError):
|
||||
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):
|
||||
if name in PRIMITIVES:
|
||||
return True
|
||||
@@ -864,7 +886,7 @@ trampoline = lambda val: (lambda result: (trampoline(eval_expr(thunk_expr(result
|
||||
eval_expr = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('dict', lambda: map_dict(lambda k, v: trampoline(eval_expr(v, env)), expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else eval_list(expr, env))), (None, lambda: expr)])
|
||||
|
||||
# eval-list
|
||||
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defkeyframes(args, env) if sx_truthy((name == 'defkeyframes')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
|
||||
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defkeyframes(args, env) if sx_truthy((name == 'defkeyframes')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
|
||||
|
||||
# eval-call
|
||||
eval_call = lambda head, args, env: (lambda f: (lambda evaluated_args: (apply(f, evaluated_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))) else (call_lambda(f, evaluated_args, env) if sx_truthy(is_lambda(f)) else (call_component(f, args, env) if sx_truthy(is_component(f)) else error(sx_str('Not callable: ', inspect(f)))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))
|
||||
|
||||
Reference in New Issue
Block a user