the_other_tui_markdown/lib.rs
1//! Convert Markdown text into [`ratatui_core::text::Text`] for display in a TUI.
2//!
3//! # Overview
4//!
5//! This crate parses Markdown with [`pulldown_cmark`] and maps every element to
6//! styled [`ratatui_core::text::Span`]s and [`ratatui_core::text::Line`]s,
7//! producing a self-contained [`ratatui_core::text::Text<'static>`] ready to
8//! hand to a `ratatui` `Paragraph` widget (or any other widget that accepts
9//! `Text`).
10//!
11//! # Quick start
12//!
13//! ```rust
14//! use the_other_tui_markdown::into_text;
15//!
16//! let text = into_text("# Hello\n\nSome **bold** and _italic_ text.");
17//! // `text` is a `ratatui_core::text::Text<'static>` ready to render.
18//! ```
19//!
20//! # Theming
21//!
22//! Every Markdown element has a corresponding [`Style`] field on [`Theme`].
23//! All defaults are based on the 16-colour ANSI palette (no true-colour
24//! required). Override what you need:
25//!
26//! ```rust
27//! use the_other_tui_markdown::{RendererBuilder, Theme, into_text_with_renderer};
28//! use ratatui_core::style::{Color, Modifier, Style};
29//!
30//! let mut theme = Theme::default();
31//! theme.h1 = Style::new().fg(Color::Green).add_modifier(Modifier::BOLD);
32//!
33//! let renderer = RendererBuilder::new().with_theme(theme).build();
34//! let text = into_text_with_renderer("# My heading", &renderer);
35//! ```
36//!
37//! # Per-element rendering customization
38//!
39//! [`RendererBuilder`] lets you replace the default rendering for any element
40//! type with a custom closure. This is especially useful for links — you can
41//! map each URL to a short hint number so the user can open it by typing a
42//! number:
43//!
44//! ```rust
45//! use the_other_tui_markdown::{RendererBuilder, into_text_with_renderer};
46//! use ratatui_core::text::Span;
47//! use std::collections::HashMap;
48//! use std::sync::{Arc, Mutex};
49//! use std::sync::atomic::{AtomicU32, Ordering};
50//!
51//! // A shared hint table: url → number.
52//! let hints: Arc<Mutex<HashMap<String, u32>>> = Arc::new(Mutex::new(HashMap::new()));
53//! let counter = Arc::new(AtomicU32::new(0));
54//!
55//! let hints_clone = Arc::clone(&hints);
56//! let counter_clone = Arc::clone(&counter);
57//! let renderer = RendererBuilder::new()
58//! .with_link(move |alt, url| {
59//! let mut map = hints_clone.lock().unwrap();
60//! let n = *map.entry(url.to_owned()).or_insert_with(|| {
61//! counter_clone.fetch_add(1, Ordering::Relaxed) + 1
62//! });
63//! vec", alt, n))]
64//! })
65//! .build();
66//!
67//! let text = into_text_with_renderer(
68//! "Visit [the docs](https://docs.rs) and [crates.io](https://crates.io).",
69//! &renderer,
70//! );
71//! // Renders as: "Visit [the docs](1) and [crates.io](2)."
72//! // `hints` now maps "https://docs.rs" → 1, "https://crates.io" → 2.
73//! ```
74//!
75//! # Supported Markdown elements
76//!
77//! | Element | Default output |
78//! |---|---|
79//! | `# H1` … `###### H6` | `# text` prefix + heading style |
80//! | `**bold**` / `__bold__` | [`Modifier::BOLD`] |
81//! | `*italic*` / `_italic_` | [`Modifier::ITALIC`] |
82//! | `~~strikethrough~~` | [`Modifier::CROSSED_OUT`] |
83//! | `^superscript^` | [`Modifier::DIM`] |
84//! | `~subscript~` | [`Modifier::DIM`] |
85//! | `` `inline code` `` | yellow foreground |
86//! | ` ```lang … ``` ` | `[lang]` label + yellow foreground |
87//! | `[alt](url)` | `[alt](url)` in link style |
88//! | `` | `🖼 alt(url)` in image style |
89//! | `> quote` | `▌ text` with quote style (GFM alerts supported) |
90//! | `- item` / `1. item` | `• item` / `1. item` with nesting |
91//! | `- [x] done` | `[x] ` / `[ ] ` prefix |
92//! | `---` (rule) | `────────────────────────────────────────` |
93//! | Tables | aligned columns with `─┼─` separator |
94//! | `[^1]` footnote refs | `[^1]` in dim style; defs appended at end |
95//! | Inline / block HTML | verbatim, unstyled |
96//! | Inline / display math | verbatim content in math style |
97//! | Definition lists | term in bold, definition indented |
98
99pub mod converter;
100pub mod renderer;
101pub mod theme;
102
103pub use renderer::{
104 CodeBlockFn, FootnoteRefFn, HeadingFn, ImageFn, InlineCodeFn, LinkFn, Renderer,
105 RendererBuilder, RuleFn,
106};
107pub use theme::Theme;
108
109use ratatui_core::text::Text;
110
111// ── Public API ────────────────────────────────────────────────────────────────
112
113/// Convert Markdown to [`Text`] using the default [`Theme`] and no custom
114/// element renderers.
115///
116/// Equivalent to `into_text_with_renderer(markdown, &RendererBuilder::new().build())`.
117///
118/// ```rust
119/// use the_other_tui_markdown::into_text;
120///
121/// let text = into_text("**Hello**, _world_!");
122/// assert!(!text.lines.is_empty());
123/// ```
124pub fn into_text(markdown: &str) -> Text<'static> {
125 let renderer = RendererBuilder::new().build();
126 into_text_with_renderer(markdown, &renderer)
127}
128
129/// Convert Markdown to [`Text`] using the default [`Theme`] and a custom
130/// theme override, but no custom element renderers.
131///
132/// This is a convenience shorthand for:
133/// ```rust,ignore
134/// RendererBuilder::new().with_theme(theme).build()
135/// ```
136///
137/// ```rust
138/// use the_other_tui_markdown::{Theme, into_text_with_theme};
139/// use ratatui_core::style::{Color, Style};
140///
141/// let mut theme = Theme::default();
142/// theme.h1 = Style::new().fg(Color::Red);
143///
144/// let text = into_text_with_theme("# Red heading", theme);
145/// ```
146pub fn into_text_with_theme(markdown: &str, theme: Theme) -> Text<'static> {
147 let renderer = RendererBuilder::new().with_theme(theme).build();
148 into_text_with_renderer(markdown, &renderer)
149}
150
151/// Convert Markdown to [`Text`] using a fully configured [`Renderer`].
152///
153/// This is the primary entry point when you need custom per-element rendering.
154///
155/// ```rust
156/// use the_other_tui_markdown::{RendererBuilder, into_text_with_renderer};
157/// use ratatui_core::text::Span;
158///
159/// let renderer = RendererBuilder::new()
160/// .with_link(|alt, url| vec", alt, url))])
161/// .build();
162///
163/// let text = into_text_with_renderer("[example](https://example.com)", &renderer);
164/// assert!(!text.lines.is_empty());
165/// ```
166pub fn into_text_with_renderer(markdown: &str, renderer: &Renderer) -> Text<'static> {
167 let mut conv = converter::Converter::new(renderer);
168 conv.convert(markdown)
169}