Spec modularization: - Add (define-module :name) markers to primitives.sx creating 11 modules (7 core, 4 stdlib). Bootstrappers can now selectively include modules. - Add parse_primitives_by_module() to boundary_parser.py. - Remove split-ids primitive; inline at 4 call sites in blog/market queries. Python file split: - primitives.py: slimmed to registry + core primitives only (~350 lines) - primitives_stdlib.py: NEW — stdlib primitives (format, text, style, debug) - primitives_ctx.py: NEW — extracted 12 page context builders from IO - primitives_io.py: add register_io_handler decorator, auto-derive IO_PRIMITIVES from registry, move sync IO bridges here JS parity fixes: - = uses === (strict equality), != uses !== - round supports optional ndigits parameter - concat uses nil-check not falsy-check (preserves 0, "", false) - escape adds single quote entity (') matching Python/markupsafe - assert added (was missing from JS entirely) Bootstrapper modularization: - PRIMITIVES_JS_MODULES / PRIMITIVES_PY_MODULES dicts keyed by module - --modules CLI flag for selective inclusion (core.* always included) - Regenerated sx-ref.js and sx_ref.py with all fixes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
4.1 KiB
Python
132 lines
4.1 KiB
Python
"""
|
|
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
|