| Crates.io | markdown_view_leptos |
| lib.rs | markdown_view_leptos |
| version | 0.1.92 |
| created_at | 2025-09-01 02:43:54.67972+00 |
| updated_at | 2025-12-23 18:42:56.335487+00 |
| description | Render Markdown with embedded Leptos components via a simple macro (MDX-like for Leptos). |
| homepage | |
| repository | https://github.com/victorfds/markdown_view_leptos |
| max_upload_size | |
| id | 1819067 |
| size | 167,109 |
Compile-time Markdown to Leptos view! with MDX‑like inline components.
{{ <MyComp prop=value/> }}file = "...", or url = "..." sources{{ ... }} inside triple‑backtick code blocksSecurity note: the generated HTML is injected via
inner_html. Only use trusted Markdown or sanitize upstream.
cargo add markdown_view_leptos
For CSR (WASM):
rustup target add wasm32-unknown-unknowncargo install trunk)use leptos::prelude::*;
use markdown_view_leptos::markdown_view;
#[component]
fn Hello() -> impl IntoView {
view! { <em>"Hello from a component"</em> }
}
#[component]
pub fn App() -> impl IntoView {
view! { <main>{markdown_view!(r#"
# Title
Some text before the component.
{{ <Hello/> }}
And some text after.
"#)}</main> }
}
Both markdown_view! and markdown_anchors! accept the same input forms:
"...", r#"..."#, String::from("..."), "...".to_string()).file = "path.md": resolved at compile time relative to CARGO_MANIFEST_DIR.file = <expr>: resolved at compile time if the file exists; otherwise read at runtime
(non-wasm only).url = "https://...": fetched at compile time (disabled in rust-analyzer). If the URL
expression cannot be resolved to a literal, the macro treats it like a dynamic
Markdown string expression.String/&str): handled at runtime.Works with normal or raw strings; inline {{ ... }} components are spliced in.
The same inline forms work with markdown_anchors!.
use leptos::prelude::*;
use markdown_view_leptos::markdown_view;
#[component]
pub fn App() -> impl IntoView {
view! { <section>{markdown_view!(file = "content.md")}</section> }
}
file paths are resolved relative to your crate root (CARGO_MANIFEST_DIR). Literal paths are embedded at compile time so edits trigger recompiles.
You can also point to a variable. If the macro can resolve it at compile time (e.g., let content = "content.md"; or format!("content.md") where the file exists), it behaves like a literal path. Otherwise it falls back to reading at runtime (non-wasm only) and inline components are not expanded.
markdown_anchors! accepts the same file = ... forms.
let base = "/opt/articles";
let name = "welcome.md";
let view = markdown_view!(file = format!("{}/{}", base, name));
For wasm builds, use a path the macro can resolve at compile time so the content is embedded (no filesystem at runtime).
String/&str variables are accepted.
let body = r#"
# Title
Inline component: {{ <Hello/> }}
"#;
let inline_view = markdown_view!(body); // components work: `body` is a literal binding
let runtime_body: String = fetch_from_server();
let runtime_view = markdown_view!(runtime_body); // runtime parsing, `{{ ... }}` stays literal
If the macro can see a string literal binding in the same file (as with body above),
it inlines it so {{ ... }} components still render. Otherwise Markdown is rendered
at runtime and {{ ... }} blocks stay literal text.
markdown_anchors! uses the same runtime path for dynamic strings.
use leptos::prelude::*;
use markdown_view_leptos::markdown_view;
#[component]
pub fn App() -> impl IntoView {
view! {
<div>{markdown_view!(url = "https://github.com/leptos-rs/awesome-leptos/blob/main/README.md")}</div>
}
}
file = "..." for reproducible builds.markdown_anchors! accepts the same url = ... forms.
pulldown‑cmark with the full option set (definition lists, footnotes, GFM, math, heading attributes, metadata blocks, and more). Runtime strings use a lightweight built-in parser (headings + paragraphs + heading IDs). Injected via inner_html into a view! tree.{{ ... }} outside fenced code blocks is parsed as Rust/RSX and spliced into the view! tree for compile-time sources (string literal, file, url, or identifiers that resolve to literals in the same file). Runtime String inputs render {{ ... }} literally.{{ ... }} inside them is ignored and rendered literally.{{ ... }} doesn’t parse, it is rendered as plain Markdown so your build doesn’t fail unexpectedly.--- or +++) via pulldown‑cmark’s metadata block options.markdown_anchors! uses pulldown‑cmark at compile time; runtime strings use the lightweight parser and honor {#id} (other attributes are ignored).Headings get IDs plus an anchor link (rendered before the heading text). IDs are slugified by lowercasing, stripping accents, and replacing non-alphanumeric runs with - (for example, Último parágrafo becomes ultimo-paragrafo). Customize or disable:
let view = markdown_view!(
file = "content.md",
anchor = true,
anchor_class = "my-anchor",
anchor_style = "color: #f40;",
anchor_wrapper_class = "anchor-wrap",
anchor_wrapper_style = "display: inline-block;",
anchor_symbol = "§"
);
anchor_wrapper_class and anchor_wrapper_style apply to the heading element
that wraps the anchor link for additional layout control.
To disable anchor links (IDs still render for deep links):
let view = markdown_view!(
file = "content.md",
anchor = false
);
Anchor styling is handled via CSS. A VitePress-like pattern:
.markdown-view { --mdv-anchor-color: #6b7280; --mdv-anchor-hover-color: #111827; }
.markdown-view :is(h1, h2, h3, h4, h5, h6) { position: relative; }
.markdown-view .header-anchor {
position: absolute;
left: -0.9em;
opacity: 0;
text-decoration: none;
color: var(--mdv-anchor-color);
transition: opacity 0.15s ease, color 0.15s ease;
}
.markdown-view :is(h1, h2, h3, h4, h5, h6):hover .header-anchor {
opacity: 1;
color: var(--mdv-anchor-hover-color);
}
Use markdown_anchors! to get all heading (title, id) pairs (for TOCs or navigation). The same slug rules apply when headings do not define a custom {#id}:
use markdown_view_leptos::markdown_anchors;
let anchors = markdown_anchors!(r#"
# Getting Started
## Install
## Install {#custom-install}
"#);
// anchors == [
// ("Getting Started", "getting-started"),
// ("Install", "install"),
// ("Install", "custom-install")
// ]
let toc = anchors
.iter()
.map(|(title, id)| format!("<li><a href=\"#{}\">{}</a></li>", id, title))
.collect::<String>();
markdown_anchors! only returns anchors when the input contains [[toc]]. The marker
is stripped from markdown_view! output and never rendered.
When you pass a dynamic string expression to markdown_anchors!, the runtime path
uses the lightweight parser and honors {#id} (other heading attributes are ignored).
This repo includes a CSR example (Trunk):
examples/markdown-csr-example: Renders a canvas‑based ParticleText component from within Markdown.Run with Trunk:
cd examples/markdown-csr-example
trunk serve --open
{{ ... }} must be in scope where you invoke the macro.file = "..." uses include_str! to make the file a build input; saving it triggers a rebuild.cargo testcargo clippy --all-targets -- -D warnings and cargo fmt --allThank you for reading this 💙