diff --git a/sx/sx/layouts.sx b/sx/sx/layouts.sx index 0cc5f2b..e2c6917 100644 --- a/sx/sx/layouts.sx +++ b/sx/sx/layouts.sx @@ -12,7 +12,7 @@ (dict :label "Protocols" :href "/protocols/wire-format") (dict :label "Examples" :href "/examples/click-to-load") (dict :label "Essays" :href "/essays/sx-sucks") - (dict :label "Specs" :href "/specs/core")))) + (dict :label "Specs" :href "/specs/")))) (<> (map (lambda (item) (~nav-link :href (get item "href") diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 162d746..0f61bd8 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -68,11 +68,17 @@ (dict :label "The Reflexive Web" :href "/essays/reflexive-web"))) (define specs-nav-items (list + (dict :label "Architecture" :href "/specs/") (dict :label "Core" :href "/specs/core") (dict :label "Parser" :href "/specs/parser") (dict :label "Evaluator" :href "/specs/evaluator") (dict :label "Primitives" :href "/specs/primitives") - (dict :label "Renderer" :href "/specs/renderer"))) + (dict :label "Renderer" :href "/specs/renderer") + (dict :label "Adapters" :href "/specs/adapters") + (dict :label "DOM Adapter" :href "/specs/adapter-dom") + (dict :label "HTML Adapter" :href "/specs/adapter-html") + (dict :label "SX Wire Adapter" :href "/specs/adapter-sx") + (dict :label "SxEngine" :href "/specs/engine"))) ;; Find the current nav label for a slug by matching href suffix. ;; Returns the label string or nil if no match. diff --git a/sx/sx/specs.sx b/sx/sx/specs.sx index 6e83467..4f8bb8b 100644 --- a/sx/sx/specs.sx +++ b/sx/sx/specs.sx @@ -1,9 +1,157 @@ ;; Spec viewer components — display canonical SX specification source -(defcomp ~spec-core-content (&key spec-files) - (~doc-page :title "SX Core Specification" +;; --------------------------------------------------------------------------- +;; Architecture intro page +;; --------------------------------------------------------------------------- + +(defcomp ~spec-architecture-content () + (~doc-page :title "Spec Architecture" + (div :class "space-y-8" + + (div :class "space-y-4" + (p :class "text-lg text-stone-600" + "SX is defined in SX. The canonical specification is a set of s-expression files that are both documentation and executable definition. Bootstrap compilers read these files to generate native implementations in JavaScript, Python, Rust, or any other target.") + (p :class "text-stone-600" + "The spec is split into two layers: a " + (strong "core") " that defines the language itself, and " + (strong "adapters") " that connect it to specific environments.")) + + (div :class "space-y-3" + (h2 :class "text-2xl font-semibold text-stone-800" "Core") + (p :class "text-stone-600" + "The core is platform-independent. It defines how SX source is parsed, how expressions are evaluated, what primitives exist, and what shared rendering definitions all adapters use. These four files are the language.") + (div :class "overflow-x-auto rounded border border-stone-200" + (table :class "w-full text-left text-sm" + (thead (tr :class "border-b border-stone-200 bg-stone-100" + (th :class "px-3 py-2 font-medium text-stone-600" "File") + (th :class "px-3 py-2 font-medium text-stone-600" "Role"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-mono text-sm text-violet-700" + (a :href "/specs/parser" :class "hover:underline" + :sx-get "/specs/parser" :sx-target "#main-panel" :sx-select "#main-panel" + :sx-swap "outerHTML" :sx-push-url "true" + "parser.sx")) + (td :class "px-3 py-2 text-stone-700" "Tokenization and parsing of SX source text into AST")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-mono text-sm text-violet-700" + (a :href "/specs/evaluator" :class "hover:underline" + :sx-get "/specs/evaluator" :sx-target "#main-panel" :sx-select "#main-panel" + :sx-swap "outerHTML" :sx-push-url "true" + "eval.sx")) + (td :class "px-3 py-2 text-stone-700" "Tree-walking evaluation of SX expressions")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-mono text-sm text-violet-700" + (a :href "/specs/primitives" :class "hover:underline" + :sx-get "/specs/primitives" :sx-target "#main-panel" :sx-select "#main-panel" + :sx-swap "outerHTML" :sx-push-url "true" + "primitives.sx")) + (td :class "px-3 py-2 text-stone-700" "All built-in pure functions and their signatures")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-mono text-sm text-violet-700" + (a :href "/specs/renderer" :class "hover:underline" + :sx-get "/specs/renderer" :sx-target "#main-panel" :sx-select "#main-panel" + :sx-swap "outerHTML" :sx-push-url "true" + "render.sx")) + (td :class "px-3 py-2 text-stone-700" "Shared rendering registries and utilities used by all adapters")))))) + + (div :class "space-y-3" + (h2 :class "text-2xl font-semibold text-stone-800" "Adapters") + (p :class "text-stone-600" + "Adapters are selectable rendering backends. Each one takes the same evaluated expression tree and produces output for a specific environment. You only need the adapters relevant to your target.") + (div :class "overflow-x-auto rounded border border-stone-200" + (table :class "w-full text-left text-sm" + (thead (tr :class "border-b border-stone-200 bg-stone-100" + (th :class "px-3 py-2 font-medium text-stone-600" "File") + (th :class "px-3 py-2 font-medium text-stone-600" "Output") + (th :class "px-3 py-2 font-medium text-stone-600" "Environment"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-mono text-sm text-violet-700" + (a :href "/specs/adapter-dom" :class "hover:underline" + :sx-get "/specs/adapter-dom" :sx-target "#main-panel" :sx-select "#main-panel" + :sx-swap "outerHTML" :sx-push-url "true" + "adapter-dom.sx")) + (td :class "px-3 py-2 text-stone-700" "Live DOM nodes") + (td :class "px-3 py-2 text-stone-500" "Browser")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-mono text-sm text-violet-700" + (a :href "/specs/adapter-html" :class "hover:underline" + :sx-get "/specs/adapter-html" :sx-target "#main-panel" :sx-select "#main-panel" + :sx-swap "outerHTML" :sx-push-url "true" + "adapter-html.sx")) + (td :class "px-3 py-2 text-stone-700" "HTML strings") + (td :class "px-3 py-2 text-stone-500" "Server")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-mono text-sm text-violet-700" + (a :href "/specs/adapter-sx" :class "hover:underline" + :sx-get "/specs/adapter-sx" :sx-target "#main-panel" :sx-select "#main-panel" + :sx-swap "outerHTML" :sx-push-url "true" + "adapter-sx.sx")) + (td :class "px-3 py-2 text-stone-700" "SX wire format") + (td :class "px-3 py-2 text-stone-500" "Server to client")))))) + + (div :class "space-y-3" + (h2 :class "text-2xl font-semibold text-stone-800" "Engine") + (p :class "text-stone-600" + "The engine is the browser-side fetch/swap/history system. It processes " + (code :class "text-violet-700 text-sm" "sx-*") + " attributes on elements to make HTTP requests, swap content, manage browser history, and handle events. It depends on the core evaluator and the DOM adapter.") + (div :class "overflow-x-auto rounded border border-stone-200" + (table :class "w-full text-left text-sm" + (thead (tr :class "border-b border-stone-200 bg-stone-100" + (th :class "px-3 py-2 font-medium text-stone-600" "File") + (th :class "px-3 py-2 font-medium text-stone-600" "Role"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-mono text-sm text-violet-700" + (a :href "/specs/engine" :class "hover:underline" + :sx-get "/specs/engine" :sx-target "#main-panel" :sx-select "#main-panel" + :sx-swap "outerHTML" :sx-push-url "true" + "engine.sx")) + (td :class "px-3 py-2 text-stone-700" "SxEngine — fetch, swap, history, SSE, triggers, indicators")))))) + + (div :class "space-y-3" + (h2 :class "text-2xl font-semibold text-stone-800" "Dependency graph") + (div :class "bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl" + (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words font-mono text-stone-700" +"parser.sx (standalone — no dependencies) +primitives.sx (standalone — declarative registry) +eval.sx depends on: parser, primitives +render.sx (standalone — shared registries) + +adapter-dom.sx depends on: render, eval +adapter-html.sx depends on: render, eval +adapter-sx.sx depends on: render, eval + +engine.sx depends on: eval, adapter-dom"))) + + (div :class "space-y-3" + (h2 :class "text-2xl font-semibold text-stone-800" "Self-hosting") + (p :class "text-stone-600" + "Every spec file is written in the same restricted subset of SX that the evaluator itself defines. A bootstrap compiler for a new target only needs to understand this subset — roughly 20 special forms and 80 primitives — to generate a fully native implementation. The spec files are the single source of truth; implementations are derived artifacts.") + (p :class "text-stone-600" + "This is not a theoretical exercise. The JavaScript implementation (" + (code :class "text-violet-700 text-sm" "sx.js") + ") and the Python implementation (" + (code :class "text-violet-700 text-sm" "shared/sx/") + ") are both generated from these spec files via " + (code :class "text-violet-700 text-sm" "bootstrap_js.py") + " and its Python counterpart."))))) + +;; --------------------------------------------------------------------------- +;; Overview pages (Core / Adapters) — show truncated previews of each file +;; --------------------------------------------------------------------------- + +(defcomp ~spec-overview-content (&key spec-files) + (~doc-page :title (or spec-title "Specs") (p :class "text-stone-600 mb-6" - "SX is defined in SX. These four files constitute the canonical, self-hosting specification of the language. Each file is both documentation and executable definition — bootstrap compilers read them to generate native implementations.") + (case spec-title + "Core Language" + "The core specification defines the language itself — parsing, evaluation, primitives, and shared rendering definitions. These four files are platform-independent and sufficient to implement SX on any target." + "Adapters & Engine" + "Adapters connect the core language to specific environments. Each adapter takes evaluated expression trees and produces output for its target. The engine adds browser-side fetch/swap behaviour." + :else "")) (div :class "space-y-8" (map (fn (spec) (div :class "space-y-3" @@ -21,6 +169,10 @@ (code (highlight (get spec "source") "sx")))))) spec-files)))) +;; --------------------------------------------------------------------------- +;; Detail page — full source of a single spec file +;; --------------------------------------------------------------------------- + (defcomp ~spec-detail-content (&key spec-title spec-desc spec-filename spec-source) (~doc-page :title spec-title (div :class "flex items-baseline gap-3 mb-4" @@ -30,6 +182,10 @@ (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code (highlight spec-source "sx")))))) +;; --------------------------------------------------------------------------- +;; Not found +;; --------------------------------------------------------------------------- + (defcomp ~spec-not-found (&key slug) (~doc-page :title "Spec Not Found" (p :class "text-stone-600" diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index 6f46692..9041617 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -253,11 +253,10 @@ :layout (:sx-section :section "Specs" :sub-label "Specs" - :sub-href "/specs/core" - :sub-nav (~section-nav :items specs-nav-items :current "Core") - :selected "Core") - :data (spec-data "core") - :content (~spec-core-content :spec-files spec-files)) + :sub-href "/specs/" + :sub-nav (~section-nav :items specs-nav-items :current "Architecture") + :selected "Architecture") + :content (~spec-architecture-content)) (defpage specs-page :path "/specs/" @@ -265,7 +264,7 @@ :layout (:sx-section :section "Specs" :sub-label "Specs" - :sub-href "/specs/core" + :sub-href "/specs/" :sub-nav (~section-nav :items specs-nav-items :current (find-current specs-nav-items slug)) :selected (or (find-current specs-nav-items slug) "")) @@ -273,7 +272,8 @@ :content (if spec-not-found (~spec-not-found :slug slug) (case slug - "core" (~spec-core-content :spec-files spec-files) + "core" (~spec-overview-content :spec-files spec-files) + "adapters" (~spec-overview-content :spec-files spec-files) :else (~spec-detail-content :spec-title spec-title :spec-desc spec-desc diff --git a/sx/sxc/pages/helpers.py b/sx/sxc/pages/helpers.py index f7ce796..1d0bec5 100644 --- a/sx/sxc/pages/helpers.py +++ b/sx/sxc/pages/helpers.py @@ -104,21 +104,28 @@ def _reference_data(slug: str) -> dict: } -_SPEC_FILES = { +_CORE_SPECS = { "parser": ("parser.sx", "Parser", "Tokenization and parsing of SX source text into AST."), "evaluator": ("eval.sx", "Evaluator", "Tree-walking evaluation of SX expressions."), "primitives": ("primitives.sx", "Primitives", "All built-in pure functions and their signatures."), - "renderer": ("render.sx", "Renderer", "Rendering evaluated expressions to DOM, HTML, or SX wire format."), + "renderer": ("render.sx", "Renderer", "Shared rendering registries and utilities used by all adapters."), } +_ADAPTER_SPECS = { + "adapter-dom": ("adapter-dom.sx", "DOM Adapter", "Renders SX expressions to live DOM nodes. Browser-only."), + "adapter-html": ("adapter-html.sx", "HTML Adapter", "Renders SX expressions to HTML strings. Server-side."), + "adapter-sx": ("adapter-sx.sx", "SX Wire Adapter", "Serializes SX for client-side rendering. Component calls stay unexpanded."), + "engine": ("engine.sx", "SxEngine", "Fetch/swap/history engine for browser-side SX. Like HTMX but native to SX."), +} + +_ALL_SPECS = {**_CORE_SPECS, **_ADAPTER_SPECS} + def _spec_data(slug: str) -> dict: - """Return spec file source and highlighted version for display.""" + """Return spec file source and metadata for display.""" import os - from content.highlight import highlight as _highlight ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref") - # Normalise — inside container shared is at /app/shared if not os.path.isdir(ref_dir): ref_dir = "/app/shared/sx/ref" @@ -128,19 +135,28 @@ def _spec_data(slug: str) -> dict: if slug == "core": specs = [] for key in ("parser", "evaluator", "primitives", "renderer"): - filename, title, desc = _SPEC_FILES[key] + filename, title, desc = _CORE_SPECS[key] filepath = os.path.join(ref_dir, filename) source = _read_spec(filepath) specs.append({ - "title": title, - "desc": desc, - "filename": filename, - "source": source, - "href": f"/specs/{key}", + "title": title, "desc": desc, "filename": filename, + "source": source, "href": f"/specs/{key}", }) - return {**base, "spec-title": "SX Core Specification", "spec-files": specs} + return {**base, "spec-title": "Core Language", "spec-files": specs} - info = _SPEC_FILES.get(slug) + if slug == "adapters": + specs = [] + for key in ("adapter-dom", "adapter-html", "adapter-sx", "engine"): + filename, title, desc = _ADAPTER_SPECS[key] + filepath = os.path.join(ref_dir, filename) + source = _read_spec(filepath) + specs.append({ + "title": title, "desc": desc, "filename": filename, + "source": source, "href": f"/specs/{key}", + }) + return {**base, "spec-title": "Adapters & Engine", "spec-files": specs} + + info = _ALL_SPECS.get(slug) if not info: return {**base, "spec-not-found": True}