autumn-plugin-markstream
Render and stream Markdown in Autumn apps with markstream. Use Maud, htmx and Server-Sent Events. You need no npm, no bundler and no inline script.
The crate vendors markstream-vue 2.0.16 and Vue 3.5.43 (runtime build). Autumn serves the files from memory.
Quickstart
Add the plugin, put the head tags in the layout, and render Markdown:
use ;
use *;
async
async
Streaming
Point a container at an SSE URL. Send the Markdown in chunks:
use ;
use ;
use *;
async
async
sse::stream and sse::Stream come from futures-util. Any
Stream<Item = T> with T: Into<String> works. Use try_markstream_sse
for a stream of Results. An Err sends an error event with no details
and stops the stream.
Protocol
| Event | Data | Effect |
|---|---|---|
chunk |
JSON string | Add the text to the end. |
replace |
JSON string | Replace all text. |
done |
null |
Mark the text final. Close the stream. |
error |
null |
Mark the text final and failed. Close the stream. |
- The data is JSON. Spaces,
\r,\nand Unicode stay the same. - The init module closes the stream and does not reconnect. A stream that
stops before
doneshows the error state. - To make your own events, use
sse::StreamEventandautumn_web::sse::Event::from(event).
htmx
The init module mounts each new container after an htmx swap
(htmx:load). It closes the stream and unmounts the container on
htmx:beforeCleanupElement. A partial can return a streaming container:
use ;
use *;
async
Ask
Builder and attributes
| Builder | Attribute | Values | Default |
|---|---|---|---|
| (always) | data-markstream |
marker | required |
.html_policy(p) |
data-markstream-html |
escape, safe, trusted |
escape |
.mode(m) |
data-markstream-mode |
docs, chat, minimal |
markstream default |
.theme(t) |
data-markstream-theme |
light, dark |
prefers-color-scheme |
.typewriter(true) |
data-markstream-typewriter |
true |
off |
.max_live_nodes(n) |
data-markstream-max-live-nodes |
maximum live nodes (0: all) |
markstream default |
::stream(url), .stream_from(url) |
data-markstream-src |
URL | none |
.id(s), .class(s) |
id, class |
text | class="markstream" (the plugin styles need it) |
- Unknown attribute values fall back to safe values.
trustedneedsmarkstream_allow_trusted()in the page head.- The Markdown source is in
<pre class="markstream-source">. Users without JavaScript see this source. - The init module sets
data-markstream-mounted,data-markstream-state(streaming,done,error) andaria-busy.
JavaScript API
The init module sends these DOM events. They bubble.
| Event | detail |
|---|---|
markstream:mount |
{ status } |
markstream:done |
{ content } |
markstream:error |
{ content } |
It sends markstream:ready on document.
window.AutumnMarkstream gives:
scan(root): mount each container inroot. It skips containers inside rendered Markdown.mount(el): mount one container. It returns a handle. It does not do the nested check.get(el): the handle, ornull.
A handle has these functions:
append(text),replace(text): change the text of an open stream.finish(),fail(): mark the text final (doneorerror).reset(text): set the text and mark it as not final. It opens no connection.content(),status(): read the state.unmount(): close the stream and remove markstream. The<pre>source shows again.
append, replace, finish and fail do nothing after done or
error. Call reset first. Use a handle with other transports, for
example WebSocket:
const h = ;
h.;
socket h.;
socket h.;
Security
- The default HTML policy is
escape. Raw HTML in the Markdown shows as text. saferemoves event handlers (on*),<script>and<iframe>. It keeps other attributes, for examplehx-*anddata-*.trustedkeeps all raw HTML except<script>. Use it only for Markdown that you wrote. The page head must holdmarkstream_allow_trusted(). Without this meta tag, the init module usessafe. Injected markup withdata-markstream-html="trusted"then gets no raw HTML.- Rendered Markdown goes in a
<div hx-disable>. htmx ignoreshx-*attributes in it, also after an htmx history restore. - The init module does not mount a container inside rendered Markdown. Markdown cannot open a new stream.
data-markstream-srcmust have the same origin as the page. Else the container shows the error state and opens no connection.- The head tags have SRI hashes. The browser checks the init module and
each eager module (
<link rel="modulepreload">). Lazy modules load from the same origin with no hash. Usemarkstream_head():markstream_script()alone checks only the init module. - The served JavaScript has no
evaland nonew Function. It works with the default Autumn CSP (script-src 'self').
Demo
Open http://127.0.0.1:3000. The page shows static Markdown, a live stream and htmx swaps.
How it works
scripts/vendor.pydownloads the pinned npm tarballs and checks each one against the registry sha512. It rewrites each bare import ("vue") to a relative path. It writesassets/manifest.jsonandsrc/vendored.rs.- The bundle
MARKSTREAM_ASSETSholds the vendored tree,init.js,core.jsandmarkstream.css.MarkstreamPlugininstalls it throughAppBuilder::plugin_assets. - Files serve under
/static/_plugins/markstream/at a hashed URL (immutable) and a plain URL (must-revalidate). Relative imports resolve to the plain URLs. - Decisions:
docs/adr/. Plan:docs/planning/.
To update markstream, change the versions in scripts/vendor.py. Then run
python3 scripts/vendor.py and cargo test.
Tests
CSP_NONCE=1
Limits
- No syntax highlighting. Code blocks render as plain
<pre>. - No Mermaid, D2 or infographic. These blocks show their source.
- No KaTeX unless the page loads KaTeX as a global (
window.katex). - markstream shows new text smoothly. The DOM can show the last text
after the
doneevent. - The init module unmounts on htmx swaps and cleanup only. Other DOM
removal keeps the stream open until
done. - The vendored parser is not minified (850 KB raw).
License
Apache-2.0. Vendored files are MIT. See assets/licenses/.