NIL is a frozen sentinel object ({_nil:true}) which is truthy in JS.
(not expr) compiled to !expr, so (not nil) returned false instead of
true. Fixed to compile as !isSxTruthy(expr) which correctly handles
NIL. This was preventing client-side routing from activating.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3259 lines
121 KiB
Python
3259 lines
121 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Bootstrap compiler: reference SX evaluator → JavaScript.
|
|
|
|
Reads the .sx reference specification and emits a standalone JavaScript
|
|
evaluator (sx-ref.js) that can be compared against the hand-written sx.js.
|
|
|
|
The compiler translates the restricted SX subset used in eval.sx/render.sx
|
|
into idiomatic JavaScript. Platform interface functions are emitted as
|
|
native JS implementations.
|
|
|
|
Usage:
|
|
python bootstrap_js.py > sx-ref.js
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
# Add project root to path for imports
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
|
sys.path.insert(0, _PROJECT)
|
|
|
|
from shared.sx.parser import parse_all
|
|
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SX → JavaScript transpiler
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class JSEmitter:
|
|
"""Transpile an SX AST node to JavaScript source code."""
|
|
|
|
def __init__(self):
|
|
self.indent = 0
|
|
|
|
def emit(self, expr) -> str:
|
|
"""Emit a JS expression from an SX AST node."""
|
|
# Bool MUST be checked before int (bool is subclass of int in Python)
|
|
if isinstance(expr, bool):
|
|
return "true" if expr else "false"
|
|
if isinstance(expr, (int, float)):
|
|
return str(expr)
|
|
if isinstance(expr, str):
|
|
return self._js_string(expr)
|
|
if expr is None or expr is SX_NIL:
|
|
return "NIL"
|
|
if isinstance(expr, Symbol):
|
|
return self._emit_symbol(expr.name)
|
|
if isinstance(expr, Keyword):
|
|
return self._js_string(expr.name)
|
|
if isinstance(expr, dict):
|
|
return self._emit_native_dict(expr)
|
|
if isinstance(expr, list):
|
|
return self._emit_list(expr)
|
|
return str(expr)
|
|
|
|
def emit_statement(self, expr) -> str:
|
|
"""Emit a JS statement (with semicolon) from an SX AST node."""
|
|
if isinstance(expr, list) and expr:
|
|
head = expr[0]
|
|
if isinstance(head, Symbol):
|
|
name = head.name
|
|
if name == "define":
|
|
return self._emit_define(expr)
|
|
if name == "set!":
|
|
return f"{self._mangle(expr[1].name)} = {self.emit(expr[2])};"
|
|
if name == "when":
|
|
return self._emit_when_stmt(expr)
|
|
if name == "do" or name == "begin":
|
|
return "\n".join(self.emit_statement(e) for e in expr[1:])
|
|
if name == "for-each":
|
|
return self._emit_for_each_stmt(expr)
|
|
if name == "dict-set!":
|
|
return f"{self.emit(expr[1])}[{self.emit(expr[2])}] = {self.emit(expr[3])};"
|
|
if name == "append!":
|
|
return f"{self.emit(expr[1])}.push({self.emit(expr[2])});"
|
|
if name == "env-set!":
|
|
return f"{self.emit(expr[1])}[{self.emit(expr[2])}] = {self.emit(expr[3])};"
|
|
if name == "set-lambda-name!":
|
|
return f"{self.emit(expr[1])}.name = {self.emit(expr[2])};"
|
|
return f"{self.emit(expr)};"
|
|
|
|
# --- Symbol emission ---
|
|
|
|
def _emit_symbol(self, name: str) -> str:
|
|
# Map SX names to JS names
|
|
return self._mangle(name)
|
|
|
|
def _mangle(self, name: str) -> str:
|
|
"""Convert SX identifier to valid JS identifier."""
|
|
RENAMES = {
|
|
"nil": "NIL",
|
|
"true": "true",
|
|
"false": "false",
|
|
"nil?": "isNil",
|
|
"type-of": "typeOf",
|
|
"symbol-name": "symbolName",
|
|
"keyword-name": "keywordName",
|
|
"make-lambda": "makeLambda",
|
|
"make-component": "makeComponent",
|
|
"make-macro": "makeMacro",
|
|
"make-thunk": "makeThunk",
|
|
"make-symbol": "makeSymbol",
|
|
"make-keyword": "makeKeyword",
|
|
"lambda-params": "lambdaParams",
|
|
"lambda-body": "lambdaBody",
|
|
"lambda-closure": "lambdaClosure",
|
|
"lambda-name": "lambdaName",
|
|
"set-lambda-name!": "setLambdaName",
|
|
"component-params": "componentParams",
|
|
"component-body": "componentBody",
|
|
"component-closure": "componentClosure",
|
|
"component-has-children?": "componentHasChildren",
|
|
"component-name": "componentName",
|
|
"macro-params": "macroParams",
|
|
"macro-rest-param": "macroRestParam",
|
|
"macro-body": "macroBody",
|
|
"macro-closure": "macroClosure",
|
|
"thunk?": "isThunk",
|
|
"thunk-expr": "thunkExpr",
|
|
"thunk-env": "thunkEnv",
|
|
"callable?": "isCallable",
|
|
"lambda?": "isLambda",
|
|
"component?": "isComponent",
|
|
"macro?": "isMacro",
|
|
"primitive?": "isPrimitive",
|
|
"get-primitive": "getPrimitive",
|
|
"env-has?": "envHas",
|
|
"env-get": "envGet",
|
|
"env-set!": "envSet",
|
|
"env-extend": "envExtend",
|
|
"env-merge": "envMerge",
|
|
"dict-set!": "dictSet",
|
|
"dict-get": "dictGet",
|
|
"eval-expr": "evalExpr",
|
|
"eval-list": "evalList",
|
|
"eval-call": "evalCall",
|
|
"is-render-expr?": "isRenderExpr",
|
|
"render-expr": "renderExpr",
|
|
"call-lambda": "callLambda",
|
|
"call-component": "callComponent",
|
|
"parse-keyword-args": "parseKeywordArgs",
|
|
"parse-comp-params": "parseCompParams",
|
|
"parse-macro-params": "parseMacroParams",
|
|
"expand-macro": "expandMacro",
|
|
"render-to-html": "renderToHtml",
|
|
"render-to-sx": "renderToSx",
|
|
"render-value-to-html": "renderValueToHtml",
|
|
"render-list-to-html": "renderListToHtml",
|
|
"render-html-element": "renderHtmlElement",
|
|
"render-html-component": "renderHtmlComponent",
|
|
"parse-element-args": "parseElementArgs",
|
|
"render-attrs": "renderAttrs",
|
|
"aser-list": "aserList",
|
|
"aser-fragment": "aserFragment",
|
|
"aser-call": "aserCall",
|
|
"aser-special": "aserSpecial",
|
|
"sf-if": "sfIf",
|
|
"sf-when": "sfWhen",
|
|
"sf-cond": "sfCond",
|
|
"sf-cond-scheme": "sfCondScheme",
|
|
"sf-cond-clojure": "sfCondClojure",
|
|
"sf-case": "sfCase",
|
|
"sf-case-loop": "sfCaseLoop",
|
|
"sf-and": "sfAnd",
|
|
"sf-or": "sfOr",
|
|
"sf-let": "sfLet",
|
|
"sf-named-let": "sfNamedLet",
|
|
"sf-letrec": "sfLetrec",
|
|
"sf-dynamic-wind": "sfDynamicWind",
|
|
"push-wind!": "pushWind",
|
|
"pop-wind!": "popWind",
|
|
"call-thunk": "callThunk",
|
|
"sf-lambda": "sfLambda",
|
|
"sf-define": "sfDefine",
|
|
"sf-defcomp": "sfDefcomp",
|
|
"sf-defmacro": "sfDefmacro",
|
|
"sf-begin": "sfBegin",
|
|
"sf-quote": "sfQuote",
|
|
"sf-quasiquote": "sfQuasiquote",
|
|
"sf-thread-first": "sfThreadFirst",
|
|
"sf-set!": "sfSetBang",
|
|
"qq-expand": "qqExpand",
|
|
"ho-map": "hoMap",
|
|
"ho-map-indexed": "hoMapIndexed",
|
|
"ho-filter": "hoFilter",
|
|
"ho-reduce": "hoReduce",
|
|
"ho-some": "hoSome",
|
|
"ho-every": "hoEvery",
|
|
"ho-for-each": "hoForEach",
|
|
"sf-defstyle": "sfDefstyle",
|
|
"sf-defkeyframes": "sfDefkeyframes",
|
|
"build-keyframes": "buildKeyframes",
|
|
"style-value?": "isStyleValue",
|
|
"style-value-class": "styleValueClass",
|
|
"kf-name": "kfName",
|
|
"special-form?": "isSpecialForm",
|
|
"ho-form?": "isHoForm",
|
|
"strip-prefix": "stripPrefix",
|
|
"escape-html": "escapeHtml",
|
|
"escape-attr": "escapeAttr",
|
|
"escape-string": "escapeString",
|
|
"raw-html-content": "rawHtmlContent",
|
|
"HTML_TAGS": "HTML_TAGS",
|
|
"VOID_ELEMENTS": "VOID_ELEMENTS",
|
|
"BOOLEAN_ATTRS": "BOOLEAN_ATTRS",
|
|
# render.sx core
|
|
"definition-form?": "isDefinitionForm",
|
|
# adapter-html.sx
|
|
"RENDER_HTML_FORMS": "RENDER_HTML_FORMS",
|
|
"render-html-form?": "isRenderHtmlForm",
|
|
"dispatch-html-form": "dispatchHtmlForm",
|
|
"render-lambda-html": "renderLambdaHtml",
|
|
"make-raw-html": "makeRawHtml",
|
|
# adapter-dom.sx
|
|
"SVG_NS": "SVG_NS",
|
|
"MATH_NS": "MATH_NS",
|
|
"render-to-dom": "renderToDom",
|
|
"render-dom-list": "renderDomList",
|
|
"render-dom-element": "renderDomElement",
|
|
"render-dom-component": "renderDomComponent",
|
|
"render-dom-fragment": "renderDomFragment",
|
|
"render-dom-raw": "renderDomRaw",
|
|
"render-dom-unknown-component": "renderDomUnknownComponent",
|
|
"RENDER_DOM_FORMS": "RENDER_DOM_FORMS",
|
|
"render-dom-form?": "isRenderDomForm",
|
|
"dispatch-render-form": "dispatchRenderForm",
|
|
"render-lambda-dom": "renderLambdaDom",
|
|
"dom-create-element": "domCreateElement",
|
|
"dom-append": "domAppend",
|
|
"dom-set-attr": "domSetAttr",
|
|
"dom-get-attr": "domGetAttr",
|
|
"dom-remove-attr": "domRemoveAttr",
|
|
"dom-has-attr?": "domHasAttr",
|
|
"dom-parse-html": "domParseHtml",
|
|
"dom-clone": "domClone",
|
|
"create-text-node": "createTextNode",
|
|
"create-fragment": "createFragment",
|
|
"dom-parent": "domParent",
|
|
"dom-id": "domId",
|
|
"dom-node-type": "domNodeType",
|
|
"dom-node-name": "domNodeName",
|
|
"dom-text-content": "domTextContent",
|
|
"dom-set-text-content": "domSetTextContent",
|
|
"dom-is-fragment?": "domIsFragment",
|
|
"dom-is-child-of?": "domIsChildOf",
|
|
"dom-is-active-element?": "domIsActiveElement",
|
|
"dom-is-input-element?": "domIsInputElement",
|
|
"dom-first-child": "domFirstChild",
|
|
"dom-next-sibling": "domNextSibling",
|
|
"dom-child-list": "domChildList",
|
|
"dom-attr-list": "domAttrList",
|
|
"dom-insert-before": "domInsertBefore",
|
|
"dom-insert-after": "domInsertAfter",
|
|
"dom-prepend": "domPrepend",
|
|
"dom-remove-child": "domRemoveChild",
|
|
"dom-replace-child": "domReplaceChild",
|
|
"dom-set-inner-html": "domSetInnerHtml",
|
|
"dom-insert-adjacent-html": "domInsertAdjacentHtml",
|
|
"dom-get-style": "domGetStyle",
|
|
"dom-set-style": "domSetStyle",
|
|
"dom-get-prop": "domGetProp",
|
|
"dom-set-prop": "domSetProp",
|
|
"dom-add-class": "domAddClass",
|
|
"dom-remove-class": "domRemoveClass",
|
|
"dom-dispatch": "domDispatch",
|
|
"dom-query": "domQuery",
|
|
"dom-query-all": "domQueryAll",
|
|
"dom-tag-name": "domTagName",
|
|
"dict-has?": "dictHas",
|
|
"dict-delete!": "dictDelete",
|
|
"process-bindings": "processBindings",
|
|
"eval-cond": "evalCond",
|
|
"eval-cond-scheme": "evalCondScheme",
|
|
"eval-cond-clojure": "evalCondClojure",
|
|
"for-each-indexed": "forEachIndexed",
|
|
"index-of": "indexOf_",
|
|
"component-has-children?": "componentHasChildren",
|
|
# engine.sx
|
|
"ENGINE_VERBS": "ENGINE_VERBS",
|
|
"DEFAULT_SWAP": "DEFAULT_SWAP",
|
|
"parse-time": "parseTime",
|
|
"parse-trigger-spec": "parseTriggerSpec",
|
|
"default-trigger": "defaultTrigger",
|
|
"get-verb-info": "getVerbInfo",
|
|
"build-request-headers": "buildRequestHeaders",
|
|
"process-response-headers": "processResponseHeaders",
|
|
"parse-swap-spec": "parseSwapSpec",
|
|
"parse-retry-spec": "parseRetrySpec",
|
|
"next-retry-ms": "nextRetryMs",
|
|
"filter-params": "filterParams",
|
|
"resolve-target": "resolveTarget",
|
|
"apply-optimistic": "applyOptimistic",
|
|
"revert-optimistic": "revertOptimistic",
|
|
"find-oob-swaps": "findOobSwaps",
|
|
"morph-node": "morphNode",
|
|
"sync-attrs": "syncAttrs",
|
|
"morph-children": "morphChildren",
|
|
"swap-dom-nodes": "swapDomNodes",
|
|
"insert-remaining-siblings": "insertRemainingSiblings",
|
|
"swap-html-string": "swapHtmlString",
|
|
"handle-history": "handleHistory",
|
|
"PRELOAD_TTL": "PRELOAD_TTL",
|
|
"preload-cache-get": "preloadCacheGet",
|
|
"preload-cache-set": "preloadCacheSet",
|
|
"classify-trigger": "classifyTrigger",
|
|
"should-boost-link?": "shouldBoostLink",
|
|
"should-boost-form?": "shouldBoostForm",
|
|
"parse-sse-swap": "parseSseSwap",
|
|
# engine.sx orchestration
|
|
"_preload-cache": "_preloadCache",
|
|
"_css-hash": "_cssHash",
|
|
"dispatch-trigger-events": "dispatchTriggerEvents",
|
|
"init-css-tracking": "initCssTracking",
|
|
"execute-request": "executeRequest",
|
|
"do-fetch": "doFetch",
|
|
"handle-fetch-success": "handleFetchSuccess",
|
|
"handle-sx-response": "handleSxResponse",
|
|
"handle-html-response": "handleHtmlResponse",
|
|
"handle-retry": "handleRetry",
|
|
"bind-triggers": "bindTriggers",
|
|
"bind-event": "bindEvent",
|
|
"post-swap": "postSwap",
|
|
"activate-scripts": "activateScripts",
|
|
"process-oob-swaps": "processOobSwaps",
|
|
"hoist-head-elements": "hoistHeadElements",
|
|
"process-boosted": "processBoosted",
|
|
"boost-descendants": "boostDescendants",
|
|
"process-sse": "processSse",
|
|
"bind-sse": "bindSse",
|
|
"bind-sse-swap": "bindSseSwap",
|
|
"bind-inline-handlers": "bindInlineHandlers",
|
|
"bind-preload-for": "bindPreloadFor",
|
|
"do-preload": "doPreload",
|
|
"VERB_SELECTOR": "VERB_SELECTOR",
|
|
"process-elements": "processElements",
|
|
"process-one": "processOne",
|
|
"handle-popstate": "handlePopstate",
|
|
"engine-init": "engineInit",
|
|
# engine orchestration platform
|
|
"promise-resolve": "promiseResolve",
|
|
"promise-catch": "promiseCatch",
|
|
"abort-previous": "abortPrevious",
|
|
"track-controller": "trackController",
|
|
"new-abort-controller": "newAbortController",
|
|
"controller-signal": "controllerSignal",
|
|
"abort-error?": "isAbortError",
|
|
"set-timeout": "setTimeout_",
|
|
"set-interval": "setInterval_",
|
|
"clear-timeout": "clearTimeout_",
|
|
"request-animation-frame": "requestAnimationFrame_",
|
|
"csrf-token": "csrfToken",
|
|
"cross-origin?": "isCrossOrigin",
|
|
"loaded-component-names": "loadedComponentNames",
|
|
"build-request-body": "buildRequestBody",
|
|
"show-indicator": "showIndicator",
|
|
"disable-elements": "disableElements",
|
|
"clear-loading-state": "clearLoadingState",
|
|
"fetch-request": "fetchRequest",
|
|
"fetch-location": "fetchLocation",
|
|
"fetch-and-restore": "fetchAndRestore",
|
|
"fetch-preload": "fetchPreload",
|
|
"dom-query-by-id": "domQueryById",
|
|
"dom-matches?": "domMatches",
|
|
"dom-closest": "domClosest",
|
|
"dom-body": "domBody",
|
|
"dom-has-class?": "domHasClass",
|
|
"dom-append-to-head": "domAppendToHead",
|
|
"dom-parse-html-document": "domParseHtmlDocument",
|
|
"dom-outer-html": "domOuterHtml",
|
|
"dom-body-inner-html": "domBodyInnerHtml",
|
|
"prevent-default": "preventDefault_",
|
|
"element-value": "elementValue",
|
|
"validate-for-request": "validateForRequest",
|
|
"with-transition": "withTransition",
|
|
"observe-intersection": "observeIntersection",
|
|
"event-source-connect": "eventSourceConnect",
|
|
"event-source-listen": "eventSourceListen",
|
|
"bind-boost-link": "bindBoostLink",
|
|
"bind-boost-form": "bindBoostForm",
|
|
"bind-client-route-link": "bindClientRouteLink",
|
|
"bind-client-route-click": "bindClientRouteClick",
|
|
"try-client-route": "tryClientRoute",
|
|
"try-eval-content": "tryEvalContent",
|
|
"url-pathname": "urlPathname",
|
|
"bind-inline-handler": "bindInlineHandler",
|
|
"bind-preload": "bindPreload",
|
|
"mark-processed!": "markProcessed",
|
|
"is-processed?": "isProcessed",
|
|
"create-script-clone": "createScriptClone",
|
|
"sx-render": "sxRender",
|
|
"sx-process-scripts": "sxProcessScripts",
|
|
"sx-hydrate": "sxHydrate",
|
|
"strip-component-scripts": "stripComponentScripts",
|
|
"extract-response-css": "extractResponseCss",
|
|
"select-from-container": "selectFromContainer",
|
|
"children-to-fragment": "childrenToFragment",
|
|
"select-html-from-doc": "selectHtmlFromDoc",
|
|
"try-parse-json": "tryParseJson",
|
|
"process-css-response": "processCssResponse",
|
|
"browser-location-href": "browserLocationHref",
|
|
"browser-same-origin?": "browserSameOrigin",
|
|
"browser-push-state": "browserPushState",
|
|
"browser-replace-state": "browserReplaceState",
|
|
"browser-navigate": "browserNavigate",
|
|
"browser-reload": "browserReload",
|
|
"browser-scroll-to": "browserScrollTo",
|
|
"browser-media-matches?": "browserMediaMatches",
|
|
"browser-confirm": "browserConfirm",
|
|
"browser-prompt": "browserPrompt",
|
|
"now-ms": "nowMs",
|
|
"parse-header-value": "parseHeaderValue",
|
|
"replace": "replace_",
|
|
"whitespace?": "isWhitespace",
|
|
"digit?": "isDigit",
|
|
"ident-start?": "isIdentStart",
|
|
"ident-char?": "isIdentChar",
|
|
"parse-number": "parseNumber",
|
|
"sx-expr-source": "sxExprSource",
|
|
"starts-with?": "startsWith",
|
|
"ends-with?": "endsWith",
|
|
"contains?": "contains",
|
|
"empty?": "isEmpty",
|
|
"odd?": "isOdd",
|
|
"even?": "isEven",
|
|
"zero?": "isZero",
|
|
"number?": "isNumber",
|
|
"string?": "isString",
|
|
"list?": "isList",
|
|
"dict?": "isDict",
|
|
"every?": "isEvery",
|
|
"map-indexed": "mapIndexed",
|
|
"for-each": "forEach",
|
|
"map-dict": "mapDict",
|
|
"chunk-every": "chunkEvery",
|
|
"zip-pairs": "zipPairs",
|
|
"strip-tags": "stripTags",
|
|
"format-date": "formatDate",
|
|
"format-decimal": "formatDecimal",
|
|
"parse-int": "parseInt_",
|
|
# cssx.sx
|
|
"_style-atoms": "_styleAtoms",
|
|
"_pseudo-variants": "_pseudoVariants",
|
|
"_responsive-breakpoints": "_responsiveBreakpoints",
|
|
"_style-keyframes": "_styleKeyframes",
|
|
"_arbitrary-patterns": "_arbitraryPatterns",
|
|
"_child-selector-prefixes": "_childSelectorPrefixes",
|
|
"_style-cache": "_styleCache",
|
|
"_injected-styles": "_injectedStyles",
|
|
"load-style-dict": "loadStyleDict",
|
|
"split-variant": "splitVariant",
|
|
"resolve-atom": "resolveAtom",
|
|
"is-child-selector-atom?": "isChildSelectorAtom",
|
|
"hash-style": "hashStyle",
|
|
"resolve-style": "resolveStyle",
|
|
"merge-style-values": "mergeStyleValues",
|
|
"fnv1a-hash": "fnv1aHash",
|
|
"compile-regex": "compileRegex",
|
|
"regex-match": "regexMatch",
|
|
"regex-replace-groups": "regexReplaceGroups",
|
|
"make-style-value": "makeStyleValue_",
|
|
"style-value-declarations": "styleValueDeclarations",
|
|
"style-value-media-rules": "styleValueMediaRules",
|
|
"style-value-pseudo-rules": "styleValuePseudoRules",
|
|
"style-value-keyframes": "styleValueKeyframes_",
|
|
"inject-style-value": "injectStyleValue",
|
|
# boot.sx
|
|
"HEAD_HOIST_SELECTOR": "HEAD_HOIST_SELECTOR",
|
|
"hoist-head-elements-full": "hoistHeadElementsFull",
|
|
"sx-mount": "sxMount",
|
|
"sx-hydrate-elements": "sxHydrateElements",
|
|
"sx-update-element": "sxUpdateElement",
|
|
"sx-render-component": "sxRenderComponent",
|
|
"process-sx-scripts": "processSxScripts",
|
|
"process-component-script": "processComponentScript",
|
|
"init-style-dict": "initStyleDict",
|
|
"SX_VERSION": "SX_VERSION",
|
|
"boot-init": "bootInit",
|
|
"resolve-mount-target": "resolveMountTarget",
|
|
"sx-render-with-env": "sxRenderWithEnv",
|
|
"get-render-env": "getRenderEnv",
|
|
"merge-envs": "mergeEnvs",
|
|
"sx-load-components": "sxLoadComponents",
|
|
"set-document-title": "setDocumentTitle",
|
|
"remove-head-element": "removeHeadElement",
|
|
"query-sx-scripts": "querySxScripts",
|
|
"query-style-scripts": "queryStyleScripts",
|
|
"local-storage-get": "localStorageGet",
|
|
"local-storage-set": "localStorageSet",
|
|
"local-storage-remove": "localStorageRemove",
|
|
"set-sx-comp-cookie": "setSxCompCookie",
|
|
"clear-sx-comp-cookie": "clearSxCompCookie",
|
|
"set-sx-styles-cookie": "setSxStylesCookie",
|
|
"clear-sx-styles-cookie": "clearSxStylesCookie",
|
|
"parse-env-attr": "parseEnvAttr",
|
|
"store-env-attr": "storeEnvAttr",
|
|
"to-kebab": "toKebab",
|
|
"log-info": "logInfo",
|
|
"log-warn": "logWarn",
|
|
"log-parse-error": "logParseError",
|
|
"parse-and-load-style-dict": "parseAndLoadStyleDict",
|
|
"_page-routes": "_pageRoutes",
|
|
"process-page-scripts": "processPageScripts",
|
|
"query-page-scripts": "queryPageScripts",
|
|
# deps.sx
|
|
"scan-refs": "scanRefs",
|
|
"scan-refs-walk": "scanRefsWalk",
|
|
"transitive-deps": "transitiveDeps",
|
|
"compute-all-deps": "computeAllDeps",
|
|
"scan-components-from-source": "scanComponentsFromSource",
|
|
"components-needed": "componentsNeeded",
|
|
"page-component-bundle": "pageComponentBundle",
|
|
"page-css-classes": "pageCssClasses",
|
|
"component-deps": "componentDeps",
|
|
"component-set-deps!": "componentSetDeps",
|
|
"component-css-classes": "componentCssClasses",
|
|
"component-io-refs": "componentIoRefs",
|
|
"component-set-io-refs!": "componentSetIoRefs",
|
|
"env-components": "envComponents",
|
|
"regex-find-all": "regexFindAll",
|
|
"scan-css-classes": "scanCssClasses",
|
|
# deps.sx IO detection
|
|
"scan-io-refs": "scanIoRefs",
|
|
"scan-io-refs-walk": "scanIoRefsWalk",
|
|
"transitive-io-refs": "transitiveIoRefs",
|
|
"compute-all-io-refs": "computeAllIoRefs",
|
|
"component-pure?": "componentPure_p",
|
|
# router.sx
|
|
"split-path-segments": "splitPathSegments",
|
|
"make-route-segment": "makeRouteSegment",
|
|
"parse-route-pattern": "parseRoutePattern",
|
|
"match-route-segments": "matchRouteSegments",
|
|
"match-route": "matchRoute",
|
|
"find-matching-route": "findMatchingRoute",
|
|
"for-each-indexed": "forEachIndexed",
|
|
}
|
|
if name in RENAMES:
|
|
return RENAMES[name]
|
|
# General mangling: replace - with camelCase, ? with _p, ! with _b
|
|
result = name
|
|
if result.endswith("?"):
|
|
result = result[:-1] + "_p"
|
|
if result.endswith("!"):
|
|
result = result[:-1] + "_b"
|
|
# Kebab to camel
|
|
parts = result.split("-")
|
|
if len(parts) > 1:
|
|
result = parts[0] + "".join(p.capitalize() for p in parts[1:])
|
|
return result
|
|
|
|
# --- List emission ---
|
|
|
|
def _emit_list(self, expr: list) -> str:
|
|
if not expr:
|
|
return "[]"
|
|
head = expr[0]
|
|
if not isinstance(head, Symbol):
|
|
# Data list
|
|
return "[" + ", ".join(self.emit(x) for x in expr) + "]"
|
|
name = head.name
|
|
handler = getattr(self, f"_sf_{name.replace('-', '_').replace('!', '_b').replace('?', '_p')}", None)
|
|
if handler:
|
|
return handler(expr)
|
|
# Built-in forms
|
|
if name == "fn" or name == "lambda":
|
|
return self._emit_fn(expr)
|
|
if name == "let" or name == "let*":
|
|
return self._emit_let(expr)
|
|
if name == "if":
|
|
return self._emit_if(expr)
|
|
if name == "when":
|
|
return self._emit_when(expr)
|
|
if name == "cond":
|
|
return self._emit_cond(expr)
|
|
if name == "case":
|
|
return self._emit_case(expr)
|
|
if name == "and":
|
|
return self._emit_and(expr)
|
|
if name == "or":
|
|
return self._emit_or(expr)
|
|
if name == "not":
|
|
return f"!isSxTruthy({self.emit(expr[1])})"
|
|
if name == "do" or name == "begin":
|
|
return self._emit_do(expr)
|
|
if name == "list":
|
|
return "[" + ", ".join(self.emit(x) for x in expr[1:]) + "]"
|
|
if name == "dict":
|
|
return self._emit_dict_literal(expr)
|
|
if name == "quote":
|
|
return self._emit_quote(expr[1])
|
|
if name == "set!":
|
|
return f"({self._mangle(expr[1].name)} = {self.emit(expr[2])})"
|
|
if name == "str":
|
|
parts = [self.emit(x) for x in expr[1:]]
|
|
return "(" + " + ".join(f'String({p})' for p in parts) + ")"
|
|
# Infix operators
|
|
if name in ("+", "-", "*", "/", "=", "!=", "<", ">", "<=", ">=", "mod"):
|
|
return self._emit_infix(name, expr[1:])
|
|
if name == "inc":
|
|
return f"({self.emit(expr[1])} + 1)"
|
|
if name == "dec":
|
|
return f"({self.emit(expr[1])} - 1)"
|
|
|
|
# Regular function call
|
|
fn_name = self._mangle(name)
|
|
args = ", ".join(self.emit(x) for x in expr[1:])
|
|
return f"{fn_name}({args})"
|
|
|
|
# --- Special form emitters ---
|
|
|
|
def _emit_fn(self, expr) -> str:
|
|
params = expr[1]
|
|
body = expr[2:]
|
|
param_names = []
|
|
for p in params:
|
|
if isinstance(p, Symbol):
|
|
param_names.append(self._mangle(p.name))
|
|
else:
|
|
param_names.append(str(p))
|
|
params_str = ", ".join(param_names)
|
|
if len(body) == 1:
|
|
body_js = self.emit(body[0])
|
|
return f"function({params_str}) {{ return {body_js}; }}"
|
|
# Multi-expression body: statements then return last
|
|
parts = []
|
|
for b in body[:-1]:
|
|
parts.append(self.emit_statement(b))
|
|
parts.append(f"return {self.emit(body[-1])};")
|
|
inner = "\n".join(parts)
|
|
return f"function({params_str}) {{ {inner} }}"
|
|
|
|
def _emit_let(self, expr) -> str:
|
|
bindings = expr[1]
|
|
body = expr[2:]
|
|
parts = ["(function() {"]
|
|
if isinstance(bindings, list):
|
|
if bindings and isinstance(bindings[0], list):
|
|
# Scheme-style: ((name val) ...)
|
|
for b in bindings:
|
|
vname = b[0].name if isinstance(b[0], Symbol) else str(b[0])
|
|
parts.append(f" var {self._mangle(vname)} = {self.emit(b[1])};")
|
|
else:
|
|
# Clojure-style: (name val name val ...)
|
|
for i in range(0, len(bindings), 2):
|
|
vname = bindings[i].name if isinstance(bindings[i], Symbol) else str(bindings[i])
|
|
parts.append(f" var {self._mangle(vname)} = {self.emit(bindings[i + 1])};")
|
|
for b_expr in body[:-1]:
|
|
parts.append(f" {self.emit_statement(b_expr)}")
|
|
parts.append(f" return {self.emit(body[-1])};")
|
|
parts.append("})()")
|
|
return "\n".join(parts)
|
|
|
|
def _emit_if(self, expr) -> str:
|
|
cond = self.emit(expr[1])
|
|
then = self.emit(expr[2])
|
|
els = self.emit(expr[3]) if len(expr) > 3 else "NIL"
|
|
return f"(isSxTruthy({cond}) ? {then} : {els})"
|
|
|
|
def _emit_when(self, expr) -> str:
|
|
cond = self.emit(expr[1])
|
|
body_parts = expr[2:]
|
|
if len(body_parts) == 1:
|
|
return f"(isSxTruthy({cond}) ? {self.emit(body_parts[0])} : NIL)"
|
|
body = self._emit_do_inner(body_parts)
|
|
return f"(isSxTruthy({cond}) ? {body} : NIL)"
|
|
|
|
def _emit_when_stmt(self, expr) -> str:
|
|
cond = self.emit(expr[1])
|
|
body_parts = expr[2:]
|
|
stmts = "\n".join(f" {self.emit_statement(e)}" for e in body_parts)
|
|
return f"if (isSxTruthy({cond})) {{\n{stmts}\n}}"
|
|
|
|
def _emit_cond(self, expr) -> str:
|
|
clauses = expr[1:]
|
|
if not clauses:
|
|
return "NIL"
|
|
# Determine style ONCE: Scheme-style if every element is a 2-element
|
|
# list AND no bare keywords appear (bare :else = Clojure).
|
|
is_scheme = (
|
|
all(isinstance(c, list) and len(c) == 2 for c in clauses)
|
|
and not any(isinstance(c, Keyword) for c in clauses)
|
|
)
|
|
if is_scheme:
|
|
return self._cond_scheme(clauses)
|
|
return self._cond_clojure(clauses)
|
|
|
|
def _cond_scheme(self, clauses) -> str:
|
|
if not clauses:
|
|
return "NIL"
|
|
clause = clauses[0]
|
|
test = clause[0]
|
|
body = clause[1]
|
|
if isinstance(test, Symbol) and test.name in ("else", ":else"):
|
|
return self.emit(body)
|
|
if isinstance(test, Keyword) and test.name == "else":
|
|
return self.emit(body)
|
|
return f"(isSxTruthy({self.emit(test)}) ? {self.emit(body)} : {self._cond_scheme(clauses[1:])})"
|
|
|
|
def _cond_clojure(self, clauses) -> str:
|
|
if len(clauses) < 2:
|
|
return "NIL"
|
|
test = clauses[0]
|
|
body = clauses[1]
|
|
if isinstance(test, Keyword) and test.name == "else":
|
|
return self.emit(body)
|
|
if isinstance(test, Symbol) and test.name in ("else", ":else"):
|
|
return self.emit(body)
|
|
return f"(isSxTruthy({self.emit(test)}) ? {self.emit(body)} : {self._cond_clojure(clauses[2:])})"
|
|
|
|
def _emit_case(self, expr) -> str:
|
|
match_expr = self.emit(expr[1])
|
|
clauses = expr[2:]
|
|
return f"(function() {{ var _m = {match_expr}; {self._case_chain(clauses)} }})()"
|
|
|
|
def _case_chain(self, clauses) -> str:
|
|
if len(clauses) < 2:
|
|
return "return NIL;"
|
|
test = clauses[0]
|
|
body = clauses[1]
|
|
if isinstance(test, Keyword) and test.name == "else":
|
|
return f"return {self.emit(body)};"
|
|
if isinstance(test, Symbol) and test.name in ("else", ":else"):
|
|
return f"return {self.emit(body)};"
|
|
return f"if (_m == {self.emit(test)}) return {self.emit(body)}; {self._case_chain(clauses[2:])}"
|
|
|
|
def _emit_and(self, expr) -> str:
|
|
parts = [self.emit(x) for x in expr[1:]]
|
|
return "(" + " && ".join(f"isSxTruthy({p})" for p in parts[:-1]) + (" && " if len(parts) > 1 else "") + parts[-1] + ")"
|
|
|
|
def _emit_or(self, expr) -> str:
|
|
if len(expr) == 2:
|
|
return self.emit(expr[1])
|
|
parts = [self.emit(x) for x in expr[1:]]
|
|
# Use a helper that returns the first truthy value
|
|
return f"sxOr({', '.join(parts)})"
|
|
|
|
def _emit_do(self, expr) -> str:
|
|
return self._emit_do_inner(expr[1:])
|
|
|
|
def _emit_do_inner(self, exprs) -> str:
|
|
if len(exprs) == 1:
|
|
return self.emit(exprs[0])
|
|
parts = [self.emit(e) for e in exprs]
|
|
return "(" + ", ".join(parts) + ")"
|
|
|
|
def _emit_native_dict(self, expr: dict) -> str:
|
|
"""Emit a native Python dict (from parser's {:key val} syntax)."""
|
|
parts = []
|
|
for key, val in expr.items():
|
|
parts.append(f"{self._js_string(key)}: {self.emit(val)}")
|
|
return "{" + ", ".join(parts) + "}"
|
|
|
|
def _emit_dict_literal(self, expr) -> str:
|
|
pairs = expr[1:]
|
|
parts = []
|
|
i = 0
|
|
while i < len(pairs) - 1:
|
|
key = pairs[i]
|
|
val = pairs[i + 1]
|
|
if isinstance(key, Keyword):
|
|
parts.append(f"{self._js_string(key.name)}: {self.emit(val)}")
|
|
else:
|
|
parts.append(f"[{self.emit(key)}]: {self.emit(val)}")
|
|
i += 2
|
|
return "{" + ", ".join(parts) + "}"
|
|
|
|
def _emit_infix(self, op: str, args: list) -> str:
|
|
JS_OPS = {"=": "==", "!=": "!=", "mod": "%"}
|
|
js_op = JS_OPS.get(op, op)
|
|
if len(args) == 1 and op == "-":
|
|
return f"(-{self.emit(args[0])})"
|
|
return f"({self.emit(args[0])} {js_op} {self.emit(args[1])})"
|
|
|
|
def _emit_define(self, expr) -> str:
|
|
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
|
|
# Detect zero-arg self-tail-recursive functions and emit as while loops
|
|
fn_expr = expr[2] if len(expr) > 2 else None
|
|
if (fn_expr and isinstance(fn_expr, list) and fn_expr
|
|
and isinstance(fn_expr[0], Symbol) and fn_expr[0].name in ("fn", "lambda")
|
|
and isinstance(fn_expr[1], list) and len(fn_expr[1]) == 0
|
|
and self._is_self_tail_recursive(name, fn_expr[2:])):
|
|
body = fn_expr[2:]
|
|
loop_body = self._emit_loop_body(name, body)
|
|
return f"var {self._mangle(name)} = function() {{ while(true) {{ {loop_body} }} }};"
|
|
val = self.emit(fn_expr) if fn_expr else "NIL"
|
|
return f"var {self._mangle(name)} = {val};"
|
|
|
|
def _is_self_tail_recursive(self, name: str, body: list) -> bool:
|
|
"""Check if a function body contains tail calls to itself."""
|
|
if not body:
|
|
return False
|
|
last = body[-1]
|
|
return self._has_tail_call(name, last)
|
|
|
|
def _has_tail_call(self, name: str, expr) -> bool:
|
|
"""Check if expr has a tail call to name in any branch."""
|
|
if not isinstance(expr, list) or not expr:
|
|
return False
|
|
head = expr[0]
|
|
if not isinstance(head, Symbol):
|
|
return False
|
|
h = head.name
|
|
# Direct tail call
|
|
if h == name:
|
|
return True
|
|
# Branching forms — check if any branch tail-calls
|
|
if h == "if":
|
|
return (self._has_tail_call(name, expr[2])
|
|
or (len(expr) > 3 and self._has_tail_call(name, expr[3])))
|
|
if h == "when":
|
|
return any(self._has_tail_call(name, e) for e in expr[2:])
|
|
if h == "cond":
|
|
for clause in expr[1:]:
|
|
if isinstance(clause, list) and len(clause) == 2:
|
|
if self._has_tail_call(name, clause[1]):
|
|
return True
|
|
elif isinstance(clause, Keyword):
|
|
continue
|
|
elif isinstance(clause, list):
|
|
if self._has_tail_call(name, clause):
|
|
return True
|
|
else:
|
|
if self._has_tail_call(name, clause):
|
|
return True
|
|
return False
|
|
if h in ("do", "begin"):
|
|
return self._has_tail_call(name, expr[-1]) if len(expr) > 1 else False
|
|
if h == "let" or h == "let*":
|
|
return self._has_tail_call(name, expr[-1]) if len(expr) > 2 else False
|
|
return False
|
|
|
|
def _emit_loop_body(self, name: str, body: list) -> str:
|
|
"""Emit a function body as while-loop statements.
|
|
|
|
Replaces tail-self-calls with `continue` and non-recursive exits with
|
|
`return`.
|
|
"""
|
|
if not body:
|
|
return "return NIL;"
|
|
# Emit side-effect statements first, then the tail expression as loop logic
|
|
parts = []
|
|
for b in body[:-1]:
|
|
parts.append(self.emit_statement(b))
|
|
parts.append(self._emit_tail_as_stmt(name, body[-1]))
|
|
return "\n".join(parts)
|
|
|
|
def _emit_tail_as_stmt(self, name: str, expr) -> str:
|
|
"""Emit an expression in tail position as loop statements.
|
|
|
|
Tail-self-calls → continue; other exits → return expr;
|
|
"""
|
|
if not isinstance(expr, list) or not expr:
|
|
return f"return {self.emit(expr)};"
|
|
|
|
head = expr[0]
|
|
if not isinstance(head, Symbol):
|
|
return f"return {self.emit(expr)};"
|
|
|
|
h = head.name
|
|
|
|
# Direct tail call to self → continue
|
|
if h == name:
|
|
return "continue;"
|
|
|
|
# (do stmt1 stmt2 ... tail) → emit stmts then recurse on tail
|
|
if h in ("do", "begin"):
|
|
stmts = []
|
|
for e in expr[1:-1]:
|
|
stmts.append(self.emit_statement(e))
|
|
stmts.append(self._emit_tail_as_stmt(name, expr[-1]))
|
|
return "\n".join(stmts)
|
|
|
|
# (if cond then else) → if/else with tail handling in each branch
|
|
if h == "if":
|
|
cond = self.emit(expr[1])
|
|
then_branch = self._emit_tail_as_stmt(name, expr[2])
|
|
else_branch = self._emit_tail_as_stmt(name, expr[3]) if len(expr) > 3 else "return NIL;"
|
|
return f"if (isSxTruthy({cond})) {{ {then_branch} }} else {{ {else_branch} }}"
|
|
|
|
# (when cond body...) → if (cond) { body... } else { return NIL; }
|
|
if h == "when":
|
|
cond = self.emit(expr[1])
|
|
body_parts = expr[2:]
|
|
if not body_parts:
|
|
return f"if (isSxTruthy({cond})) {{}} else {{ return NIL; }}"
|
|
stmts = []
|
|
for e in body_parts[:-1]:
|
|
stmts.append(self.emit_statement(e))
|
|
stmts.append(self._emit_tail_as_stmt(name, body_parts[-1]))
|
|
inner = "\n".join(stmts)
|
|
return f"if (isSxTruthy({cond})) {{ {inner} }} else {{ return NIL; }}"
|
|
|
|
# (cond clause1 clause2 ...) → if/else if/else chain
|
|
if h == "cond":
|
|
return self._emit_cond_as_loop_stmt(name, expr[1:])
|
|
|
|
# (let ((bindings)) body...) → { var ...; tail }
|
|
if h in ("let", "let*"):
|
|
bindings = expr[1]
|
|
body = expr[2:]
|
|
parts = []
|
|
if isinstance(bindings, list):
|
|
if bindings and isinstance(bindings[0], list):
|
|
for b in bindings:
|
|
vname = b[0].name if isinstance(b[0], Symbol) else str(b[0])
|
|
parts.append(f"var {self._mangle(vname)} = {self.emit(b[1])};")
|
|
else:
|
|
for i in range(0, len(bindings), 2):
|
|
vname = bindings[i].name if isinstance(bindings[i], Symbol) else str(bindings[i])
|
|
parts.append(f"var {self._mangle(vname)} = {self.emit(bindings[i + 1])};")
|
|
for b_expr in body[:-1]:
|
|
parts.append(self.emit_statement(b_expr))
|
|
parts.append(self._emit_tail_as_stmt(name, body[-1]))
|
|
inner = "\n".join(parts)
|
|
return f"{{ {inner} }}"
|
|
|
|
# Not a tail call to self — regular return
|
|
return f"return {self.emit(expr)};"
|
|
|
|
def _emit_cond_as_loop_stmt(self, name: str, clauses) -> str:
|
|
"""Emit cond clauses as if/else if/else for loop body."""
|
|
if not clauses:
|
|
return "return NIL;"
|
|
|
|
# Detect style: Scheme vs Clojure (same as _emit_cond)
|
|
is_scheme = (
|
|
all(isinstance(c, list) and len(c) == 2 for c in clauses)
|
|
and not any(isinstance(c, Keyword) for c in clauses)
|
|
)
|
|
if is_scheme:
|
|
return self._cond_scheme_loop(name, clauses)
|
|
return self._cond_clojure_loop(name, clauses)
|
|
|
|
def _cond_scheme_loop(self, name: str, clauses) -> str:
|
|
parts = []
|
|
for i, clause in enumerate(clauses):
|
|
cond_expr = clause[0]
|
|
body_expr = clause[1]
|
|
# Check for :else / else
|
|
is_else = (isinstance(cond_expr, Keyword) and cond_expr.name == "else") or \
|
|
(isinstance(cond_expr, Symbol) and cond_expr.name == "else") or \
|
|
(isinstance(cond_expr, bool) and cond_expr is True)
|
|
if is_else:
|
|
parts.append(f"{{ {self._emit_tail_as_stmt(name, body_expr)} }}")
|
|
break
|
|
prefix = "if" if i == 0 else "else if"
|
|
cond = self.emit(cond_expr)
|
|
body = self._emit_tail_as_stmt(name, body_expr)
|
|
parts.append(f"{prefix} (isSxTruthy({cond})) {{ {body} }}")
|
|
else:
|
|
parts.append("else { return NIL; }")
|
|
return " ".join(parts)
|
|
|
|
def _cond_clojure_loop(self, name: str, clauses) -> str:
|
|
parts = []
|
|
i = 0
|
|
clause_idx = 0
|
|
has_else = False
|
|
while i < len(clauses):
|
|
c = clauses[i]
|
|
if isinstance(c, Keyword) and c.name == "else":
|
|
if i + 1 < len(clauses):
|
|
parts.append(f"else {{ {self._emit_tail_as_stmt(name, clauses[i + 1])} }}")
|
|
has_else = True
|
|
break
|
|
if i + 1 < len(clauses):
|
|
prefix = "if" if clause_idx == 0 else "else if"
|
|
cond = self.emit(c)
|
|
body = self._emit_tail_as_stmt(name, clauses[i + 1])
|
|
parts.append(f"{prefix} (isSxTruthy({cond})) {{ {body} }}")
|
|
i += 2
|
|
else:
|
|
parts.append(f"else {{ {self._emit_tail_as_stmt(name, c)} }}")
|
|
has_else = True
|
|
i += 1
|
|
clause_idx += 1
|
|
if not has_else:
|
|
parts.append("else { return NIL; }")
|
|
return " ".join(parts)
|
|
|
|
def _emit_for_each_stmt(self, expr) -> str:
|
|
fn_expr = expr[1]
|
|
coll_expr = expr[2]
|
|
coll = self.emit(coll_expr)
|
|
# If fn is an inline lambda, emit a for loop
|
|
if isinstance(fn_expr, list) and fn_expr[0] == Symbol("fn"):
|
|
params = fn_expr[1]
|
|
body = fn_expr[2:]
|
|
p = params[0].name if isinstance(params[0], Symbol) else str(params[0])
|
|
p_js = self._mangle(p)
|
|
body_js = "\n".join(self.emit_statement(b) for b in body)
|
|
return f"{{ var _c = {coll}; for (var _i = 0; _i < _c.length; _i++) {{ var {p_js} = _c[_i]; {body_js} }} }}"
|
|
fn = self.emit(fn_expr)
|
|
return f"{{ var _c = {coll}; for (var _i = 0; _i < _c.length; _i++) {{ {fn}(_c[_i]); }} }}"
|
|
|
|
def _emit_quote(self, expr) -> str:
|
|
"""Emit a quoted expression as a JS literal AST."""
|
|
if isinstance(expr, bool):
|
|
return "true" if expr else "false"
|
|
if isinstance(expr, (int, float)):
|
|
return str(expr)
|
|
if isinstance(expr, str):
|
|
return self._js_string(expr)
|
|
if expr is None or expr is SX_NIL:
|
|
return "NIL"
|
|
if isinstance(expr, Symbol):
|
|
return f'new Symbol({self._js_string(expr.name)})'
|
|
if isinstance(expr, Keyword):
|
|
return f'new Keyword({self._js_string(expr.name)})'
|
|
if isinstance(expr, list):
|
|
return "[" + ", ".join(self._emit_quote(x) for x in expr) + "]"
|
|
return str(expr)
|
|
|
|
def _js_string(self, s: str) -> str:
|
|
return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") + '"'
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bootstrap compiler
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def extract_defines(source: str) -> list[tuple[str, list]]:
|
|
"""Parse .sx source, return list of (name, define-expr) for top-level defines."""
|
|
exprs = parse_all(source)
|
|
defines = []
|
|
for expr in exprs:
|
|
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
|
|
if expr[0].name == "define":
|
|
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
|
|
defines.append((name, expr))
|
|
return defines
|
|
|
|
|
|
ADAPTER_FILES = {
|
|
"parser": ("parser.sx", "parser"),
|
|
"html": ("adapter-html.sx", "adapter-html"),
|
|
"sx": ("adapter-sx.sx", "adapter-sx"),
|
|
"dom": ("adapter-dom.sx", "adapter-dom"),
|
|
"engine": ("engine.sx", "engine"),
|
|
"orchestration": ("orchestration.sx","orchestration"),
|
|
"cssx": ("cssx.sx", "cssx"),
|
|
"boot": ("boot.sx", "boot"),
|
|
}
|
|
|
|
# Dependencies
|
|
ADAPTER_DEPS = {
|
|
"engine": ["dom"],
|
|
"orchestration": ["engine", "dom"],
|
|
"cssx": [],
|
|
"boot": ["dom", "engine", "orchestration", "cssx", "parser"],
|
|
"parser": [],
|
|
}
|
|
|
|
SPEC_MODULES = {
|
|
"deps": ("deps.sx", "deps (component dependency analysis)"),
|
|
"router": ("router.sx", "router (client-side route matching)"),
|
|
}
|
|
|
|
|
|
EXTENSION_NAMES = {"continuations"}
|
|
|
|
CONTINUATIONS_JS = '''
|
|
// =========================================================================
|
|
// Extension: Delimited continuations (shift/reset)
|
|
// =========================================================================
|
|
|
|
function Continuation(fn) { this.fn = fn; }
|
|
Continuation.prototype._continuation = true;
|
|
Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); };
|
|
|
|
function ShiftSignal(kName, body, env) {
|
|
this.kName = kName;
|
|
this.body = body;
|
|
this.env = env;
|
|
}
|
|
|
|
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
|
|
|
|
var _resetResume = [];
|
|
|
|
function sfReset(args, env) {
|
|
var body = args[0];
|
|
try {
|
|
return trampoline(evalExpr(body, env));
|
|
} catch (e) {
|
|
if (e instanceof ShiftSignal) {
|
|
var sig = e;
|
|
var cont = new Continuation(function(value) {
|
|
if (value === undefined) value = NIL;
|
|
_resetResume.push(value);
|
|
try {
|
|
return trampoline(evalExpr(body, env));
|
|
} finally {
|
|
_resetResume.pop();
|
|
}
|
|
});
|
|
var sigEnv = merge(sig.env);
|
|
sigEnv[sig.kName] = cont;
|
|
return trampoline(evalExpr(sig.body, sigEnv));
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
function sfShift(args, env) {
|
|
if (_resetResume.length > 0) {
|
|
return _resetResume[_resetResume.length - 1];
|
|
}
|
|
var kName = symbolName(args[0]);
|
|
var body = args[1];
|
|
throw new ShiftSignal(kName, body, env);
|
|
}
|
|
|
|
// Wrap evalList to intercept reset/shift
|
|
var _baseEvalList = evalList;
|
|
evalList = function(expr, env) {
|
|
var head = expr[0];
|
|
if (isSym(head)) {
|
|
var name = head.name;
|
|
if (name === "reset") return sfReset(expr.slice(1), env);
|
|
if (name === "shift") return sfShift(expr.slice(1), env);
|
|
}
|
|
return _baseEvalList(expr, env);
|
|
};
|
|
|
|
// Wrap aserSpecial to handle reset/shift in SX wire mode
|
|
if (typeof aserSpecial === "function") {
|
|
var _baseAserSpecial = aserSpecial;
|
|
aserSpecial = function(name, expr, env) {
|
|
if (name === "reset") return sfReset(expr.slice(1), env);
|
|
if (name === "shift") return sfShift(expr.slice(1), env);
|
|
return _baseAserSpecial(name, expr, env);
|
|
};
|
|
}
|
|
|
|
// Wrap typeOf to recognize continuations
|
|
var _baseTypeOf = typeOf;
|
|
typeOf = function(x) {
|
|
if (x != null && x._continuation) return "continuation";
|
|
return _baseTypeOf(x);
|
|
};
|
|
'''
|
|
|
|
|
|
def compile_ref_to_js(
|
|
adapters: list[str] | None = None,
|
|
modules: list[str] | None = None,
|
|
extensions: list[str] | None = None,
|
|
spec_modules: 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.
|
|
spec_modules: List of spec module names to include.
|
|
Valid names: deps.
|
|
None = no spec modules.
|
|
"""
|
|
ref_dir = os.path.dirname(os.path.abspath(__file__))
|
|
emitter = JSEmitter()
|
|
|
|
# Platform JS blocks keyed by adapter name
|
|
adapter_platform = {
|
|
"parser": PLATFORM_PARSER_JS,
|
|
"dom": PLATFORM_DOM_JS,
|
|
"engine": PLATFORM_ENGINE_PURE_JS,
|
|
"orchestration": PLATFORM_ORCHESTRATION_JS,
|
|
"cssx": PLATFORM_CSSX_JS,
|
|
"boot": PLATFORM_BOOT_JS,
|
|
}
|
|
|
|
# Resolve adapter set
|
|
if adapters is None:
|
|
adapter_set = set(ADAPTER_FILES.keys())
|
|
else:
|
|
adapter_set = set()
|
|
for a in adapters:
|
|
if a not in ADAPTER_FILES:
|
|
raise ValueError(f"Unknown adapter: {a!r}. Valid: {', '.join(ADAPTER_FILES)}")
|
|
adapter_set.add(a)
|
|
# Pull in dependencies
|
|
for dep in ADAPTER_DEPS.get(a, []):
|
|
adapter_set.add(dep)
|
|
|
|
# Resolve spec modules
|
|
spec_mod_set = set()
|
|
if spec_modules:
|
|
for sm in spec_modules:
|
|
if sm not in SPEC_MODULES:
|
|
raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
|
|
spec_mod_set.add(sm)
|
|
has_deps = "deps" in spec_mod_set
|
|
has_router = "router" in spec_mod_set
|
|
|
|
# Core files always included, then selected adapters, then spec modules
|
|
sx_files = [
|
|
("eval.sx", "eval"),
|
|
("render.sx", "render (core)"),
|
|
]
|
|
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "cssx", "boot"):
|
|
if name in adapter_set:
|
|
sx_files.append(ADAPTER_FILES[name])
|
|
for name in sorted(spec_mod_set):
|
|
sx_files.append(SPEC_MODULES[name])
|
|
|
|
all_sections = []
|
|
for filename, label in sx_files:
|
|
filepath = os.path.join(ref_dir, filename)
|
|
if not os.path.exists(filepath):
|
|
continue
|
|
with open(filepath) as f:
|
|
src = f.read()
|
|
defines = extract_defines(src)
|
|
all_sections.append((label, defines))
|
|
|
|
# Resolve extensions
|
|
ext_set = set()
|
|
if extensions:
|
|
for e in extensions:
|
|
if e not in EXTENSION_NAMES:
|
|
raise ValueError(f"Unknown extension: {e!r}. Valid: {', '.join(EXTENSION_NAMES)}")
|
|
ext_set.add(e)
|
|
has_continuations = "continuations" in ext_set
|
|
|
|
# Build output
|
|
has_html = "html" in adapter_set
|
|
has_sx = "sx" in adapter_set
|
|
has_dom = "dom" in adapter_set
|
|
has_engine = "engine" in adapter_set
|
|
has_orch = "orchestration" in adapter_set
|
|
has_cssx = "cssx" in adapter_set
|
|
has_boot = "boot" in adapter_set
|
|
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_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)
|
|
|
|
if has_deps:
|
|
parts.append(PLATFORM_DEPS_JS)
|
|
|
|
# Parser platform must come before compiled parser.sx
|
|
if has_parser:
|
|
parts.append(adapter_platform["parser"])
|
|
|
|
for label, defines in all_sections:
|
|
parts.append(f"\n // === Transpiled from {label} ===\n")
|
|
for name, expr in defines:
|
|
parts.append(f" // {name}")
|
|
parts.append(f" {emitter.emit_statement(expr)}")
|
|
parts.append("")
|
|
|
|
# Platform JS for selected adapters
|
|
if not has_dom:
|
|
parts.append("\n var _hasDom = false;\n")
|
|
for name in ("dom", "engine", "orchestration", "cssx", "boot"):
|
|
if name in adapter_set and name in adapter_platform:
|
|
parts.append(adapter_platform[name])
|
|
|
|
parts.append(fixups_js(has_html, has_sx, has_dom))
|
|
if has_continuations:
|
|
parts.append(CONTINUATIONS_JS)
|
|
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps, has_router))
|
|
parts.append(EPILOGUE)
|
|
from datetime import datetime, timezone
|
|
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
return "\n".join(parts).replace("BUILD_TIMESTAMP", build_ts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Static JS sections
|
|
# ---------------------------------------------------------------------------
|
|
|
|
PREAMBLE = '''\
|
|
/**
|
|
* sx-ref.js — Generated from reference SX evaluator specification.
|
|
*
|
|
* Bootstrap-compiled from shared/sx/ref/{eval,render,primitives}.sx
|
|
* Compare against hand-written sx.js for correctness verification.
|
|
*
|
|
* DO NOT EDIT — regenerate with: python bootstrap_js.py
|
|
*/
|
|
;(function(global) {
|
|
"use strict";
|
|
|
|
// =========================================================================
|
|
// Types
|
|
// =========================================================================
|
|
|
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
|
var SX_VERSION = "BUILD_TIMESTAMP";
|
|
|
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
|
|
|
function Symbol(name) { this.name = name; }
|
|
Symbol.prototype.toString = function() { return this.name; };
|
|
Symbol.prototype._sym = true;
|
|
|
|
function Keyword(name) { this.name = name; }
|
|
Keyword.prototype.toString = function() { return ":" + this.name; };
|
|
Keyword.prototype._kw = true;
|
|
|
|
function Lambda(params, body, closure, name) {
|
|
this.params = params;
|
|
this.body = body;
|
|
this.closure = closure || {};
|
|
this.name = name || null;
|
|
}
|
|
Lambda.prototype._lambda = true;
|
|
|
|
function Component(name, params, hasChildren, body, closure) {
|
|
this.name = name;
|
|
this.params = params;
|
|
this.hasChildren = hasChildren;
|
|
this.body = body;
|
|
this.closure = closure || {};
|
|
}
|
|
Component.prototype._component = true;
|
|
|
|
function Macro(params, restParam, body, closure, name) {
|
|
this.params = params;
|
|
this.restParam = restParam;
|
|
this.body = body;
|
|
this.closure = closure || {};
|
|
this.name = name || null;
|
|
}
|
|
Macro.prototype._macro = true;
|
|
|
|
function Thunk(expr, env) { this.expr = expr; this.env = env; }
|
|
Thunk.prototype._thunk = true;
|
|
|
|
function RawHTML(html) { this.html = html; }
|
|
RawHTML.prototype._raw = true;
|
|
|
|
function StyleValue(className, declarations, mediaRules, pseudoRules, keyframes) {
|
|
this.className = className;
|
|
this.declarations = declarations || "";
|
|
this.mediaRules = mediaRules || [];
|
|
this.pseudoRules = pseudoRules || [];
|
|
this.keyframes = keyframes || [];
|
|
}
|
|
StyleValue.prototype._styleValue = true;
|
|
|
|
function isSym(x) { return x != null && x._sym === true; }
|
|
function isKw(x) { return x != null && x._kw === true; }
|
|
|
|
function merge() {
|
|
var out = {};
|
|
for (var i = 0; i < arguments.length; i++) {
|
|
var d = arguments[i];
|
|
if (d) for (var k in d) out[k] = d[k];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function sxOr() {
|
|
for (var i = 0; i < arguments.length; i++) {
|
|
if (isSxTruthy(arguments[i])) return arguments[i];
|
|
}
|
|
return arguments.length ? arguments[arguments.length - 1] : false;
|
|
}'''
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); };
|
|
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
|
|
// =========================================================================
|
|
|
|
function typeOf(x) {
|
|
if (isNil(x)) return "nil";
|
|
if (typeof x === "number") return "number";
|
|
if (typeof x === "string") return "string";
|
|
if (typeof x === "boolean") return "boolean";
|
|
if (x._sym) return "symbol";
|
|
if (x._kw) return "keyword";
|
|
if (x._thunk) return "thunk";
|
|
if (x._lambda) return "lambda";
|
|
if (x._component) return "component";
|
|
if (x._macro) return "macro";
|
|
if (x._raw) return "raw-html";
|
|
if (x._styleValue) return "style-value";
|
|
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
|
if (Array.isArray(x)) return "list";
|
|
if (typeof x === "object") return "dict";
|
|
return "unknown";
|
|
}
|
|
|
|
function symbolName(s) { return s.name; }
|
|
function keywordName(k) { return k.name; }
|
|
function makeSymbol(n) { return new Symbol(n); }
|
|
function makeKeyword(n) { return new Keyword(n); }
|
|
|
|
function makeLambda(params, body, env) { return new Lambda(params, body, merge(env)); }
|
|
function makeComponent(name, params, hasChildren, body, env) {
|
|
return new Component(name, params, hasChildren, body, merge(env));
|
|
}
|
|
function makeMacro(params, restParam, body, env, name) {
|
|
return new Macro(params, restParam, body, merge(env), name);
|
|
}
|
|
function makeThunk(expr, env) { return new Thunk(expr, env); }
|
|
|
|
function lambdaParams(f) { return f.params; }
|
|
function lambdaBody(f) { return f.body; }
|
|
function lambdaClosure(f) { return f.closure; }
|
|
function lambdaName(f) { return f.name; }
|
|
function setLambdaName(f, n) { f.name = n; }
|
|
|
|
function componentParams(c) { return c.params; }
|
|
function componentBody(c) { return c.body; }
|
|
function componentClosure(c) { return c.closure; }
|
|
function componentHasChildren(c) { return c.hasChildren; }
|
|
function componentName(c) { return c.name; }
|
|
|
|
function macroParams(m) { return m.params; }
|
|
function macroRestParam(m) { return m.restParam; }
|
|
function macroBody(m) { return m.body; }
|
|
function macroClosure(m) { return m.closure; }
|
|
|
|
function isThunk(x) { return x != null && x._thunk === true; }
|
|
function thunkExpr(t) { return t.expr; }
|
|
function thunkEnv(t) { return t.env; }
|
|
|
|
function isCallable(x) { return typeof x === "function" || (x != null && x._lambda === true); }
|
|
function isLambda(x) { return x != null && x._lambda === true; }
|
|
function isComponent(x) { return x != null && x._component === true; }
|
|
function isMacro(x) { return x != null && x._macro === true; }
|
|
|
|
function isStyleValue(x) { return x != null && x._styleValue === true; }
|
|
function styleValueClass(x) { return x.className; }
|
|
function styleValue_p(x) { return x != null && x._styleValue === true; }
|
|
|
|
function buildKeyframes(kfName, steps, env) {
|
|
// Platform implementation of defkeyframes
|
|
var parts = [];
|
|
for (var i = 0; i < steps.length; i++) {
|
|
var step = steps[i];
|
|
var selector = isSym(step[0]) ? step[0].name : String(step[0]);
|
|
var body = trampoline(evalExpr(step[1], env));
|
|
var decls = isStyleValue(body) ? body.declarations : String(body);
|
|
parts.push(selector + "{" + decls + "}");
|
|
}
|
|
var kfRule = "@keyframes " + kfName + "{" + parts.join("") + "}";
|
|
var cn = "sx-ref-kf-" + kfName;
|
|
var sv = new StyleValue(cn, "animation-name:" + kfName, [], [], [[kfName, kfRule]]);
|
|
env[kfName] = sv;
|
|
return sv;
|
|
}
|
|
|
|
function envHas(env, name) { return name in env; }
|
|
function envGet(env, name) { return env[name]; }
|
|
function envSet(env, name, val) { env[name] = val; }
|
|
function envExtend(env) { return merge(env); }
|
|
function envMerge(base, overlay) { return merge(base, overlay); }
|
|
|
|
function dictSet(d, k, v) { d[k] = v; }
|
|
function dictGet(d, k) { var v = d[k]; return v !== undefined ? v : NIL; }
|
|
|
|
// Render-expression detection — lets the evaluator delegate to the active adapter.
|
|
// Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements.
|
|
function isRenderExpr(expr) {
|
|
if (!Array.isArray(expr) || !expr.length) return false;
|
|
var h = expr[0];
|
|
if (!h || !h._sym) return false;
|
|
var n = h.name;
|
|
return !!(n === "<>" || n === "raw!" ||
|
|
n.charAt(0) === "~" || n.indexOf("html:") === 0 ||
|
|
(typeof HTML_TAGS !== "undefined" && HTML_TAGS.indexOf(n) >= 0) ||
|
|
(typeof SVG_TAGS !== "undefined" && SVG_TAGS.indexOf(n) >= 0) ||
|
|
(n.indexOf("-") > 0 && expr.length > 1 && expr[1] && expr[1]._kw));
|
|
}
|
|
|
|
// Render dispatch — call the active adapter's render function.
|
|
// Set by each adapter when loaded; defaults to identity (no rendering).
|
|
var _renderExprFn = null;
|
|
function renderExpr(expr, env) {
|
|
if (_renderExprFn) return _renderExprFn(expr, env);
|
|
// No adapter loaded — just return the expression as-is
|
|
return expr;
|
|
}
|
|
|
|
function stripPrefix(s, prefix) {
|
|
return s.indexOf(prefix) === 0 ? s.slice(prefix.length) : s;
|
|
}
|
|
|
|
function error(msg) { throw new Error(msg); }
|
|
function inspect(x) { return JSON.stringify(x); }
|
|
|
|
'''
|
|
|
|
PLATFORM_JS_POST = '''
|
|
function isPrimitive(name) { return name in PRIMITIVES; }
|
|
function getPrimitive(name) { return PRIMITIVES[name]; }
|
|
|
|
// Higher-order helpers used by the transpiled code
|
|
function map(fn, coll) { return coll.map(fn); }
|
|
function mapIndexed(fn, coll) { return coll.map(function(item, i) { return fn(i, item); }); }
|
|
function filter(fn, coll) { return coll.filter(function(x) { return isSxTruthy(fn(x)); }); }
|
|
function reduce(fn, init, coll) {
|
|
var acc = init;
|
|
for (var i = 0; i < coll.length; i++) acc = fn(acc, coll[i]);
|
|
return acc;
|
|
}
|
|
function some(fn, coll) {
|
|
for (var i = 0; i < coll.length; i++) { var r = fn(coll[i]); if (isSxTruthy(r)) return r; }
|
|
return NIL;
|
|
}
|
|
function forEach(fn, coll) { for (var i = 0; i < coll.length; i++) fn(coll[i]); return NIL; }
|
|
function isEvery(fn, coll) {
|
|
for (var i = 0; i < coll.length; i++) { if (!isSxTruthy(fn(coll[i]))) return false; }
|
|
return true;
|
|
}
|
|
function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; }
|
|
|
|
// List primitives used directly by transpiled code
|
|
var len = PRIMITIVES["len"];
|
|
var first = PRIMITIVES["first"];
|
|
var last = PRIMITIVES["last"];
|
|
var rest = PRIMITIVES["rest"];
|
|
var nth = PRIMITIVES["nth"];
|
|
var cons = PRIMITIVES["cons"];
|
|
var append = PRIMITIVES["append"];
|
|
var isEmpty = PRIMITIVES["empty?"];
|
|
var contains = PRIMITIVES["contains?"];
|
|
var startsWith = PRIMITIVES["starts-with?"];
|
|
var slice = PRIMITIVES["slice"];
|
|
var concat = PRIMITIVES["concat"];
|
|
var str = PRIMITIVES["str"];
|
|
var join = PRIMITIVES["join"];
|
|
var keys = PRIMITIVES["keys"];
|
|
var get = PRIMITIVES["get"];
|
|
var assoc = PRIMITIVES["assoc"];
|
|
var range = PRIMITIVES["range"];
|
|
function zip(a, b) { var r = []; for (var i = 0; i < Math.min(a.length, b.length); i++) r.push([a[i], b[i]]); return r; }
|
|
function append_b(arr, x) { arr.push(x); return arr; }
|
|
var apply = function(f, args) { return f.apply(null, args); };
|
|
|
|
// Additional primitive aliases used by adapter/engine transpiled code
|
|
var split = PRIMITIVES["split"];
|
|
var trim = PRIMITIVES["trim"];
|
|
var upper = PRIMITIVES["upper"];
|
|
var lower = PRIMITIVES["lower"];
|
|
var replace_ = function(s, old, nw) { return s.split(old).join(nw); };
|
|
var endsWith = PRIMITIVES["ends-with?"];
|
|
var parseInt_ = PRIMITIVES["parse-int"];
|
|
var dict_fn = PRIMITIVES["dict"];
|
|
|
|
// HTML rendering helpers
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
|
}
|
|
function escapeAttr(s) { return escapeHtml(s); }
|
|
function rawHtmlContent(r) { return r.html; }
|
|
function makeRawHtml(s) { return { _raw: true, html: s }; }
|
|
|
|
// Serializer
|
|
function serialize(val) {
|
|
if (isNil(val)) return "nil";
|
|
if (typeof val === "boolean") return val ? "true" : "false";
|
|
if (typeof val === "number") return String(val);
|
|
if (typeof val === "string") return \'"\' + val.replace(/\\\\/g, "\\\\\\\\").replace(/"/g, \'\\\\"\') + \'"\';
|
|
if (isSym(val)) return val.name;
|
|
if (isKw(val)) return ":" + val.name;
|
|
if (Array.isArray(val)) return "(" + val.map(serialize).join(" ") + ")";
|
|
return String(val);
|
|
}
|
|
|
|
function isSpecialForm(n) { return n in {
|
|
"if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1,
|
|
"lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1,
|
|
"defkeyframes":1,"defhandler":1,"begin":1,"do":1,
|
|
"quote":1,"quasiquote":1,"->":1,"set!":1
|
|
}; }
|
|
function isHoForm(n) { return n in {
|
|
"map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1
|
|
}; }
|
|
|
|
// processBindings and evalCond — now specced in render.sx, bootstrapped above
|
|
|
|
function isDefinitionForm(name) {
|
|
return name === "define" || name === "defcomp" || name === "defmacro" ||
|
|
name === "defstyle" || name === "defkeyframes" || name === "defhandler";
|
|
}
|
|
|
|
function indexOf_(s, ch) {
|
|
return typeof s === "string" ? s.indexOf(ch) : -1;
|
|
}
|
|
|
|
function dictHas(d, k) { return d != null && k in d; }
|
|
function dictDelete(d, k) { delete d[k]; }
|
|
|
|
function forEachIndexed(fn, coll) {
|
|
for (var i = 0; i < coll.length; i++) fn(i, coll[i]);
|
|
return NIL;
|
|
}'''
|
|
|
|
PLATFORM_DEPS_JS = '''
|
|
// =========================================================================
|
|
// Platform: deps module — component dependency analysis
|
|
// =========================================================================
|
|
|
|
function componentDeps(c) {
|
|
return c.deps ? c.deps.slice() : [];
|
|
}
|
|
|
|
function componentSetDeps(c, deps) {
|
|
c.deps = deps;
|
|
}
|
|
|
|
function componentCssClasses(c) {
|
|
return c.cssClasses ? c.cssClasses.slice() : [];
|
|
}
|
|
|
|
function envComponents(env) {
|
|
var names = [];
|
|
for (var k in env) {
|
|
var v = env[k];
|
|
if (v && (v._component || v._macro)) names.push(k);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
function regexFindAll(pattern, source) {
|
|
var re = new RegExp(pattern, "g");
|
|
var results = [];
|
|
var m;
|
|
while ((m = re.exec(source)) !== null) {
|
|
if (m[1] !== undefined) results.push(m[1]);
|
|
else results.push(m[0]);
|
|
}
|
|
return results;
|
|
}
|
|
|
|
function scanCssClasses(source) {
|
|
var classes = {};
|
|
var result = [];
|
|
var m;
|
|
var re1 = /:class\\s+"([^"]*)"/g;
|
|
while ((m = re1.exec(source)) !== null) {
|
|
var parts = m[1].split(/\\s+/);
|
|
for (var i = 0; i < parts.length; i++) {
|
|
if (parts[i] && !classes[parts[i]]) {
|
|
classes[parts[i]] = true;
|
|
result.push(parts[i]);
|
|
}
|
|
}
|
|
}
|
|
var re2 = /:class\\s+\\(str\\s+((?:"[^"]*"\\s*)+)\\)/g;
|
|
while ((m = re2.exec(source)) !== null) {
|
|
var re3 = /"([^"]*)"/g;
|
|
var m2;
|
|
while ((m2 = re3.exec(m[1])) !== null) {
|
|
var parts2 = m2[1].split(/\\s+/);
|
|
for (var j = 0; j < parts2.length; j++) {
|
|
if (parts2[j] && !classes[parts2[j]]) {
|
|
classes[parts2[j]] = true;
|
|
result.push(parts2[j]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var re4 = /;;\\s*@css\\s+(.+)/g;
|
|
while ((m = re4.exec(source)) !== null) {
|
|
var parts3 = m[1].split(/\\s+/);
|
|
for (var k = 0; k < parts3.length; k++) {
|
|
if (parts3[k] && !classes[parts3[k]]) {
|
|
classes[parts3[k]] = true;
|
|
result.push(parts3[k]);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function componentIoRefs(c) {
|
|
return c.ioRefs ? c.ioRefs.slice() : [];
|
|
}
|
|
|
|
function componentSetIoRefs(c, refs) {
|
|
c.ioRefs = refs;
|
|
}
|
|
'''
|
|
|
|
PLATFORM_PARSER_JS = r"""
|
|
// =========================================================================
|
|
// Platform interface — Parser
|
|
// =========================================================================
|
|
// Character classification derived from the grammar:
|
|
// ident-start → [a-zA-Z_~*+\-><=/!?&]
|
|
// ident-char → ident-start + [0-9.:\/\[\]#,]
|
|
|
|
var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/;
|
|
var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]/;
|
|
|
|
function isIdentStart(ch) { return _identStartRe.test(ch); }
|
|
function isIdentChar(ch) { return _identCharRe.test(ch); }
|
|
function parseNumber(s) { return Number(s); }
|
|
function escapeString(s) {
|
|
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
}
|
|
function sxExprSource(e) { return typeof e === "string" ? e : String(e); }
|
|
"""
|
|
|
|
PLATFORM_DOM_JS = """
|
|
// =========================================================================
|
|
// Platform interface — DOM adapter (browser-only)
|
|
// =========================================================================
|
|
|
|
var _hasDom = typeof document !== "undefined";
|
|
|
|
// Register DOM adapter as the render dispatch target for the evaluator.
|
|
_renderExprFn = function(expr, env) { return renderToDom(expr, env, null); };
|
|
|
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
var MATH_NS = "http://www.w3.org/1998/Math/MathML";
|
|
|
|
function domCreateElement(tag, ns) {
|
|
if (!_hasDom) return null;
|
|
if (ns) return document.createElementNS(ns, tag);
|
|
return document.createElement(tag);
|
|
}
|
|
|
|
function createTextNode(s) {
|
|
return _hasDom ? document.createTextNode(s) : null;
|
|
}
|
|
|
|
function createFragment() {
|
|
return _hasDom ? document.createDocumentFragment() : null;
|
|
}
|
|
|
|
function domAppend(parent, child) {
|
|
if (parent && child) parent.appendChild(child);
|
|
}
|
|
|
|
function domPrepend(parent, child) {
|
|
if (parent && child) parent.insertBefore(child, parent.firstChild);
|
|
}
|
|
|
|
function domSetAttr(el, name, val) {
|
|
if (el && el.setAttribute) el.setAttribute(name, val);
|
|
}
|
|
|
|
function domGetAttr(el, name) {
|
|
if (!el || !el.getAttribute) return NIL;
|
|
var v = el.getAttribute(name);
|
|
return v === null ? NIL : v;
|
|
}
|
|
|
|
function domRemoveAttr(el, name) {
|
|
if (el && el.removeAttribute) el.removeAttribute(name);
|
|
}
|
|
|
|
function domHasAttr(el, name) {
|
|
return !!(el && el.hasAttribute && el.hasAttribute(name));
|
|
}
|
|
|
|
function domParseHtml(html) {
|
|
if (!_hasDom) return null;
|
|
var tpl = document.createElement("template");
|
|
tpl.innerHTML = html;
|
|
return tpl.content;
|
|
}
|
|
|
|
function domClone(node) {
|
|
return node && node.cloneNode ? node.cloneNode(true) : node;
|
|
}
|
|
|
|
function domParent(el) { return el ? el.parentNode : null; }
|
|
function domId(el) { return el && el.id ? el.id : NIL; }
|
|
function domNodeType(el) { return el ? el.nodeType : 0; }
|
|
function domNodeName(el) { return el ? el.nodeName : ""; }
|
|
function domTextContent(el) { return el ? el.textContent || el.nodeValue || "" : ""; }
|
|
function domSetTextContent(el, s) { if (el) { if (el.nodeType === 3 || el.nodeType === 8) el.nodeValue = s; else el.textContent = s; } }
|
|
function domIsFragment(el) { return el ? el.nodeType === 11 : false; }
|
|
function domIsChildOf(child, parent) { return !!(parent && child && child.parentNode === parent); }
|
|
function domIsActiveElement(el) { return _hasDom && el === document.activeElement; }
|
|
function domIsInputElement(el) {
|
|
if (!el || !el.tagName) return false;
|
|
var t = el.tagName;
|
|
return t === "INPUT" || t === "TEXTAREA" || t === "SELECT";
|
|
}
|
|
function domFirstChild(el) { return el ? el.firstChild : null; }
|
|
function domNextSibling(el) { return el ? el.nextSibling : null; }
|
|
|
|
function domChildList(el) {
|
|
if (!el || !el.childNodes) return [];
|
|
return Array.prototype.slice.call(el.childNodes);
|
|
}
|
|
|
|
function domAttrList(el) {
|
|
if (!el || !el.attributes) return [];
|
|
var r = [];
|
|
for (var i = 0; i < el.attributes.length; i++) {
|
|
r.push([el.attributes[i].name, el.attributes[i].value]);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
function domInsertBefore(parent, node, ref) {
|
|
if (parent && node) parent.insertBefore(node, ref || null);
|
|
}
|
|
|
|
function domInsertAfter(ref, node) {
|
|
if (ref && ref.parentNode && node) {
|
|
ref.parentNode.insertBefore(node, ref.nextSibling);
|
|
}
|
|
}
|
|
|
|
function domRemoveChild(parent, child) {
|
|
if (parent && child && child.parentNode === parent) parent.removeChild(child);
|
|
}
|
|
|
|
function domReplaceChild(parent, newChild, oldChild) {
|
|
if (parent && newChild && oldChild) parent.replaceChild(newChild, oldChild);
|
|
}
|
|
|
|
function domSetInnerHtml(el, html) {
|
|
if (el) el.innerHTML = html;
|
|
}
|
|
|
|
function domInsertAdjacentHtml(el, pos, html) {
|
|
if (el && el.insertAdjacentHTML) el.insertAdjacentHTML(pos, html);
|
|
}
|
|
|
|
function domGetStyle(el, prop) {
|
|
return el && el.style ? el.style[prop] || "" : "";
|
|
}
|
|
|
|
function domSetStyle(el, prop, val) {
|
|
if (el && el.style) el.style[prop] = val;
|
|
}
|
|
|
|
function domGetProp(el, name) { return el ? el[name] : NIL; }
|
|
function domSetProp(el, name, val) { if (el) el[name] = val; }
|
|
|
|
function domAddClass(el, cls) {
|
|
if (el && el.classList) el.classList.add(cls);
|
|
}
|
|
|
|
function domRemoveClass(el, cls) {
|
|
if (el && el.classList) el.classList.remove(cls);
|
|
}
|
|
|
|
function domDispatch(el, name, detail) {
|
|
if (!_hasDom || !el) return false;
|
|
var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} });
|
|
return el.dispatchEvent(evt);
|
|
}
|
|
|
|
function domQuery(sel) {
|
|
return _hasDom ? document.querySelector(sel) : null;
|
|
}
|
|
|
|
function domQueryAll(root, sel) {
|
|
if (!root || !root.querySelectorAll) return [];
|
|
return Array.prototype.slice.call(root.querySelectorAll(sel));
|
|
}
|
|
|
|
function domTagName(el) { return el && el.tagName ? el.tagName : ""; }
|
|
"""
|
|
|
|
PLATFORM_ENGINE_PURE_JS = """
|
|
// =========================================================================
|
|
// Platform interface — Engine pure logic (browser + node compatible)
|
|
// =========================================================================
|
|
|
|
function browserLocationHref() {
|
|
return typeof location !== "undefined" ? location.href : "";
|
|
}
|
|
|
|
function browserSameOrigin(url) {
|
|
try { return new URL(url, location.href).origin === location.origin; }
|
|
catch (e) { return true; }
|
|
}
|
|
|
|
function browserPushState(url) {
|
|
if (typeof history !== "undefined") {
|
|
try { history.pushState({ sxUrl: url, scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", url); }
|
|
catch (e) {}
|
|
}
|
|
}
|
|
|
|
function browserReplaceState(url) {
|
|
if (typeof history !== "undefined") {
|
|
try { history.replaceState({ sxUrl: url, scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", url); }
|
|
catch (e) {}
|
|
}
|
|
}
|
|
|
|
function nowMs() { return Date.now(); }
|
|
|
|
function parseHeaderValue(s) {
|
|
if (!s) return null;
|
|
try {
|
|
if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s);
|
|
return JSON.parse(s);
|
|
} catch (e) { return null; }
|
|
}
|
|
"""
|
|
|
|
PLATFORM_ORCHESTRATION_JS = """
|
|
// =========================================================================
|
|
// Platform interface — Orchestration (browser-only)
|
|
// =========================================================================
|
|
|
|
// --- Browser/Network ---
|
|
|
|
function browserNavigate(url) {
|
|
if (typeof location !== "undefined") location.assign(url);
|
|
}
|
|
|
|
function browserReload() {
|
|
if (typeof location !== "undefined") location.reload();
|
|
}
|
|
|
|
function browserScrollTo(x, y) {
|
|
if (typeof window !== "undefined") window.scrollTo(x, y);
|
|
}
|
|
|
|
function browserMediaMatches(query) {
|
|
if (typeof window === "undefined") return false;
|
|
return window.matchMedia(query).matches;
|
|
}
|
|
|
|
function browserConfirm(msg) {
|
|
if (typeof window === "undefined") return false;
|
|
return window.confirm(msg);
|
|
}
|
|
|
|
function browserPrompt(msg) {
|
|
if (typeof window === "undefined") return NIL;
|
|
var r = window.prompt(msg);
|
|
return r === null ? NIL : r;
|
|
}
|
|
|
|
function csrfToken() {
|
|
if (!_hasDom) return NIL;
|
|
var m = document.querySelector('meta[name="csrf-token"]');
|
|
return m ? m.getAttribute("content") : NIL;
|
|
}
|
|
|
|
function isCrossOrigin(url) {
|
|
try {
|
|
var h = new URL(url, location.href).hostname;
|
|
return h !== location.hostname &&
|
|
(h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0);
|
|
} catch (e) { return false; }
|
|
}
|
|
|
|
// --- Promises ---
|
|
|
|
function promiseResolve(val) { return Promise.resolve(val); }
|
|
|
|
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
|
|
|
|
// --- Abort controllers ---
|
|
|
|
var _controllers = typeof WeakMap !== "undefined" ? new WeakMap() : null;
|
|
|
|
function abortPrevious(el) {
|
|
if (_controllers) {
|
|
var prev = _controllers.get(el);
|
|
if (prev) prev.abort();
|
|
}
|
|
}
|
|
|
|
function trackController(el, ctrl) {
|
|
if (_controllers) _controllers.set(el, ctrl);
|
|
}
|
|
|
|
function newAbortController() {
|
|
return typeof AbortController !== "undefined" ? new AbortController() : { signal: null, abort: function() {} };
|
|
}
|
|
|
|
function controllerSignal(ctrl) { return ctrl ? ctrl.signal : null; }
|
|
|
|
function isAbortError(err) { return err && err.name === "AbortError"; }
|
|
|
|
// --- Timers ---
|
|
|
|
function setTimeout_(fn, ms) { return setTimeout(fn, ms || 0); }
|
|
function setInterval_(fn, ms) { return setInterval(fn, ms || 1000); }
|
|
function clearTimeout_(id) { clearTimeout(id); }
|
|
function requestAnimationFrame_(fn) {
|
|
if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(fn);
|
|
else setTimeout(fn, 16);
|
|
}
|
|
|
|
// --- Fetch ---
|
|
|
|
function fetchRequest(config, successFn, errorFn) {
|
|
var opts = { method: config.method, headers: config.headers };
|
|
if (config.signal) opts.signal = config.signal;
|
|
if (config.body && config.method !== "GET") opts.body = config.body;
|
|
if (config["cross-origin"]) opts.credentials = "include";
|
|
|
|
var p = (config.preloaded && config.preloaded !== NIL)
|
|
? Promise.resolve({
|
|
ok: true, status: 200,
|
|
headers: new Headers({ "Content-Type": config.preloaded["content-type"] || "" }),
|
|
text: function() { return Promise.resolve(config.preloaded.text); }
|
|
})
|
|
: fetch(config.url, opts);
|
|
|
|
return p.then(function(resp) {
|
|
return resp.text().then(function(text) {
|
|
var getHeader = function(name) {
|
|
var v = resp.headers.get(name);
|
|
return v === null ? NIL : v;
|
|
};
|
|
return successFn(resp.ok, resp.status, getHeader, text);
|
|
});
|
|
}).catch(function(err) {
|
|
return errorFn(err);
|
|
});
|
|
}
|
|
|
|
function fetchLocation(headerVal) {
|
|
if (!_hasDom) return;
|
|
var locUrl = headerVal;
|
|
try { var obj = JSON.parse(headerVal); locUrl = obj.path || obj; } catch (e) {}
|
|
fetch(locUrl, { headers: { "SX-Request": "true" } }).then(function(r) {
|
|
return r.text().then(function(t) {
|
|
var main = document.getElementById("main-panel");
|
|
if (main) {
|
|
main.innerHTML = t;
|
|
postSwap(main);
|
|
try { history.pushState({ sxUrl: locUrl }, "", locUrl); } catch (e) {}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function fetchAndRestore(main, url, headers, scrollY) {
|
|
var opts = { headers: headers };
|
|
try {
|
|
var h = new URL(url, location.href).hostname;
|
|
if (h !== location.hostname &&
|
|
(h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0)) {
|
|
opts.credentials = "include";
|
|
}
|
|
} catch (e) {}
|
|
|
|
fetch(url, opts).then(function(resp) {
|
|
return resp.text().then(function(text) {
|
|
text = stripComponentScripts(text);
|
|
text = extractResponseCss(text);
|
|
text = text.trim();
|
|
if (text.charAt(0) === "(") {
|
|
try {
|
|
var dom = sxRender(text);
|
|
var container = document.createElement("div");
|
|
container.appendChild(dom);
|
|
processOobSwaps(container, function(t, oob, s) {
|
|
swapDomNodes(t, oob, s);
|
|
sxHydrate(t);
|
|
processElements(t);
|
|
});
|
|
var newMain = container.querySelector("#main-panel");
|
|
morphChildren(main, newMain || container);
|
|
postSwap(main);
|
|
if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0);
|
|
} catch (err) {
|
|
console.error("sx-ref popstate error:", err);
|
|
location.reload();
|
|
}
|
|
} else {
|
|
var parser = new DOMParser();
|
|
var doc = parser.parseFromString(text, "text/html");
|
|
var newMain = doc.getElementById("main-panel");
|
|
if (newMain) {
|
|
morphChildren(main, newMain);
|
|
postSwap(main);
|
|
if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0);
|
|
} else {
|
|
location.reload();
|
|
}
|
|
}
|
|
});
|
|
}).catch(function() { location.reload(); });
|
|
}
|
|
|
|
function fetchPreload(url, headers, cache) {
|
|
fetch(url, { headers: headers }).then(function(resp) {
|
|
if (!resp.ok) return;
|
|
var ct = resp.headers.get("Content-Type") || "";
|
|
return resp.text().then(function(text) {
|
|
preloadCacheSet(cache, url, text, ct);
|
|
});
|
|
}).catch(function() { /* ignore */ });
|
|
}
|
|
|
|
// --- Request body building ---
|
|
|
|
function buildRequestBody(el, method, url) {
|
|
if (!_hasDom) return { body: null, url: url, "content-type": NIL };
|
|
var body = null;
|
|
var ct = NIL;
|
|
var finalUrl = url;
|
|
var isJson = el.getAttribute("sx-encoding") === "json";
|
|
|
|
if (method !== "GET") {
|
|
var form = el.closest("form") || (el.tagName === "FORM" ? el : null);
|
|
if (form) {
|
|
if (isJson) {
|
|
var fd = new FormData(form);
|
|
var obj = {};
|
|
fd.forEach(function(v, k) {
|
|
if (obj[k] !== undefined) {
|
|
if (!Array.isArray(obj[k])) obj[k] = [obj[k]];
|
|
obj[k].push(v);
|
|
} else { obj[k] = v; }
|
|
});
|
|
body = JSON.stringify(obj);
|
|
ct = "application/json";
|
|
} else {
|
|
body = new URLSearchParams(new FormData(form));
|
|
ct = "application/x-www-form-urlencoded";
|
|
}
|
|
}
|
|
}
|
|
|
|
// sx-params
|
|
var paramsSpec = el.getAttribute("sx-params");
|
|
if (paramsSpec && body instanceof URLSearchParams) {
|
|
if (paramsSpec === "none") {
|
|
body = new URLSearchParams();
|
|
} else if (paramsSpec.indexOf("not ") === 0) {
|
|
paramsSpec.substring(4).split(",").forEach(function(k) { body.delete(k.trim()); });
|
|
} else if (paramsSpec !== "*") {
|
|
var allowed = paramsSpec.split(",").map(function(s) { return s.trim(); });
|
|
var filtered = new URLSearchParams();
|
|
allowed.forEach(function(k) {
|
|
body.getAll(k).forEach(function(v) { filtered.append(k, v); });
|
|
});
|
|
body = filtered;
|
|
}
|
|
}
|
|
|
|
// sx-include
|
|
var includeSel = el.getAttribute("sx-include");
|
|
if (includeSel && method !== "GET") {
|
|
if (!body) body = new URLSearchParams();
|
|
document.querySelectorAll(includeSel).forEach(function(inp) {
|
|
if (inp.name) body.append(inp.name, inp.value);
|
|
});
|
|
}
|
|
|
|
// sx-vals
|
|
var valsAttr = el.getAttribute("sx-vals");
|
|
if (valsAttr) {
|
|
try {
|
|
var vals = valsAttr.charAt(0) === "{" && valsAttr.charAt(1) === ":" ? parse(valsAttr) : JSON.parse(valsAttr);
|
|
if (method === "GET") {
|
|
for (var vk in vals) finalUrl += (finalUrl.indexOf("?") >= 0 ? "&" : "?") + encodeURIComponent(vk) + "=" + encodeURIComponent(vals[vk]);
|
|
} else if (body instanceof URLSearchParams) {
|
|
for (var vk2 in vals) body.append(vk2, vals[vk2]);
|
|
} else if (!body) {
|
|
body = new URLSearchParams();
|
|
for (var vk3 in vals) body.append(vk3, vals[vk3]);
|
|
ct = "application/x-www-form-urlencoded";
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
// GET form data → URL
|
|
if (method === "GET") {
|
|
var form2 = el.closest("form") || (el.tagName === "FORM" ? el : null);
|
|
if (form2) {
|
|
var qs = new URLSearchParams(new FormData(form2)).toString();
|
|
if (qs) finalUrl += (finalUrl.indexOf("?") >= 0 ? "&" : "?") + qs;
|
|
}
|
|
if ((el.tagName === "INPUT" || el.tagName === "SELECT" || el.tagName === "TEXTAREA") && el.name) {
|
|
finalUrl += (finalUrl.indexOf("?") >= 0 ? "&" : "?") + encodeURIComponent(el.name) + "=" + encodeURIComponent(el.value);
|
|
}
|
|
}
|
|
|
|
return { body: body, url: finalUrl, "content-type": ct };
|
|
}
|
|
|
|
// --- Loading state ---
|
|
|
|
function showIndicator(el) {
|
|
if (!_hasDom) return NIL;
|
|
var sel = el.getAttribute("sx-indicator");
|
|
var ind = sel ? (document.querySelector(sel) || el.closest(sel)) : null;
|
|
if (ind) { ind.classList.add("sx-request"); ind.style.display = ""; }
|
|
return ind || NIL;
|
|
}
|
|
|
|
function disableElements(el) {
|
|
if (!_hasDom) return [];
|
|
var sel = el.getAttribute("sx-disabled-elt");
|
|
if (!sel) return [];
|
|
var elts = Array.prototype.slice.call(document.querySelectorAll(sel));
|
|
elts.forEach(function(e) { e.disabled = true; });
|
|
return elts;
|
|
}
|
|
|
|
function clearLoadingState(el, indicator, disabledElts) {
|
|
el.classList.remove("sx-request");
|
|
el.removeAttribute("aria-busy");
|
|
if (indicator && !isNil(indicator)) {
|
|
indicator.classList.remove("sx-request");
|
|
indicator.style.display = "none";
|
|
}
|
|
if (disabledElts) {
|
|
for (var i = 0; i < disabledElts.length; i++) disabledElts[i].disabled = false;
|
|
}
|
|
}
|
|
|
|
// --- DOM extras ---
|
|
|
|
function domQueryById(id) {
|
|
return _hasDom ? document.getElementById(id) : null;
|
|
}
|
|
|
|
function domMatches(el, sel) {
|
|
return el && el.matches ? el.matches(sel) : false;
|
|
}
|
|
|
|
function domClosest(el, sel) {
|
|
return el && el.closest ? el.closest(sel) : null;
|
|
}
|
|
|
|
function domBody() {
|
|
return _hasDom ? document.body : null;
|
|
}
|
|
|
|
function domHasClass(el, cls) {
|
|
return el && el.classList ? el.classList.contains(cls) : false;
|
|
}
|
|
|
|
function domAppendToHead(el) {
|
|
if (_hasDom && document.head) document.head.appendChild(el);
|
|
}
|
|
|
|
function domParseHtmlDocument(text) {
|
|
if (!_hasDom) return null;
|
|
return new DOMParser().parseFromString(text, "text/html");
|
|
}
|
|
|
|
function domOuterHtml(el) {
|
|
return el ? el.outerHTML : "";
|
|
}
|
|
|
|
function domBodyInnerHtml(doc) {
|
|
return doc && doc.body ? doc.body.innerHTML : "";
|
|
}
|
|
|
|
// --- Events ---
|
|
|
|
function preventDefault_(e) { if (e && e.preventDefault) e.preventDefault(); }
|
|
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
|
|
|
function domAddListener(el, event, fn, opts) {
|
|
if (!el || !el.addEventListener) return;
|
|
var o = {};
|
|
if (opts && !isNil(opts)) {
|
|
if (opts.once || opts["once"]) o.once = true;
|
|
}
|
|
el.addEventListener(event, function(e) {
|
|
try { fn(e); } catch (err) { logInfo("EVENT ERROR: " + event + " " + (err && err.message ? err.message : err)); console.error("[sx-ref] event handler error:", event, err); }
|
|
}, o);
|
|
}
|
|
|
|
// --- Validation ---
|
|
|
|
function validateForRequest(el) {
|
|
if (!_hasDom) return true;
|
|
var attr = el.getAttribute("sx-validate");
|
|
if (attr === null) {
|
|
var vForm = el.closest("[sx-validate]");
|
|
if (vForm) attr = vForm.getAttribute("sx-validate");
|
|
}
|
|
if (attr === null) return true; // no validation configured
|
|
var form = el.tagName === "FORM" ? el : el.closest("form");
|
|
if (form && !form.reportValidity()) return false;
|
|
if (attr && attr !== "true" && attr !== "") {
|
|
var fn = window[attr];
|
|
if (typeof fn === "function" && !fn(el)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// --- View Transitions ---
|
|
|
|
function withTransition(enabled, fn) {
|
|
if (enabled && _hasDom && document.startViewTransition) {
|
|
document.startViewTransition(fn);
|
|
} else {
|
|
fn();
|
|
}
|
|
}
|
|
|
|
// --- IntersectionObserver ---
|
|
|
|
function observeIntersection(el, fn, once, delay) {
|
|
if (!_hasDom || !("IntersectionObserver" in window)) { fn(); return; }
|
|
var fired = false;
|
|
var d = isNil(delay) ? 0 : delay;
|
|
var obs = new IntersectionObserver(function(entries) {
|
|
entries.forEach(function(entry) {
|
|
if (!entry.isIntersecting) return;
|
|
if (once && fired) return;
|
|
fired = true;
|
|
if (once) obs.unobserve(el);
|
|
if (d) setTimeout(fn, d); else fn();
|
|
});
|
|
});
|
|
obs.observe(el);
|
|
}
|
|
|
|
// --- EventSource ---
|
|
|
|
function eventSourceConnect(url, el) {
|
|
var source = new EventSource(url);
|
|
source.addEventListener("error", function() { domDispatch(el, "sx:sseError", {}); });
|
|
source.addEventListener("open", function() { domDispatch(el, "sx:sseOpen", {}); });
|
|
if (typeof MutationObserver !== "undefined") {
|
|
var obs = new MutationObserver(function() {
|
|
if (!document.body.contains(el)) { source.close(); obs.disconnect(); }
|
|
});
|
|
obs.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
return source;
|
|
}
|
|
|
|
function eventSourceListen(source, event, fn) {
|
|
source.addEventListener(event, function(e) { fn(e.data); });
|
|
}
|
|
|
|
// --- Boost bindings ---
|
|
|
|
function bindBoostLink(el, href) {
|
|
el.addEventListener("click", function(e) {
|
|
e.preventDefault();
|
|
executeRequest(el, { method: "GET", url: href }).then(function() {
|
|
try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {}
|
|
});
|
|
});
|
|
}
|
|
|
|
function bindBoostForm(form, method, action) {
|
|
form.addEventListener("submit", function(e) {
|
|
e.preventDefault();
|
|
executeRequest(form, { method: method, url: action }).then(function() {
|
|
try { history.pushState({ sxUrl: action, scrollY: window.scrollY }, "", action); } catch (err) {}
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- Client-side route bindings ---
|
|
|
|
function bindClientRouteClick(link, href, fallbackFn) {
|
|
link.addEventListener("click", function(e) {
|
|
e.preventDefault();
|
|
var pathname = urlPathname(href);
|
|
if (tryClientRoute(pathname)) {
|
|
try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {}
|
|
if (typeof window !== "undefined") window.scrollTo(0, 0);
|
|
} else {
|
|
logInfo("sx:route server " + pathname);
|
|
executeRequest(link, { method: "GET", url: href }).then(function() {
|
|
try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function tryEvalContent(source, env) {
|
|
try {
|
|
var merged = merge(componentEnv);
|
|
if (env && !isNil(env)) {
|
|
var ks = Object.keys(env);
|
|
for (var i = 0; i < ks.length; i++) merged[ks[i]] = env[ks[i]];
|
|
}
|
|
return sxRenderWithEnv(source, merged);
|
|
} catch (e) {
|
|
console.error("[sx-ref] sx:route eval error for:", source, e);
|
|
return NIL;
|
|
}
|
|
}
|
|
|
|
function urlPathname(href) {
|
|
try {
|
|
return new URL(href, location.href).pathname;
|
|
} catch (e) {
|
|
// Fallback: strip query/hash
|
|
var idx = href.indexOf("?");
|
|
if (idx >= 0) href = href.substring(0, idx);
|
|
idx = href.indexOf("#");
|
|
if (idx >= 0) href = href.substring(0, idx);
|
|
return href;
|
|
}
|
|
}
|
|
|
|
// --- Inline handlers ---
|
|
|
|
function bindInlineHandler(el, eventName, body) {
|
|
el.addEventListener(eventName, new Function("event", body));
|
|
}
|
|
|
|
// --- Preload binding ---
|
|
|
|
function bindPreload(el, events, debounceMs, fn) {
|
|
var timer = null;
|
|
events.forEach(function(evt) {
|
|
el.addEventListener(evt, function() {
|
|
if (debounceMs) {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(fn, debounceMs);
|
|
} else {
|
|
fn();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- Processing markers ---
|
|
|
|
var PROCESSED = "_sxBound";
|
|
|
|
function markProcessed(el, key) { el[PROCESSED + key] = true; }
|
|
function isProcessed(el, key) { return !!el[PROCESSED + key]; }
|
|
|
|
// --- Script cloning ---
|
|
|
|
function createScriptClone(dead) {
|
|
var live = document.createElement("script");
|
|
for (var i = 0; i < dead.attributes.length; i++)
|
|
live.setAttribute(dead.attributes[i].name, dead.attributes[i].value);
|
|
live.textContent = dead.textContent;
|
|
return live;
|
|
}
|
|
|
|
// --- SX API references ---
|
|
|
|
function sxRender(source) {
|
|
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
|
if (SxObj && SxObj.render) return SxObj.render(source);
|
|
throw new Error("No SX renderer available");
|
|
}
|
|
|
|
function sxProcessScripts(root) {
|
|
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
|
var r = (root && root !== NIL) ? root : undefined;
|
|
if (SxObj && SxObj.processScripts) SxObj.processScripts(r);
|
|
}
|
|
|
|
function sxHydrate(root) {
|
|
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
|
var r = (root && root !== NIL) ? root : undefined;
|
|
if (SxObj && SxObj.hydrate) SxObj.hydrate(r);
|
|
}
|
|
|
|
function loadedComponentNames() {
|
|
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
|
if (!SxObj) return [];
|
|
var env = SxObj.componentEnv || (SxObj.getEnv ? SxObj.getEnv() : {});
|
|
return Object.keys(env).filter(function(k) { return k.charAt(0) === "~"; });
|
|
}
|
|
|
|
// --- Response processing ---
|
|
|
|
function stripComponentScripts(text) {
|
|
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
|
return text.replace(/<script[^>]*type="text\\/sx"[^>]*data-components[^>]*>([\\s\\S]*?)<\\/script>/gi,
|
|
function(_, defs) { if (SxObj && SxObj.loadComponents) SxObj.loadComponents(defs); return ""; });
|
|
}
|
|
|
|
function extractResponseCss(text) {
|
|
if (!_hasDom) return text;
|
|
var target = document.getElementById("sx-css");
|
|
if (!target) return text;
|
|
return text.replace(/<style[^>]*data-sx-css[^>]*>([\\s\\S]*?)<\\/style>/gi,
|
|
function(_, css) { target.textContent += css; return ""; });
|
|
}
|
|
|
|
function selectFromContainer(container, sel) {
|
|
var frag = document.createDocumentFragment();
|
|
sel.split(",").forEach(function(s) {
|
|
container.querySelectorAll(s.trim()).forEach(function(m) { frag.appendChild(m); });
|
|
});
|
|
return frag;
|
|
}
|
|
|
|
function childrenToFragment(container) {
|
|
var frag = document.createDocumentFragment();
|
|
while (container.firstChild) frag.appendChild(container.firstChild);
|
|
return frag;
|
|
}
|
|
|
|
function selectHtmlFromDoc(doc, sel) {
|
|
var parts = sel.split(",").map(function(s) { return s.trim(); });
|
|
var frags = [];
|
|
parts.forEach(function(s) {
|
|
doc.querySelectorAll(s).forEach(function(m) { frags.push(m.outerHTML); });
|
|
});
|
|
return frags.join("");
|
|
}
|
|
|
|
// --- Parsing ---
|
|
|
|
function tryParseJson(s) {
|
|
if (!s) return NIL;
|
|
try { return JSON.parse(s); } catch (e) { return NIL; }
|
|
}
|
|
"""
|
|
|
|
PLATFORM_CSSX_JS = """
|
|
// =========================================================================
|
|
// Platform interface — CSSX (style dictionary)
|
|
// =========================================================================
|
|
|
|
function fnv1aHash(input) {
|
|
var h = 0x811c9dc5;
|
|
for (var i = 0; i < input.length; i++) {
|
|
h ^= input.charCodeAt(i);
|
|
h = (h * 0x01000193) >>> 0;
|
|
}
|
|
return h.toString(16).padStart(8, "0").substring(0, 6);
|
|
}
|
|
|
|
function compileRegex(pattern) {
|
|
try { return new RegExp(pattern); } catch (e) { return null; }
|
|
}
|
|
|
|
function regexMatch(re, s) {
|
|
if (!re) return NIL;
|
|
var m = s.match(re);
|
|
return m ? Array.prototype.slice.call(m) : NIL;
|
|
}
|
|
|
|
function regexReplaceGroups(tmpl, match) {
|
|
var result = tmpl;
|
|
for (var j = 1; j < match.length; j++) {
|
|
result = result.split("{" + (j - 1) + "}").join(match[j]);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function makeStyleValue_(cn, decls, media, pseudo, kf) {
|
|
return new StyleValue(cn, decls || "", media || [], pseudo || [], kf || []);
|
|
}
|
|
|
|
function styleValueDeclarations(sv) { return sv.declarations; }
|
|
function styleValueMediaRules(sv) { return sv.mediaRules; }
|
|
function styleValuePseudoRules(sv) { return sv.pseudoRules; }
|
|
function styleValueKeyframes_(sv) { return sv.keyframes; }
|
|
|
|
function injectStyleValue(sv, atoms) {
|
|
if (_injectedStyles[sv.className]) return;
|
|
_injectedStyles[sv.className] = true;
|
|
|
|
if (!_hasDom) return;
|
|
var cssTarget = document.getElementById("sx-css");
|
|
if (!cssTarget) return;
|
|
|
|
var rules = [];
|
|
// Child-selector atoms are now routed to pseudoRules by the resolver
|
|
// with selector ">:not(:first-child)", so base declarations are always
|
|
// applied directly to the class.
|
|
if (sv.declarations) {
|
|
rules.push("." + sv.className + "{" + sv.declarations + "}");
|
|
}
|
|
for (var pi = 0; pi < sv.pseudoRules.length; pi++) {
|
|
var sel = sv.pseudoRules[pi][0], decls = sv.pseudoRules[pi][1];
|
|
if (sel.indexOf("&") >= 0) {
|
|
rules.push(sel.replace(/&/g, "." + sv.className) + "{" + decls + "}");
|
|
} else {
|
|
rules.push("." + sv.className + sel + "{" + decls + "}");
|
|
}
|
|
}
|
|
for (var mi = 0; mi < sv.mediaRules.length; mi++) {
|
|
rules.push("@media " + sv.mediaRules[mi][0] + "{." + sv.className + "{" + sv.mediaRules[mi][1] + "}}");
|
|
}
|
|
for (var ki = 0; ki < sv.keyframes.length; ki++) {
|
|
rules.push(sv.keyframes[ki][1]);
|
|
}
|
|
cssTarget.textContent += rules.join("");
|
|
}
|
|
|
|
// Replace stub css primitive with real CSSX implementation
|
|
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 resolveStyle(atoms);
|
|
};
|
|
|
|
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];
|
|
return mergeStyleValues(valid);
|
|
};
|
|
"""
|
|
|
|
PLATFORM_BOOT_JS = """
|
|
// =========================================================================
|
|
// Platform interface — Boot (mount, hydrate, scripts, cookies)
|
|
// =========================================================================
|
|
|
|
function resolveMountTarget(target) {
|
|
if (typeof target === "string") return _hasDom ? document.querySelector(target) : null;
|
|
return target;
|
|
}
|
|
|
|
function sxRenderWithEnv(source, extraEnv) {
|
|
var env = extraEnv ? merge(componentEnv, extraEnv) : componentEnv;
|
|
var exprs = parse(source);
|
|
if (!_hasDom) return null;
|
|
var frag = document.createDocumentFragment();
|
|
for (var i = 0; i < exprs.length; i++) {
|
|
var node = renderToDom(exprs[i], env, null);
|
|
if (node) frag.appendChild(node);
|
|
}
|
|
return frag;
|
|
}
|
|
|
|
function getRenderEnv(extraEnv) {
|
|
return extraEnv ? merge(componentEnv, extraEnv) : componentEnv;
|
|
}
|
|
|
|
function mergeEnvs(base, newEnv) {
|
|
return newEnv ? merge(componentEnv, base, newEnv) : merge(componentEnv, base);
|
|
}
|
|
|
|
function sxLoadComponents(text) {
|
|
try {
|
|
var exprs = parse(text);
|
|
for (var i = 0; i < exprs.length; i++) trampoline(evalExpr(exprs[i], componentEnv));
|
|
} catch (err) {
|
|
logParseError("loadComponents", text, err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
function setDocumentTitle(s) {
|
|
if (_hasDom) document.title = s || "";
|
|
}
|
|
|
|
function removeHeadElement(sel) {
|
|
if (!_hasDom) return;
|
|
var old = document.head.querySelector(sel);
|
|
if (old) old.parentNode.removeChild(old);
|
|
}
|
|
|
|
function querySxScripts(root) {
|
|
if (!_hasDom) return [];
|
|
var r = (root && root !== NIL) ? root : document;
|
|
return Array.prototype.slice.call(
|
|
r.querySelectorAll('script[type="text/sx"]'));
|
|
}
|
|
|
|
function queryStyleScripts() {
|
|
if (!_hasDom) return [];
|
|
return Array.prototype.slice.call(
|
|
document.querySelectorAll('script[type="text/sx-styles"]'));
|
|
}
|
|
|
|
function queryPageScripts() {
|
|
if (!_hasDom) return [];
|
|
return Array.prototype.slice.call(
|
|
document.querySelectorAll('script[type="text/sx-pages"]'));
|
|
}
|
|
|
|
// --- localStorage ---
|
|
|
|
function localStorageGet(key) {
|
|
try { var v = localStorage.getItem(key); return v === null ? NIL : v; }
|
|
catch (e) { return NIL; }
|
|
}
|
|
|
|
function localStorageSet(key, val) {
|
|
try { localStorage.setItem(key, val); } catch (e) {}
|
|
}
|
|
|
|
function localStorageRemove(key) {
|
|
try { localStorage.removeItem(key); } catch (e) {}
|
|
}
|
|
|
|
// --- Cookies ---
|
|
|
|
function setSxCompCookie(hash) {
|
|
if (_hasDom) document.cookie = "sx-comp-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax";
|
|
}
|
|
|
|
function clearSxCompCookie() {
|
|
if (_hasDom) document.cookie = "sx-comp-hash=;path=/;max-age=0;SameSite=Lax";
|
|
}
|
|
|
|
function setSxStylesCookie(hash) {
|
|
if (_hasDom) document.cookie = "sx-styles-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax";
|
|
}
|
|
|
|
function clearSxStylesCookie() {
|
|
if (_hasDom) document.cookie = "sx-styles-hash=;path=/;max-age=0;SameSite=Lax";
|
|
}
|
|
|
|
// --- Env helpers ---
|
|
|
|
function parseEnvAttr(el) {
|
|
var attr = el && el.getAttribute ? el.getAttribute("data-sx-env") : null;
|
|
if (!attr) return {};
|
|
try { return JSON.parse(attr); } catch (e) { return {}; }
|
|
}
|
|
|
|
function storeEnvAttr(el, base, newEnv) {
|
|
var merged = merge(base, newEnv);
|
|
if (el && el.setAttribute) el.setAttribute("data-sx-env", JSON.stringify(merged));
|
|
}
|
|
|
|
function toKebab(s) { return s.replace(/_/g, "-"); }
|
|
|
|
// --- Logging ---
|
|
|
|
function logInfo(msg) {
|
|
if (typeof console !== "undefined") console.log("[sx-ref] " + msg);
|
|
}
|
|
|
|
function logWarn(msg) {
|
|
if (typeof console !== "undefined") console.warn("[sx-ref] " + msg);
|
|
}
|
|
|
|
function logParseError(label, text, err) {
|
|
if (typeof console === "undefined") return;
|
|
var msg = err && err.message ? err.message : String(err);
|
|
var colMatch = msg.match(/col (\\d+)/);
|
|
var lineMatch = msg.match(/line (\\d+)/);
|
|
if (colMatch && text) {
|
|
var errLine = lineMatch ? parseInt(lineMatch[1]) : 1;
|
|
var errCol = parseInt(colMatch[1]);
|
|
var lines = text.split("\\n");
|
|
var pos = 0;
|
|
for (var i = 0; i < errLine - 1 && i < lines.length; i++) pos += lines[i].length + 1;
|
|
pos += errCol;
|
|
var ws = 80;
|
|
var start = Math.max(0, pos - ws);
|
|
var end = Math.min(text.length, pos + ws);
|
|
console.error("[sx-ref] " + label + ":", msg,
|
|
"\\n around error (pos ~" + pos + "):",
|
|
"\\n \\u00ab" + text.substring(start, pos) + "\\u26d4" + text.substring(pos, end) + "\\u00bb");
|
|
} else {
|
|
console.error("[sx-ref] " + label + ":", msg);
|
|
}
|
|
}
|
|
|
|
function parseAndLoadStyleDict(text) {
|
|
try { loadStyleDict(JSON.parse(text)); }
|
|
catch (e) { if (typeof console !== "undefined") console.warn("[sx-ref] style dict parse error", e); }
|
|
}
|
|
"""
|
|
|
|
def fixups_js(has_html, has_sx, has_dom):
|
|
lines = ['''
|
|
// =========================================================================
|
|
// Post-transpilation fixups
|
|
// =========================================================================
|
|
// The reference spec's call-lambda only handles Lambda objects, but HO forms
|
|
// (map, reduce, etc.) may receive native primitives. Wrap to handle both.
|
|
var _rawCallLambda = callLambda;
|
|
callLambda = function(f, args, callerEnv) {
|
|
if (typeof f === "function") return f.apply(null, args);
|
|
return _rawCallLambda(f, args, callerEnv);
|
|
};
|
|
|
|
// Expose render functions as primitives so SX code can call them''']
|
|
if has_html:
|
|
lines.append(' if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml;')
|
|
if has_sx:
|
|
lines.append(' if (typeof renderToSx === "function") PRIMITIVES["render-to-sx"] = renderToSx;')
|
|
lines.append(' if (typeof aser === "function") PRIMITIVES["aser"] = aser;')
|
|
if has_dom:
|
|
lines.append(' if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;')
|
|
return "\n".join(lines)
|
|
|
|
|
|
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps=False, has_router=False):
|
|
# Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
|
|
if has_parser:
|
|
parser = '''
|
|
// Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes)
|
|
var parse = sxParse;'''
|
|
else:
|
|
parser = r'''
|
|
// Minimal fallback parser (no parser adapter)
|
|
function parse(text) {
|
|
throw new Error("Parser adapter not included — cannot parse SX source at runtime");
|
|
}'''
|
|
|
|
# Public API — conditional on adapters
|
|
api_lines = [parser, '''
|
|
// =========================================================================
|
|
// Public API
|
|
// =========================================================================
|
|
|
|
var componentEnv = {};
|
|
|
|
function loadComponents(source) {
|
|
var exprs = parse(source);
|
|
for (var i = 0; i < exprs.length; i++) {
|
|
trampoline(evalExpr(exprs[i], componentEnv));
|
|
}
|
|
}''']
|
|
|
|
# render() — auto-dispatches based on available adapters
|
|
if has_html and has_dom:
|
|
api_lines.append('''
|
|
function render(source) {
|
|
if (!_hasDom) {
|
|
var exprs = parse(source);
|
|
var parts = [];
|
|
for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv)));
|
|
return parts.join("");
|
|
}
|
|
var exprs = parse(source);
|
|
var frag = document.createDocumentFragment();
|
|
for (var i = 0; i < exprs.length; i++) frag.appendChild(renderToDom(exprs[i], merge(componentEnv), null));
|
|
return frag;
|
|
}''')
|
|
elif has_dom:
|
|
api_lines.append('''
|
|
function render(source) {
|
|
var exprs = parse(source);
|
|
var frag = document.createDocumentFragment();
|
|
for (var i = 0; i < exprs.length; i++) frag.appendChild(renderToDom(exprs[i], merge(componentEnv), null));
|
|
return frag;
|
|
}''')
|
|
elif has_html:
|
|
api_lines.append('''
|
|
function render(source) {
|
|
var exprs = parse(source);
|
|
var parts = [];
|
|
for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv)));
|
|
return parts.join("");
|
|
}''')
|
|
else:
|
|
api_lines.append('''
|
|
function render(source) {
|
|
var exprs = parse(source);
|
|
var results = [];
|
|
for (var i = 0; i < exprs.length; i++) results.push(trampoline(evalExpr(exprs[i], merge(componentEnv))));
|
|
return results.length === 1 ? results[0] : results;
|
|
}''')
|
|
|
|
# renderToString helper
|
|
if has_html:
|
|
api_lines.append('''
|
|
function renderToString(source) {
|
|
var exprs = parse(source);
|
|
var parts = [];
|
|
for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv)));
|
|
return parts.join("");
|
|
}''')
|
|
|
|
# Build Sx object
|
|
version = f"ref-2.0 ({adapter_label}, bootstrap-compiled)"
|
|
api_lines.append(f'''
|
|
var Sx = {{
|
|
VERSION: "ref-2.0",
|
|
parse: parse,
|
|
parseAll: parse,
|
|
eval: function(expr, env) {{ return trampoline(evalExpr(expr, env || merge(componentEnv))); }},
|
|
loadComponents: loadComponents,
|
|
render: render,{"" if has_html else ""}
|
|
{"renderToString: renderToString," if has_html else ""}
|
|
serialize: serialize,
|
|
NIL: NIL,
|
|
Symbol: Symbol,
|
|
Keyword: Keyword,
|
|
isTruthy: isSxTruthy,
|
|
isNil: isNil,
|
|
componentEnv: componentEnv,''')
|
|
|
|
if has_html:
|
|
api_lines.append(' renderToHtml: function(expr, env) { return renderToHtml(expr, env || merge(componentEnv)); },')
|
|
if has_sx:
|
|
api_lines.append(' renderToSx: function(expr, env) { return renderToSx(expr, env || merge(componentEnv)); },')
|
|
if has_dom:
|
|
api_lines.append(' renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null,')
|
|
if has_engine:
|
|
api_lines.append(' parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null,')
|
|
api_lines.append(' morphNode: typeof morphNode === "function" ? morphNode : null,')
|
|
api_lines.append(' morphChildren: typeof morphChildren === "function" ? morphChildren : null,')
|
|
api_lines.append(' swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,')
|
|
if has_orch:
|
|
api_lines.append(' process: typeof processElements === "function" ? processElements : null,')
|
|
api_lines.append(' executeRequest: typeof executeRequest === "function" ? executeRequest : null,')
|
|
api_lines.append(' postSwap: typeof postSwap === "function" ? postSwap : null,')
|
|
if has_boot:
|
|
api_lines.append(' processScripts: typeof processSxScripts === "function" ? processSxScripts : null,')
|
|
api_lines.append(' mount: typeof sxMount === "function" ? sxMount : null,')
|
|
api_lines.append(' hydrate: typeof sxHydrateElements === "function" ? sxHydrateElements : null,')
|
|
api_lines.append(' update: typeof sxUpdateElement === "function" ? sxUpdateElement : null,')
|
|
api_lines.append(' renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,')
|
|
api_lines.append(' getEnv: function() { return componentEnv; },')
|
|
api_lines.append(' init: typeof bootInit === "function" ? bootInit : null,')
|
|
elif has_orch:
|
|
api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,')
|
|
if has_deps:
|
|
api_lines.append(' scanRefs: scanRefs,')
|
|
api_lines.append(' transitiveDeps: transitiveDeps,')
|
|
api_lines.append(' computeAllDeps: computeAllDeps,')
|
|
api_lines.append(' componentsNeeded: componentsNeeded,')
|
|
api_lines.append(' pageComponentBundle: pageComponentBundle,')
|
|
api_lines.append(' pageCssClasses: pageCssClasses,')
|
|
api_lines.append(' scanIoRefs: scanIoRefs,')
|
|
api_lines.append(' transitiveIoRefs: transitiveIoRefs,')
|
|
api_lines.append(' computeAllIoRefs: computeAllIoRefs,')
|
|
api_lines.append(' componentPure_p: componentPure_p,')
|
|
if has_router:
|
|
api_lines.append(' splitPathSegments: splitPathSegments,')
|
|
api_lines.append(' parseRoutePattern: parseRoutePattern,')
|
|
api_lines.append(' matchRoute: matchRoute,')
|
|
api_lines.append(' findMatchingRoute: findMatchingRoute,')
|
|
|
|
api_lines.append(f' _version: "{version}"')
|
|
api_lines.append(' };')
|
|
api_lines.append('')
|
|
if has_orch:
|
|
api_lines.append('''
|
|
// --- Popstate listener ---
|
|
if (typeof window !== "undefined") {
|
|
window.addEventListener("popstate", function(e) {
|
|
handlePopstate(e && e.state ? e.state.scrollY || 0 : 0);
|
|
});
|
|
}''')
|
|
if has_boot:
|
|
api_lines.append('''
|
|
// --- Auto-init ---
|
|
if (typeof document !== "undefined") {
|
|
var _sxInit = function() { bootInit(); };
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", _sxInit);
|
|
} else {
|
|
_sxInit();
|
|
}
|
|
}''')
|
|
elif has_orch:
|
|
api_lines.append('''
|
|
// --- Auto-init ---
|
|
if (typeof document !== "undefined") {
|
|
var _sxInit = function() { engineInit(); };
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", _sxInit);
|
|
} else {
|
|
_sxInit();
|
|
}
|
|
}''')
|
|
|
|
api_lines.append(' if (typeof module !== "undefined" && module.exports) module.exports = Sx;')
|
|
api_lines.append(' else global.Sx = Sx;')
|
|
|
|
return "\n".join(api_lines)
|
|
|
|
EPILOGUE = '''
|
|
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);'''
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
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("--spec-modules",
|
|
help="Comma-separated spec modules (deps). Default: none.")
|
|
p.add_argument("--output", "-o",
|
|
help="Output file (default: stdout)")
|
|
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
|
|
spec_modules = args.spec_modules.split(",") if args.spec_modules else None
|
|
js = compile_ref_to_js(adapters, modules, extensions, spec_modules)
|
|
|
|
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}, modules: {mods}, extensions: {ext_label})",
|
|
file=sys.stderr)
|
|
else:
|
|
print(js)
|