Replace _post_edit_content_sx raw HTML builder with sx_call() pattern
matching render_editor_panel. Add ~blog-editor-edit-form,
~blog-editor-publish-js, ~blog-editor-sx-initial components to
editor.sx. Fixes (~sx-editor-styles) rendering as literal text on
the edit page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All app-level templates have been replaced by native sx component builders
and defpage declarative routes. Removes ~15,200 lines of dead HTML.
Kept: shared/browser templates (errors, ap_social, macros, root layout),
account + federation _email/magic_link, federation profile.html chain.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Python GET page handlers with declarative defpage definitions in .sx
files across all 8 apps (sx docs, orders, account, market, cart, federation,
events, blog). Each app now has sxc/pages/ with setup functions, layout
registrations, page helpers, and .sx defpage declarations.
Core infrastructure: add g I/O primitive, PageDef support for auth/layout/
data/content/filter/aside/menu slots, post_author auth level, and custom
layout registration. Remove ~1400 lines of render_*_page/render_*_oob
boilerplate. Update all endpoint references in routes, sx_components, and
templates to defpage_* naming.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Strip NIL values at I/O primitive boundaries (frag, query, action, service)
to prevent _Nil objects from reaching Python code that expects None. Add
mobile_nav_sx() helper that auto-populates the hamburger menu from nav_tree
and auth_menu context fragments when no menu slot is provided.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add sx_content to _post_to_edit_dict so edit page receives existing content
- Add SX/Koenig editor tabs, sx-editor mount point, and SxEditor.mount init
- Only pass sx_content to writer_update when form field is present (prevents
accidental clearing when editing via Koenig-only path)
- Add csrf_exempt to example API POST/DELETE/PUT demo endpoints
- Add defpage infrastructure (pages.py, layouts.py) and sx docs page definitions
- Add defhandler definitions for example API handlers (examples.sx)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sx.js only appended INPUT values to GET request URLs. SELECT and
TEXTAREA elements with a name attribute were silently ignored,
so the category parameter was never sent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move .sx-fade-in and .sx-loading-btn CSS from inline (style) tags to
basics.css so they go through the on-demand CSS registry
- Pretty-print sx source in wire response display (not all on one line)
- Change sx logo from </> icon to (</>) text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- All 6 examples show Component and Wire response as placeholders that
fill with actual content when the demo is triggered (via OOB swaps)
- Wire response shows full wire content including component definitions
(when not cached) and CSS style block
- Component display only includes defs the client doesn't already have,
matching real sx_response() behaviour
- Add "Clear component cache" button to reset localStorage + in-memory
component env so next interaction shows component download
- Rebuild tw.css with Tailwind v3.4.19 including sx content paths
- Optimize sx_response() CSS scanning to only scan sent comp_defs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pass :code keyword to ~doc-code and ~example-source components
(highlighted content was positional but components use &key code)
- Rebuild tw.css (v3.4.19) with sx/sxc and sx/content in content paths
so highlight.py classes (text-violet-600, text-rose-600, etc.) are included
- Add bg-violet-{100-500} classes for the sx app's violet menu bar
- Add highlight.py custom syntax highlighter (sx, python, bash)
IMPORTANT: tw.css must contain bg-violet-{100-500} rules for the sx
app's menu bar. Do not rebuild tw.css without ensuring violet classes
are included (via safelist or content paths).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
execute_handler was using async_render() which renders all the way to
HTML. Fragment providers need to return sx source (s-expression strings)
that consuming apps parse and render client-side.
Added async_eval_to_sx() — a new execution mode that evaluates I/O
primitives and control flow but serializes component/tag calls as sx
source instead of rendering them to HTML. This mirrors how the old
Python handlers used sx_call() to build sx strings.
Also fixed: _ASER_FORMS checked after HTML_TAGS, causing "map" (which
is both an HTML tag and an sx special form) to be serialized as a tag
instead of evaluated. Moved _ASER_FORMS check before HTML_TAGS.
Also fixed: empty? primitive now handles non-len()-able types gracefully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Return _RawHTML wrapper so pre-rendered HTML in let bindings isn't
escaped when used in render context.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- OOB nav updates: AJAX navigation now swaps both menu bar levels
(main nav highlighting + sub-nav with current page) using the same
oob_header_sx/oob_page_sx pattern as blog/market/events
- Enable OAuth for sx and test apps (removed from _NO_OAUTH, added sx
to ALLOWED_CLIENTS, added app_urls for sx/test/orders)
- Fetch real cross-service fragments (cart-mini, auth-menu, nav-tree)
instead of hardcoding empty values
- Add :selected param to ~menu-row-sx for white text current-page label
- Fix duplicate element IDs: use menu-row-sx child_id/child mechanism
instead of manual header_child_sx wrappers
- Fix home page copy: "Server-rendered DOM over the wire (no HTML)"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap 15 call sites in events/sx_components.py where sx-generating
functions were passed as plain strings to sx_call(), causing raw
s-expression source to leak into the rendered page.
- Add dev-mode pretty-printing (RELOAD=true) for sx responses and
full page sx source — indented output in Network tab and View Source.
- Fix Sx.render to handle multiple top-level expressions by falling
back to parseAll and returning a DocumentFragment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- sx-editor prints version on init: [sx-editor] v2026-03-02b-exorcism
- Add Markdown to card insert menu with /markdown and /md slash commands
- Add YouTube, X/Twitter, Vimeo, Spotify, CodePen as dedicated embed
menu items with brand icons (all create ~kg-embed cards)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add shared/sx/html_to_sx.py (HTMLParser-based HTML→sx converter) and
update lexical_to_sx.py so HTML cards, markdown cards, and captions all
produce native sx expressions instead of opaque HTML strings.
- ~kg-html now wraps native sx children (editor can identify the block)
- New ~kg-md component for markdown card blocks
- Captions are sx expressions, not escaped HTML strings
- kg_cards.sx: replace (raw! caption) with direct caption rendering
- sx-editor.js: htmlToSx() via DOMParser, serializeInline for captions,
_childrenSx for ~kg-html/~kg-md, new kg-md edit UI
- Migration script (blog/scripts/migrate_sx_html.py) to re-convert
stored sx_content from lexical source
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both Python and JS parsers used next_token() which returns plain strings
for both delimiter characters and string values, making them
indistinguishable. A string whose value is ")" or "(" would be
misinterpreted as a structural delimiter, causing parse errors.
Fix: use peek() (raw character) for all structural decisions in
parseExpr before consuming via next_token(). Also add enhanced error
logging to sx.js mount/loadComponents for easier future debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When server omits component source (cache hit), the script tag has
empty textContent. The early `if (!text.trim()) continue` was
skipping the data-components handler entirely, so components never
loaded from localStorage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server computes SHA-256 hash of all component source at startup.
Client signals its cached hash via cookie (sx-comp-hash). On full
page load: cookie match → server sends empty script tag with just
the hash; mismatch → sends full source. Client loads from
localStorage on hit, parses inline + caches on miss.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix _markdown() in lexical_to_sx.py: render markdown to HTML with
mistune.html() before storing in ~kg-html
- Add shared/sx/prettify.py: sx_to_pretty_sx and json_to_pretty_sx
produce sx AST for syntax-highlighted DOM (uses canonical serialize())
- Add preview tab to admin header nav
- Add GET /preview/ route with 4 views: prettified sx, prettified
lexical JSON, sx rendered HTML, lexical rendered HTML
- Add ~blog-preview-panel and ~blog-preview-section components
- Add syntax highlight CSS for sx/JSON tokens
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Computes file content hash at process startup, cached for lifetime.
Removes manual cache-busting instruction from CLAUDE.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Model: add sx_content column to Post. Writer: accept sx_content in
create_post, create_page, update_post. Routes: read sx_content from form
data in new post, new page, and edit routes. Read pipeline: ghost_db
includes sx_content in public dict, detail/home views prefer sx_content
over html when available, PostDTO includes sx_content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pure s-expression block editor replacing React/Koenig: single hover + button,
slash commands, full card edit modes (image/gallery/video/audio/file/embed/
bookmark/callout/toggle/button/HTML/code), inline format toolbar, keyboard
shortcuts, drag-drop uploads, oEmbed/bookmark metadata fetching.
Includes lexical_to_sx converter for backfilling existing posts, KG card
components matching Ghost's card CSS, migration for sx_content column, and
31 converter tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Process sx-swap-oob and hx-swap-oob elements in the popstate handler
so sidebar, filter, menu, and headers are restored on back navigation
- Disable the 62.5% base font-size hack that leaked globally and caused
all fonts to shrink when navigating to/from the editor
- Cache-bust sx.js to v=20260301d
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Parse tw.css into per-class lookup registry at startup
- Pre-scan component CSS classes at registration time (avoid per-request regex)
- Compress SX-Css header: 8-char hash replaces full class list (LRU cache)
- Add ;@css comment annotation for dynamically constructed class names
- Safelist bg-sky-{100..400} in Tailwind config for menu-row-sx dynamic shades
- Client sends/receives hash, falls back gracefully on cache miss
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add shared/static/styles/tailwind.css as Tailwind v4 input with
explicit @source paths for all service templates and safelisted
dynamic classes (bg-{colour}-{shade}, text-{size})
- Build to shared/static/styles/tw.css (93KB minified)
- Replace <script src="cdn.tailwindcss.com"> with <link> to tw.css
in sx page shell, Jinja _head.html, and ~base-shell component
- Add build-tw.sh convenience script
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Always re-fetch on popstate (drop LRU cache) for fresh content on back/forward
- Save/restore scroll position via pushState
- Add id="root-header-child" to ~app-body so OOB swaps can target it
- Fix OOB renderers: nest root-row inside root-header-child swap instead of
separate OOB that clobbers it
- Fix 3+ header rows dropped: wrap all headers in single fragment instead of
concatenating outside (<> ...)
- Strip <script data-components> from text/sx responses before renderToString
- Fall back to location.assign for cross-origin pushState (SecurityError)
- Move blog/sx/nav.sx to shared/sx/templates/ so all services have nav components
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scripts inserted via innerHTML/insertAdjacentHTML don't execute.
Add _activateScripts() to _swapContent that recreates script tags
(without type or type=text/javascript) as live elements. This fixes
editor.js not loading when navigating to edit pages via sx-get.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scripts inserted via innerHTML (template.content) don't execute.
When raw! renders HTML containing <script> tags, recreate them as
live elements so the browser fetches and executes them. Fixes
editor.js not loading on HTMX navigation to edit pages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three issues with the eager kwarg evaluation in renderComponentDOM and
renderStrComponent:
1. Data arrays (e.g. tags list of dicts) were being passed to sxEval
which tried to call a dict as a function — causing blank pages.
Fix: only evaluate arrays with a Symbol head (actual expressions);
pass data arrays through as-is.
2. Expression arrays like (get t "src") inside map lambdas lost their
scope when deferred — causing "get,t,src" URLs. Fix: eagerly evaluate
these Symbol-headed expressions in the caller's env.
3. Bare symbol `t` used as boolean in editor.sx threw "Undefined symbol".
Fix: use `true` literal instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
renderComponentDOM now eagerly renders kwarg values that are render
expressions (HTML tags, <>, ~components) into DOM nodes. But renderDOM
treated any non-array object as a dict and returned an empty fragment,
silently discarding pre-rendered content. Add a nodeType check to pass
DOM nodes through unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The string renderer's component call had the same deferred-evaluation
bug — and this is the path actually used for blog card rendering via
renderToString. Apply the same _isRenderExpr check to route render-only
forms through renderStr while data expressions go through sxEval.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous fix eagerly evaluated all kwarg expressions via sxEval,
which broke render-only forms (<>, raw!, HTML tags, ~components) that
only exist in the render pipeline. Now detect render expressions by
checking if the head symbol is an HTML/SVG tag, <>, raw!, or ~component,
and route those through renderDOM while data expressions still go
through sxEval for correct scope resolution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
renderComponentDOM was deferring evaluation of complex expressions
(arrays) passed as component kwargs, storing raw AST instead. When the
component body later used these values as attributes, the caller's env
(with lambda params like t, a) was no longer available, producing
stringified arrays like "get,t,src" as attribute values — which browsers
interpreted as relative URLs.
Evaluate all non-literal kwarg values eagerly in the caller's env,
matching the behavior of callComponent and the Python-side renderer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move Post/Author/Tag/PostAuthor/PostTag/PostUser models from
shared/models/ghost_content.py to blog/models/content.py so blog-domain
models no longer live in the shared layer. Replace the shared
SqlBlogService + BlogService protocol with a blog-local singleton
(blog_service), and switch entry_associations.py from direct DB access
to HTTP fetch_data("blog", "post-by-id") to respect the inter-service
boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Final sync script with HTML verification + author→user migration
- Make ghost_id nullable on posts/authors/tags, add UUID/timestamp defaults
- Add user profile fields (bio, slug, profile_image, etc.) to User model
- New PostUser M2M table (replaces post_authors for new posts)
- PostWriter service: direct DB CRUD with Lexical rendering, optimistic
locking, AP federation, tag upsert
- Rewrite create/edit/settings routes to use PostWriter (no Ghost API calls)
- Neuter Ghost webhooks (post/page/author/tag → 204 no-op)
- Disable Ghost startup sync
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename all sexp directories, files, identifiers, and references to sx.
artdag/ excluded (separate media processing DSL).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cart mini and auth-menu components were rendering plain <a href>
links for cross-domain navigation. Add sx-get with OOB swap
attributes so these use the SX fetch path instead of full reloads.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enable instant cross-subdomain navigation (blog → market, etc.) via
sx-get instead of full page reloads. The server prepends missing
component definitions to OOB responses so the client can render
components from other domains.
- sexp.js: send SX-Components header, add credentials for cross-origin
fetches to .rose-ash.com/.localhost, process sexp scripts in response
before OOB swap
- helpers.py: add components_for_request() to diff client/server
component sets, update sexp_response() to prepend missing defs
- factory.py: add SX-Components to CORS allowed headers, add
Access-Control-Allow-Methods
- fragments/routes.py: switch nav items from ~blog-nav-item-plain to
~blog-nav-item-link (sx-get enabled)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove old raw! layout components (~app-head, ~app-layout, ~oob-response,
~header-row, ~menu-row, ~oob-header, ~header-child) from layout.sexp
- Convert nav-tree fragment from Jinja HTML to sexp source, fixing the
"Unexpected character: ." parse error caused by HTML leaking into sexp
- Add _as_sexp() helper to safely coerce HTML fragments to ~rich-text
- Fix federation/sexp/search.sexpr extra closing paren
- Remove dead _html() wrappers from blog and account sexp_components
- Remove stale render import from cart sexp_components
- Add dev_watcher.py to auto-reload on .sexp/.sexpr/.js/.css changes
- Add test_parse_all.py to parse-check all 59 sexpr/sexp files
- Fix test assertions for sx- attribute prefix (was hx-)
- Add sexp.js version logging for cache debugging
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Server sends sexp source text, client (sexp.js) renders everything
- SexpExpr marker class for nested sexp composition in serialize()
- sexp_page() HTML shell with data-mount="body" for full page loads
- sexp_response() returns text/sexp for OOB/partial responses
- ~app-body layout component replaces ~app-layout (no raw!)
- ~rich-text is the only component using raw! (for CMS HTML content)
- Fragment endpoints return text/sexp, auto-wrapped in SexpExpr
- All _*_html() helpers converted to _*_sexp() returning sexp source
- Head auto-hoist: sexp.js moves meta/title/link/script[ld+json]
from rendered body to document.head automatically
- Unknown components render warning box instead of crashing page
- Component kwargs preserve AST for lazy rendering (fixes <> in kwargs)
- Fix unterminated paren in events/sexp/tickets.sexpr
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- HTMX beforeSwap hook intercepts text/sexp responses and renders
them client-side via sexp.js before HTMX swaps the result in
- sexp_response() helper for returning text/sexp from route handlers
- Test detail page (/test/<nodeid>) with clickable test names
- HTMX navigation to detail returns sexp wire format (4x smaller
than pre-rendered HTML), full page loads render server-side
- ~test-detail component with back link, outcome badge, and
error traceback display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds Sexp.update() for re-rendering data-sexp elements with new data,
Sexp.hydrate() for finding and rendering all [data-sexp] elements,
and auto-init on DOMContentLoaded + htmx:afterSwap integration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Load sexp.js in ~app-layout before body.js
- Auto-process <script type="text/sexp"> tags on DOMContentLoaded
- Re-process after htmx:afterSwap for dynamic content
- Sexp.mount(target, expr, env) for rendering into DOM elements
- Sexp.processScripts() picks up data-components and data-mount tags
- client_components_tag() Python helper serializes Component objects
back to sexp source for client-side consumption
- 37 parity tests all passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Vanilla JS (no build tools) counterpart to shared/sexp/ Python modules.
Parses s-expression text, evaluates special forms, and renders to DOM
nodes or HTML strings. Full component system with defcomp/~name.
Includes:
- Parser: tokenizer + parse/parseAll matching Python parser exactly
- Evaluator: all special forms (if, when, cond, let, and, or, lambda,
defcomp, define, ->, set!), higher-order forms (map, filter, reduce)
- DOM renderer: createElement for HTML tags, SVG namespace support,
component invocation, raw! for pre-rendered HTML, <> fragments
- String renderer: matches Python html.render output for SSR parity
- ~50 built-in primitives (arithmetic, string, collection, predicates)
- 35 parity tests verifying JS output matches Python output via Node.js
Also fixes Python raw! handler to properly unwrap _RawHTML objects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>