From acd2fa65412282b858457d2023549202144b7831 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 12 Mar 2026 18:54:33 +0000 Subject: [PATCH] Add SX URLs documentation page, fix layout strapline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New comprehensive documentation for SX URLs at /(applications.(sx-urls)) covering dots-as-spaces, nesting/scoping, relative URLs, keyword ops, delta values, special forms, hypermedia integration, and GraphSX. Fix layout tagline: "A" → "The" framework-free reactive hypermedium. Co-Authored-By: Claude Opus 4.6 --- sx/sx/layouts.sx | 2 +- sx/sx/nav-data.sx | 1 + sx/sx/page-functions.sx | 5 + sx/sx/sx-urls.sx | 442 ++++++++++++++++++++++++++++++++++++++ sx/sxc/pages/sx_router.py | 1 + 5 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 sx/sx/sx-urls.sx diff --git a/sx/sx/layouts.sx b/sx/sx/layouts.sx index cc2f068..e21cd13 100644 --- a/sx/sx/layouts.sx +++ b/sx/sx/layouts.sx @@ -34,7 +34,7 @@ "()"))) ;; Tagline — clicking "reactive" cycles colour. (p :style (str (mb 1) (cssx (:text (colour "stone" 500) (size "lg")))) - "A framework-free " + "The framework-free " (span :style (str (cssx (:text (colour (deref current-family) (deref shade)) (weight "bold"))) diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 87ceaa3..0cb8501 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -379,6 +379,7 @@ {:label "Testing" :href "/(language.(test))" :children testing-nav-items})} {:label "Applications" :href "/(applications)" :children (list + {:label "SX URLs" :href "/(applications.(sx-urls))"} {:label "CSSX" :href "/(applications.(cssx))" :children cssx-nav-items} {:label "Protocols" :href "/(applications.(protocol))" :children protocols-nav-items})} {:label "Etc" :href "/(etc)" diff --git a/sx/sx/page-functions.sx b/sx/sx/page-functions.sx index f894677..ae98837 100644 --- a/sx/sx/page-functions.sx +++ b/sx/sx/page-functions.sx @@ -415,6 +415,11 @@ "retry" '(~example-retry) :else '(~example-click-to-load))))) +;; SX URLs (under applications) +(define sx-urls + (fn (slug) + '(~sx-urls-content))) + ;; CSSX (under applications) (define cssx (fn (slug) diff --git a/sx/sx/sx-urls.sx b/sx/sx/sx-urls.sx new file mode 100644 index 0000000..96138f9 --- /dev/null +++ b/sx/sx/sx-urls.sx @@ -0,0 +1,442 @@ +;; SX URLs — Comprehensive documentation for s-expression URL addressing. +;; Lives under the Applications section: /(applications.(sx-urls)) + +;; --------------------------------------------------------------------------- +;; Main documentation page +;; --------------------------------------------------------------------------- + +(defcomp ~sx-urls-content () + (~doc-page :title "SX URLs" + (p :class "text-stone-500 text-sm italic mb-8" + "S-expression URLs — where the address of a thing and the thing itself are the same kind of thing.") + + ;; ----------------------------------------------------------------- + (~doc-section :title "What Is an SX URL?" :id "what" + (p "An SX URL is a URL whose path is an s-expression. Instead of " + "a flat sequence of slash-separated path segments, the URL encodes " + "a nested function call that the server evaluates to produce a page.") + (p "Every page on this site is addressed by an SX URL. You are currently reading:") + (~doc-code :code (highlight + "/(applications.(sx-urls))" + "lisp")) + (p "This is a function call: " (code "applications") " is called with the result of " + (code "(sx-urls)") ". The server evaluates it, wraps the content in a layout, " + "and returns the page. " + "The URL is simultaneously a query, a render instruction, and an address.")) + + ;; ----------------------------------------------------------------- + (~doc-section :title "Dots as Spaces" :id "dots" + (p "Spaces in URLs are ugly — they become " (code "%20") " in the address bar, " + "break when copy-pasted into terminals, and confuse proxies. " + "Dots are unreserved in " + (a :href "https://www.rfc-editor.org/rfc/rfc3986" :class "text-violet-600 hover:underline" "RFC 3986") + ", never percent-encoded, and read naturally.") + (p "The rule: " (strong "dot = space, nothing more") ". " + "Before parsing, the server replaces every dot with a space. " + "Parens carry all structural meaning.") + (~doc-code :code (highlight + ";; What you type in the browser:\n/(language.(doc.introduction))\n\n;; After dot→space transform:\n(language (doc introduction))\n\n;; This is standard SX. Parens are nesting. Atoms are arguments.\n;; 'introduction' is a string slug, 'doc' is a function, 'language' is a function." + "lisp")) + + (p "More examples:") + (~doc-code :code (highlight + ";; Geography section, hypermedia subsection, examples area, progress bar page\n/(geography.(hypermedia.(example.progress-bar)))\n→ (geography (hypermedia (example progress-bar)))\n\n;; Language section, spec subsection, signals page\n/(language.(spec.signals))\n→ (language (spec signals))\n\n;; Etc section, essay subsection, specific essay\n/(etc.(essay.sx-sucks))\n→ (etc (essay sx-sucks))" + "lisp")) + + (p "The dot-to-space transform is the " (em "only") " URL-specific syntax. " + "Everything else is standard s-expression parsing.")) + + ;; ----------------------------------------------------------------- + (~doc-section :title "Nesting Is Scoping" :id "nesting" + (p "REST URLs have an inherent ambiguity: " + "does a filter apply to the last segment, or the whole path? " + "S-expression nesting makes scope explicit.") + (~doc-code :code (highlight + ";; REST — ambiguous:\n/users/123/posts?filter=published\n;; Does 'filter' apply to posts? To the user? To the whole query?\n;; The answer depends on API documentation.\n\n;; SX URLs — explicit scoping:\n/(users.(posts.123.(filter.published))) ;; filter scoped to posts\n/(users.posts.123.(filter.published)) ;; filter scoped to the whole expression\n\n;; These are structurally different. The paren boundaries ARE scope boundaries.\n;; No documentation needed — the syntax tells you." + "lisp")) + + (p "This extends to every level of nesting on this site:") + (~doc-code :code (highlight + ";; These all have different scoping:\n/(language.(spec.signals)) ;; 'signals' scoped to spec\n/(language.(spec.(explore.signals))) ;; 'signals' scoped to explore\n/(language.(spec.(explore.signals.:section.\"batch\"))) ;; keyword scoped to explore call" + "lisp")) + (p "What took REST thirty years of convention documents to approximate, " + "SX URLs express in the syntax itself.")) + + ;; ----------------------------------------------------------------- + (~doc-section :title "The URL Hierarchy" :id "hierarchy" + (p "Every URL on this site follows a hierarchical structure. " + "Sections are functions that receive the result of their inner expressions.") + + (~doc-subsection :title "Section functions" + (p "Top-level sections — " (code "geography") ", " (code "language") ", " + (code "applications") ", " (code "etc") " — are structural. " + "Called with no arguments, they return their index page. " + "Called with content, they pass it through:") + (~doc-code :code (highlight + "/(geography) ;; geography section index\n/(language) ;; language section index\n/(applications) ;; applications section index\n/(etc) ;; etc section index" + "lisp"))) + + (~doc-subsection :title "Sub-sections" + (p "Sub-sections nest inside sections:") + (~doc-code :code (highlight + "/(geography.(hypermedia)) ;; hypermedia sub-section index\n/(geography.(reactive)) ;; reactive islands sub-section\n/(geography.(isomorphism)) ;; isomorphism sub-section\n/(language.(doc)) ;; documentation sub-section\n/(language.(spec)) ;; specification sub-section\n/(language.(bootstrapper)) ;; bootstrappers sub-section\n/(applications.(cssx)) ;; CSSX sub-section\n/(applications.(protocol)) ;; protocols sub-section" + "lisp"))) + + (~doc-subsection :title "Leaf pages" + (p "Leaf pages are the innermost function calls. " + "The slug becomes a string argument to the page function:") + (~doc-code :code (highlight + ";; Documentation pages\n/(language.(doc.introduction)) ;; doc(\"introduction\")\n/(language.(doc.getting-started)) ;; doc(\"getting-started\")\n/(language.(doc.components)) ;; doc(\"components\")\n/(language.(doc.evaluator)) ;; doc(\"evaluator\")\n/(language.(doc.primitives)) ;; doc(\"primitives\")\n/(language.(doc.special-forms)) ;; doc(\"special-forms\")\n/(language.(doc.server-rendering)) ;; doc(\"server-rendering\")\n\n;; Specification pages\n/(language.(spec.core)) ;; spec(\"core\")\n/(language.(spec.parser)) ;; spec(\"parser\")\n/(language.(spec.evaluator)) ;; spec(\"evaluator\")\n/(language.(spec.signals)) ;; spec(\"signals\")\n\n;; Deeply nested pages\n/(language.(spec.(explore.signals))) ;; explore(\"signals\") inside spec\n/(geography.(hypermedia.(example.progress-bar))) ;; example(\"progress-bar\")\n/(geography.(hypermedia.(reference.attributes))) ;; reference(\"attributes\")" + "lisp"))) + + (~doc-subsection :title "All working URLs on this site" + (p "Every link in the navigation tree is a live SX URL. Here is a sample:") + (~doc-code :code (highlight + ";; Geography\n/(geography.(reactive)) ;; reactive islands overview\n/(geography.(reactive.demo)) ;; live demo\n/(geography.(hypermedia.(reference.attributes))) ;; attribute reference\n/(geography.(hypermedia.(example.click-to-load))) ;; click-to-load example\n/(geography.(hypermedia.(example.infinite-scroll))) ;; infinite scroll\n/(geography.(isomorphism.bundle-analyzer)) ;; bundle analyzer\n/(geography.(isomorphism.streaming)) ;; streaming demo\n\n;; Language\n/(language.(doc.introduction)) ;; getting started\n/(language.(spec.core)) ;; core spec\n/(language.(spec.(explore.signals))) ;; spec explorer: signals\n/(language.(bootstrapper.python)) ;; python bootstrapper\n/(language.(bootstrapper.self-hosting)) ;; self-hosting bootstrapper\n/(language.(test.eval)) ;; evaluator tests\n/(language.(test.router)) ;; router tests\n\n;; Applications\n/(applications.(cssx)) ;; CSSX overview\n/(applications.(cssx.patterns)) ;; CSSX patterns\n/(applications.(protocol.wire-format)) ;; wire format protocol\n/(applications.(sx-urls)) ;; this page\n\n;; Etc\n/(etc.(essay.sx-sucks)) ;; the honest critique\n/(etc.(essay.self-defining-medium)) ;; the metacircular web\n/(etc.(philosophy.sx-manifesto)) ;; the manifesto\n/(etc.(plan.spec-explorer)) ;; spec explorer plan" + "lisp")))) + + ;; ----------------------------------------------------------------- + (~doc-section :title "Direct Component URLs" :id "direct" + (p "Every " (code "defcomp") " in the component environment is directly " + "addressable by its " (code "~name") " — no page function, no routing wiring, no case statement.") + (~doc-code :code (highlight + ";; Any component is instantly a URL:\n/(~essay-sx-sucks) ;; renders the essay component\n/(~plan-sx-urls-content) ;; this documentation page\n/(~docs-evaluator-content) ;; the evaluator docs\n/(~bundle-analyzer-content) ;; the bundle analyzer tool\n/(~reactive-islands-demo-content) ;; the reactive demo\n\n;; This is not a convenience — it's a consequence of the architecture.\n;; Components are values. Values are addressable. The URL evaluator\n;; sees ~name, looks it up in env, evaluates it, wraps in ~sx-doc." + "lisp")) + (p "New components are URL-accessible the moment they are defined. " + "No registration, no routing table update, no deploy. " + "Define a " (code "defcomp") ", refresh the page, visit " (code "/(~your-component)") ".")) + + ;; ----------------------------------------------------------------- + (~doc-section :title "Relative URLs" :id "relative" + (p "SX URLs support a structural algebra for relative navigation. " + "Instead of manipulating path strings, you navigate the nesting hierarchy. " + "The leading dots tell you how many levels to move.") + + (~doc-subsection :title "Same level: one dot" + (p "A single dot means \"at the current nesting level\" — append a sibling:") + (~doc-code :code (highlight + ";; Current page: /(geography.(hypermedia.(example.progress-bar)))\n\n(.click-to-load)\n;; → /(geography.(hypermedia.(example.progress-bar.click-to-load)))\n;; Appends 'click-to-load' at the same level as 'progress-bar'\n\n;; In a link:\n(a :href \"(.click-to-load)\" \"Next: Click to Load\")\n;; The client resolves this relative to the current page" + "lisp"))) + + (~doc-subsection :title "Up one level: two dots" + (p "Two dots pop one level of nesting — like " (code "cd ..") " in a filesystem:") + (~doc-code :code (highlight + ";; Current: /(geography.(hypermedia.(example.progress-bar)))\n\n(..)\n;; → /(geography.(hypermedia.(example)))\n;; Strips the innermost expression, keeps the rest\n\n;; Up and over — pop then append:\n(..inline-edit)\n;; → /(geography.(hypermedia.(example.inline-edit)))\n;; Pop 'progress-bar', append 'inline-edit'\n\n;; Navigate to a sibling sub-section:\n(..reference)\n;; → /(geography.(hypermedia.(reference)))\n;; Pop the 'example' level, land on 'reference'" + "lisp"))) + + (~doc-subsection :title "Up multiple levels" + (p "More dots pop more levels. N dots = pop N-1 levels:") + (~doc-code :code (highlight + ";; Current: /(geography.(hypermedia.(example.progress-bar)))\n\n(...)\n;; → /(geography.(hypermedia))\n;; 3 dots = pop 2 levels (example + progress-bar)\n\n(....)\n;; → /(geography)\n;; 4 dots = pop 3 levels\n\n(.....)\n;; → /\n;; 5 dots = pop 4 levels (all the way to root)" + "lisp"))) + + (~doc-subsection :title "Up and sideways" + (p "Combine dots with a slug to navigate up, then into a different branch:") + (~doc-code :code (highlight + ";; Current: /(language.(spec.(explore.signals)))\n\n(..eval)\n;; → /(language.(spec.eval))\n;; Pop one level (explore.signals → spec), append eval\n\n(...doc.introduction)\n;; → /(language.(doc.introduction))\n;; Pop two levels (to language), descend into doc.introduction\n\n(....geography.(reactive.demo))\n;; → /(geography.(reactive.demo))\n;; Pop three levels (to root), into a completely different section" + "lisp"))) + + (~doc-subsection :title "Bare-dot shorthand" + (p "For brevity, the outer parentheses are optional. " + "A URL starting with " (code ".") " is automatically wrapped:") + (~doc-code :code (highlight + ";; These pairs are identical:\n.click-to-load → (.click-to-load)\n.. → (..)\n..inline-edit → (..inline-edit)\n... → (...)\n\n;; In practice:\n(a :href \"..inline-edit\" \"Inline Edit\")\n;; is more natural than:\n(a :href \"(..inline-edit)\" \"Inline Edit\")" + "lisp")))) + + ;; ----------------------------------------------------------------- + (~doc-section :title "Keyword Arguments in URLs" :id "keywords" + (p "URLs can carry keyword arguments — named parameters that modify the innermost expression. " + "Keywords use the same " (code ":name") " syntax as SX keyword arguments.") + + (~doc-subsection :title "Setting a keyword" + (p "Use " (code ".:key.value") " to set a parameter:") + (~doc-code :code (highlight + ";; Add page number to a spec explorer URL:\n/(language.(spec.(explore.signals.:page.3)))\n→ (language (spec (explore signals :page 3)))\n\n;; Format parameter on a doc page:\n/(language.(doc.primitives.:format.\"print\"))\n→ (language (doc primitives :format \"print\"))\n\n;; Section parameter:\n/(language.(spec.(explore.signals.:section.\"batch\")))\n→ (language (spec (explore signals :section \"batch\")))" + "lisp"))) + + (~doc-subsection :title "Relative keywords" + (p "Keywords combine with relative navigation. " + "Set a keyword on the current page without changing the path:") + (~doc-code :code (highlight + ";; Current: /(language.(spec.(explore.signals.:page.1)))\n\n;; Go to page 4 (set keyword, no structural navigation):\n.:page.4\n;; → /(language.(spec.(explore.signals.:page.4)))\n;; One dot + keyword-only = modify current page's keywords\n\n;; Navigate to a sibling AND set a keyword:\n..eval.:page.1\n;; → /(language.(spec.(eval.:page.1)))\n;; Pop one level, append 'eval', set :page to 1" + "lisp"))) + + (~doc-subsection :title "Delta values" + (p "Prefix a keyword value with " (code "+") " or " (code "-") " " + "to apply a numeric delta to the current value:") + (~doc-code :code (highlight + ";; Current: /(language.(spec.(explore.signals.:page.3)))\n\n;; Next page:\n.:page.+1\n;; → /(language.(spec.(explore.signals.:page.4)))\n;; Reads current :page (3), adds 1, sets to 4\n\n;; Previous page:\n.:page.-1\n;; → /(language.(spec.(explore.signals.:page.2)))\n;; Reads current :page (3), subtracts 1, sets to 2\n\n;; Jump forward 5:\n.:page.+5\n;; → /(language.(spec.(explore.signals.:page.8)))" + "lisp")) + (p "This is pagination in the URL algebra. No JavaScript, no event handlers, " + "no state management. Just a relative URL that says \"same page, next page number.\""))) + + ;; ----------------------------------------------------------------- + (~doc-section :title "URL Special Forms" :id "special-forms" + (p "Special forms are meta-operations on URLs, distinguished by the " + (code "!") " prefix. They transform how content is resolved or displayed. " + "The " (code "!") " sigil prevents collisions with section or page function names.") + + (div :class "overflow-x-auto mt-4 mb-4" + (table :class "w-full text-sm text-left" + (thead + (tr :class "border-b border-stone-200" + (th :class "py-2 px-3 font-semibold text-stone-700" "Form") + (th :class "py-2 px-3 font-semibold text-stone-700" "Example") + (th :class "py-2 px-3 font-semibold text-stone-700" "Effect"))) + (tbody :class "text-stone-600" + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-mono text-violet-700" "!source") + (td :class "py-2 px-3 font-mono text-sm" "/(!source.(~essay-sx-sucks))") + (td :class "py-2 px-3" "Show the defcomp source code")) + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-mono text-violet-700" "!inspect") + (td :class "py-2 px-3 font-mono text-sm" "/(!inspect.(language.(doc.primitives)))") + (td :class "py-2 px-3" "Dependencies, CSS, render plan, I/O")) + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-mono text-violet-700" "!diff") + (td :class "py-2 px-3 font-mono text-sm" "/(!diff.(spec.signals).(spec.eval))") + (td :class "py-2 px-3" "Side-by-side comparison")) + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-mono text-violet-700" "!search") + (td :class "py-2 px-3 font-mono text-sm" "/(!search.\"define\".:in.(spec.signals))") + (td :class "py-2 px-3" "Grep within a page or spec")) + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-mono text-violet-700" "!raw") + (td :class "py-2 px-3 font-mono text-sm" "/(!raw.(~some-component))") + (td :class "py-2 px-3" "Skip layout wrapping — raw content")) + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-mono text-violet-700" "!json") + (td :class "py-2 px-3 font-mono text-sm" "/(!json.(language.(doc.primitives)))") + (td :class "py-2 px-3" "Return data as JSON"))))) + + (p "SX has four sigils, each marking a different kind of name:") + (ul :class "space-y-1 text-stone-600 list-disc pl-5" + (li (code "~") " — component (" (code "~card") ", " (code "~essay-sx-sucks") ")") + (li (code ":") " — keyword (" (code ":title") ", " (code ":page") ")") + (li (code ".") " — relative navigation (" (code ".slug") ", " (code "..") ")") + (li (code "!") " — URL special form (" (code "!source") ", " (code "!inspect") ")"))) + + ;; ----------------------------------------------------------------- + (~doc-section :title "SX URLs in Hypermedia" :id "hypermedia" + (p "SX URLs integrate naturally with the SX hypermedia system. " + "Every hypermedia attribute accepts SX URLs — absolute, relative, or special forms.") + + (~doc-subsection :title "Links" + (p "Standard anchor tags with SX URL hrefs:") + (~doc-code :code (highlight + ";; Absolute links (the site navigation uses these):\n(a :href \"/(language.(doc.introduction))\" \"Introduction\")\n(a :href \"/(etc.(essay.sx-sucks))\" \"SX Sucks\")\n\n;; Relative links (navigate from current page):\n(a :href \"..inline-edit\" \"Inline Edit\") ;; up and over\n(a :href \".getting-started\" \"Getting Started\") ;; same level\n(a :href \"..\" \"Back\") ;; up one level\n\n;; Direct component links:\n(a :href \"/(~essay-self-defining-medium)\" \"The True Hypermedium\")" + "lisp"))) + + (~doc-subsection :title "sx-get — HTMX-style fetching" + (p (code "sx-get") " fetches content from an SX URL and swaps it into the DOM. " + "SX URLs work exactly like path URLs:") + (~doc-code :code (highlight + ";; Fetch and swap a page section:\n(div :sx-get \"/(geography.(hypermedia.(example.progress-bar)))\"\n :sx-trigger \"click\"\n :sx-target \"#content\"\n :sx-swap \"innerHTML\"\n \"Load Progress Bar Example\")\n\n;; Relative sx-get — resolve relative to the current page:\n(div :sx-get \"..inline-edit\"\n :sx-trigger \"click\"\n :sx-target \"#content\"\n :sx-swap \"innerHTML\"\n \"Load Inline Edit\")\n\n;; Paginated content with keyword deltas:\n(button :sx-get \".:page.+1\"\n :sx-trigger \"click\"\n :sx-target \"#results\"\n :sx-swap \"innerHTML\"\n \"Next Page\")" + "lisp"))) + + (~doc-subsection :title "sx-boost — progressive enhancement" + (p (code "sx-boost") " upgrades regular links to use HTMX-style fetch+swap " + "instead of full-page navigation:") + (~doc-code :code (highlight + ";; A navigation list with boosted links:\n(nav :sx-boost \"true\"\n (ul\n (li (a :href \"/(language.(doc.introduction))\" \"Introduction\"))\n (li (a :href \"/(language.(doc.components))\" \"Components\"))\n (li (a :href \"/(language.(doc.evaluator))\" \"Evaluator\"))))\n\n;; Clicking any link fetches content via SX URL and swaps,\n;; rather than triggering a full page load.\n;; URL bar updates. Back button works. No JavaScript needed." + "lisp"))) + + (~doc-subsection :title "Pagination pattern" + (p "The relative URL algebra makes pagination trivial. " + "No state, no event handlers — just URLs:") + (~doc-code :code (highlight + ";; Current URL: /(language.(spec.(explore.signals.:page.3)))\n\n(defcomp ~paginator (&key current-page total-pages)\n (nav :class \"flex gap-2\"\n (when (> current-page 1)\n (a :href \".:page.-1\" :class \"px-3 py-1 border rounded\"\n \"Previous\"))\n (span :class \"px-3 py-1\"\n (str \"Page \" current-page \" of \" total-pages))\n (when (< current-page total-pages)\n (a :href \".:page.+1\" :class \"px-3 py-1 border rounded\"\n \"Next\"))))\n\n;; \"Previous\" resolves to .:page.-1 → :page goes from 3 to 2\n;; \"Next\" resolves to .:page.+1 → :page goes from 3 to 4\n;; Each link is a static href. Server renders the right page.\n;; Back button works. Bookmarkable. Shareable. Cacheable." + "lisp")) + (p "Compare this to the typical JavaScript pagination component with " + (code "useState") ", " (code "useEffect") ", " (code "onClick") " handlers, " + "URL sync logic, and loading states. " + "In SX, pagination is a URL transform.")) + + (~doc-subsection :title "Sibling navigation" + (p "Navigate between sibling pages using relative two-dot URLs:") + (~doc-code :code (highlight + ";; Current: /(language.(doc.components))\n;; Sibling pages: introduction, getting-started, components, evaluator, ...\n\n(nav :class \"flex justify-between\"\n (a :href \"..getting-started\" :class \"text-violet-600\"\n \"← Getting Started\")\n (a :href \"..evaluator\" :class \"text-violet-600\"\n \"Evaluator →\"))\n\n;; '..getting-started' from /(language.(doc.components))\n;; = pop 'components', append 'getting-started'\n;; = /(language.(doc.getting-started))" + "lisp")))) + + ;; ----------------------------------------------------------------- + (~doc-section :title "The Evaluation Model" :id "eval" + (p "When the server receives an SX URL, it evaluates it in four steps:") + + (~doc-subsection :title "Step 1: Parse" + (p "Strip the leading " (code "/") ", replace dots with spaces, parse as SX:") + (~doc-code :code (highlight + "/(language.(doc.introduction))\n→ strip / → (language.(doc.introduction))\n→ dots to spaces → (language (doc introduction))\n→ parse → [Symbol(\"language\"), [Symbol(\"doc\"), Symbol(\"introduction\")]]" + "lisp"))) + + (~doc-subsection :title "Step 2: Auto-quote" + (p "Unknown symbols become strings. Known functions stay as symbols:") + (~doc-code :code (highlight + ";; 'language' is a known section function → stays as Symbol\n;; 'doc' is a known page function → stays as Symbol\n;; 'introduction' is NOT a known function → becomes \"introduction\"\n\n[Symbol(\"language\"), [Symbol(\"doc\"), \"introduction\"]]" + "lisp")) + (p "This is " (strong "soft eval") ": the evaluator treats unknown symbols " + "as self-quoting strings rather than raising an error. " + "Slugs work naturally without explicit quoting.")) + + (~doc-subsection :title "Step 3: Evaluate" + (p "Standard inside-out evaluation:") + (~doc-code :code (highlight + ";; 1. Eval \"introduction\" → \"introduction\" (string, self-evaluating)\n;; 2. Eval (doc \"introduction\") → call doc function → page content AST\n;; 3. Eval (language content) → call language function → passes content through" + "lisp"))) + + (~doc-subsection :title "Step 4: Wrap" + (p "The router wraps the result in " (code "~sx-doc") " with the URL as " (code ":path") ":") + (~doc-code :code (highlight + ";; Final: (~sx-doc :path \"(language (doc introduction))\" )\n;; ~sx-doc provides the layout shell, navigation, breadcrumbs, etc.\n;; The :path tells nav resolution which tree branch to highlight." + "lisp"))) + + (p "The entire routing layer is one function. There is no routing table, " + "no URL pattern matching, no middleware chain. The URL is an expression. " + "The server evaluates it. The result is a page.")) + + ;; ----------------------------------------------------------------- + (~doc-section :title "GraphSX: URLs as Queries" :id "graphsx" + (p "The SX URL scheme is not just a routing convention — it is a query language. " + "The structural parallel with " + (a :href "https://graphql.org/" :class "text-violet-600 hover:underline" "GraphQL") + " is exact:") + + (div :class "overflow-x-auto mt-4 mb-4" + (table :class "w-full text-sm text-left" + (thead + (tr :class "border-b border-stone-200" + (th :class "py-2 px-3 font-semibold text-stone-700" "Concept") + (th :class "py-2 px-3 font-semibold text-stone-700" "GraphQL") + (th :class "py-2 px-3 font-semibold text-stone-700" "SX URLs"))) + (tbody :class "text-stone-600" + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-semibold" "Endpoint") + (td :class "py-2 px-3" (code "/graphql")) + (td :class "py-2 px-3" "Catch-all " (code "/"))) + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-semibold" "Query structure") + (td :class "py-2 px-3" "Nested fields " (code "{ language { doc { ... } } }")) + (td :class "py-2 px-3" "Nested s-expressions " (code "(language (doc ...))"))) + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-semibold" "Resolvers") + (td :class "py-2 px-3" "Per-field functions") + (td :class "py-2 px-3" "Page functions + " (code "~components"))) + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-semibold" "Fragments") + (td :class "py-2 px-3" "Named selections") + (td :class "py-2 px-3" "Components (" (code "defcomp") ")")) + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-semibold" "Arguments") + (td :class "py-2 px-3" (code "doc(slug: \"intro\")")) + (td :class "py-2 px-3" (code "(doc introduction)") " or " (code "(doc intro :format \"print\")"))) + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-semibold" "Scoping") + (td :class "py-2 px-3" "Flat — query-level only") + (td :class "py-2 px-3" "Structural — parens are scope boundaries")) + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-semibold" "Transport") + (td :class "py-2 px-3" "POST JSON body") + (td :class "py-2 px-3" "GET — the URL IS the query")) + (tr :class "border-b border-stone-100" + (td :class "py-2 px-3 font-semibold" "Response") + (td :class "py-2 px-3" "JSON (needs rendering)") + (td :class "py-2 px-3" "Content — already meaningful"))))) + + (p "The killer difference: in GraphQL, query and rendering are separate concerns. " + "You fetch JSON, then a React app renders it. In SX, " + (strong "the query language and the rendering language are the same thing") ". " + (code "(language (doc introduction))") " is simultaneously a query, a render instruction, and a URL.") + + (p "And because SX URLs are GET requests, they are cacheable, bookmarkable, " + "shareable, and indexable — everything GraphQL had to sacrifice by using POST.")) + + ;; ----------------------------------------------------------------- + (~doc-section :title "Composability" :id "composability" + (p "Because URLs are expressions, they compose. " + "The algebra of URLs is the algebra of s-expressions.") + + (~doc-code :code (highlight + ";; Two specs side by side:\n/(!diff.(language.(spec.signals)).(language.(spec.eval)))\n\n;; Source code of any page:\n/(!source.(~essay-sx-sucks))\n\n;; Inspect a page's dependency graph:\n/(!inspect.(language.(doc.primitives)))\n\n;; Search within a spec:\n/(!search.\"define\".:in.(language.(spec.signals)))\n\n;; The URL bar is a REPL:\n/(about) ;; the about page\n/(!source.(about)) ;; its source code\n\n;; Scoping composes — these are different queries:\n/(users.(posts.123.(filter.published))) ;; filter on posts\n/(users.posts.123.(filter.published)) ;; filter on the whole query" + "lisp")) + + (p "In the REST world, building a \"diff two specs\" page requires a " + "custom endpoint, a controller, a route registration, " + "and frontend code to load both specs. " + "In SX, it is a URL.")) + + ;; ----------------------------------------------------------------- + (~doc-section :title "Components as Resolvers" :id "resolvers" + (p "The " (code "~") " sigil means \"find and execute this component.\" " + "Components can make onward queries, process results, and return composed content — " + "like server-side includes but with composition.") + (~doc-code :code (highlight + ";; A component that fetches data and returns content:\n/(~get.latest-posts.(limit.5))\n\n;; The flow:\n;; 1. Server finds ~get in env\n;; 2. ~get queries the database (async IO)\n;; 3. Returns composed hypermedia\n;; 4. Router wraps in ~sx-doc\n\n;; Components compose naturally:\n/(~dashboard\n .(~get.latest-posts.(limit.5))\n .(~get.notifications)\n .(~stats.weekly))\n\n;; Each sub-expression is:\n;; - independently cacheable\n;; - independently reusable\n;; - independently testable\n;; Visit /(~get.latest-posts.(limit.5)) to test it in isolation." + "lisp")) + (p "This is what " + (a :href "https://react.dev/reference/rsc/server-components" :class "text-violet-600 hover:underline" "React Server Components") + " are trying to do — server-side data resolution composed with rendering. " + "Except React needs a framework, a bundler, a serialization protocol, and " + (code "\"use server\"") " pragmas. SX gets it from the evaluator.")) + + ;; ----------------------------------------------------------------- + (~doc-section :title "HTTP Alignment" :id "http" + (p "GraphQL sends queries as POST, even though reads are idempotent. " + "This breaks HTTP semantics — POST implies side effects, " + "so caches, CDNs, and intermediaries can't help.") + (p "SX URLs are GET requests. The query is the URL path. " + "This means:") + (ul :class "space-y-1 text-stone-600 list-disc pl-5" + (li (strong "Cacheable") " — CDNs cache by URL, and these are URLs") + (li (strong "Bookmarkable") " — save " (code "/(language.(spec.signals))") " in your browser") + (li (strong "Shareable") " — paste it in chat, it works") + (li (strong "Indexable") " — crawlers follow " (code "") " links") + (li (strong "No client library") " — " (code "curl '/(language.(doc.intro))'") " returns content")) + + (~doc-code :code (highlight + ";; HTTP verbs align naturally:\n\n;; GET — pure evaluation, cacheable, bookmarkable:\nGET /(language.(doc.introduction))\n\n;; POST — side effects, mutations:\nPOST /(submit-form)\nPOST /(cart.(add.42))\n\n;; This is what REST always wanted but GraphQL abandoned." + "lisp"))) + + ;; ----------------------------------------------------------------- + (~doc-section :title "The Router Spec" :id "spec" + (p "SX URLs are not a Python or JavaScript feature — they are specified in SX itself. " + "The " (a :href "/(language.(spec.router))" :class "text-violet-600 hover:underline" "router spec") + " (" (code "router.sx") ") defines all URL parsing, matching, relative resolution, " + "and special form detection as pure functions.") + + (p "Key functions from the spec:") + (~doc-code :code (highlight + ";; Parse a URL into a typed descriptor:\n(parse-sx-url \"/(language.(doc.intro))\")\n→ {\"type\" \"absolute\" \"raw\" \"/(language.(doc.intro))\"}\n\n(parse-sx-url \"/(!source.(~essay))\")\n→ {\"type\" \"special-form\" \"form\" \"!source\"\n \"inner\" \"(~essay)\" \"raw\" \"/(!source.(~essay))\"}\n\n(parse-sx-url \"/(~essay-sx-sucks)\")\n→ {\"type\" \"direct-component\" \"name\" \"~essay-sx-sucks\"\n \"raw\" \"/(~essay-sx-sucks)\"}\n\n(parse-sx-url \"..eval\")\n→ {\"type\" \"relative\" \"raw\" \"..eval\"}\n\n;; Resolve relative URLs:\n(resolve-relative-url\n \"/(geography.(hypermedia.(example.progress-bar)))\"\n \"..inline-edit\")\n→ \"/(geography.(hypermedia.(example.inline-edit)))\"\n\n;; Keyword delta:\n(resolve-relative-url\n \"/(language.(spec.(explore.signals.:page.3)))\"\n \".:page.+1\")\n→ \"/(language.(spec.(explore.signals.:page.4)))\"\n\n;; Check URL type:\n(relative-sx-url? \"..eval\") → true\n(relative-sx-url? \"(.slug)\") → true\n(relative-sx-url? \"/(foo)\") → false\n\n;; Special form inspection:\n(url-special-form? \"!source\") → true\n(url-special-form? \"inspect\") → false\n(url-special-form-name \"/(!source.(~essay))\") → \"!source\"\n(url-special-form-inner \"/(!source.(~essay))\") → \"(~essay)\"" + "lisp")) + + (p "These functions are " + (a :href "/(language.(test.router))" :class "text-violet-600 hover:underline" "tested with 115 SX tests") + " covering every edge case — structural navigation, keyword operations, " + "delta values, special form parsing, and bare-dot shorthand. " + "The spec is bootstrapped to both JavaScript (" (code "sx-browser.js") ") and " + "Python (" (code "sx_ref.py") "), so client and server share the same URL algebra.")) + + ;; ----------------------------------------------------------------- + (~doc-section :title "The Lisp Tax" :id "parens" + (p "People will object to the parentheses. Consider what they already accept:") + (~doc-code :code (highlight + ";; Developers write this every day:\nhttps://api.site.com/v2/users/123/posts?filter=published&sort=date&order=desc&limit=10&offset=20\n\n;; And would complain about this?\nhttps://site.com/(users.(posts.123.(filter.published.sort.date.limit.10)))\n\n;; The second is shorter, structured, unambiguous, and composable." + "lisp")) + + (p "The real question is who reads URLs:") + (ul :class "space-y-1 text-stone-600 list-disc pl-5" + (li (strong "End users") " barely look at URLs — they live in address bars that autocomplete") + (li (strong "Developers") " will love the structure once they understand it") + (li (strong "Crawlers") " do not care about syntax — they follow links")) + (p "And this site is " (em "about") " SX, " (em "implemented in") " SX. " + "Every URL is a live example. Visiting a page is evaluating an expression. " + "The parentheses are not a tax — they are the point.")) + + ;; ----------------------------------------------------------------- + (~doc-section :title "The Site Is a REPL" :id "repl" + (p "The address bar is the input line. The page is the output.") + (~doc-code :code (highlight + "/(about) ;; renders the about page\n/(!source.(about)) ;; returns its source code\n\n/(language.(spec.signals)) ;; the signals spec\n/(!inspect.(language.(spec.signals))) ;; its dependency graph\n\n/(~essay-sx-sucks) ;; the essay\n/(!source.(~essay-sx-sucks)) ;; its s-expression source\n\n;; The website IS a REPL. Every page is a function call.\n;; Every function call is a URL. Evaluation is navigation." + "lisp")) + (p "You do not need to explain what SX is. " + "Show someone a URL, and they immediately understand the philosophy. " + "The entire site is a self-hosting demonstration — " + "the medium defines itself with itself.")))) diff --git a/sx/sxc/pages/sx_router.py b/sx/sxc/pages/sx_router.py index 503b380..f031b4e 100644 --- a/sx/sxc/pages/sx_router.py +++ b/sx/sxc/pages/sx_router.py @@ -37,6 +37,7 @@ _PAGE_FNS = { "doc", "spec", "explore", "bootstrapper", "test", "reference", "reference-detail", "example", "cssx", "protocol", "essay", "philosophy", "plan", + "sx-urls", } # All known function names (don't auto-quote these)