Skip to main content

leptos_md/
components.rs

1use leptos::prelude::*;
2
3#[derive(Clone, Debug, Default, PartialEq, Eq)]
4pub enum CodeBlockTheme {
5    #[default]
6    Default,
7    Dark,
8    Light,
9    GitHub,
10    Monokai,
11}
12
13#[derive(Clone, Debug)]
14pub struct MarkdownOptions {
15    pub enable_gfm: bool,
16    /// Code block theme. `Some(theme)` applies Tailwind styling, `None` outputs no theme classes.
17    pub code_theme: Option<CodeBlockTheme>,
18    /// Whether to emit `language-xxx` classes on code blocks (for external syntax highlighters).
19    pub syntax_highlighting_language_classes: bool,
20    pub open_links_in_new_tab: bool,
21    pub allow_raw_html: bool,
22    /// Use explicit Tailwind utility classes on each element instead of relying on prose.
23    /// When `false` (default), relies on Tailwind's `prose` classes for styling.
24    /// When `true`, applies `MarkdownClasses::*` constants directly to elements.
25    pub use_explicit_classes: bool,
26}
27
28impl Default for MarkdownOptions {
29    fn default() -> Self {
30        Self {
31            enable_gfm: true,
32            code_theme: Some(CodeBlockTheme::default()),
33            syntax_highlighting_language_classes: true,
34            open_links_in_new_tab: true,
35            allow_raw_html: true,
36            use_explicit_classes: false,
37        }
38    }
39}
40
41impl MarkdownOptions {
42    /// Create a new MarkdownOptions with default values
43    #[must_use]
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Enable or disable GitHub Flavored Markdown features
49    #[must_use]
50    pub fn with_gfm(mut self, enable: bool) -> Self {
51        self.enable_gfm = enable;
52        self
53    }
54
55    /// Set the code block theme (applies Tailwind styling)
56    #[must_use]
57    pub fn with_code_theme(mut self, theme: CodeBlockTheme) -> Self {
58        self.code_theme = Some(theme);
59        self
60    }
61
62    /// Disable code block theme styling (useful when using external syntax highlighters)
63    #[must_use]
64    pub fn without_code_theme(mut self) -> Self {
65        self.code_theme = None;
66        self
67    }
68
69    /// Enable or disable `language-xxx` classes on code blocks
70    #[must_use]
71    pub fn with_language_classes(mut self, enable: bool) -> Self {
72        self.syntax_highlighting_language_classes = enable;
73        self
74    }
75
76    /// Configure whether links open in new tabs
77    #[must_use]
78    pub fn with_new_tab_links(mut self, enable: bool) -> Self {
79        self.open_links_in_new_tab = enable;
80        self
81    }
82
83    /// Configure whether raw HTML in markdown is rendered
84    #[must_use]
85    pub fn with_allow_raw_html(mut self, enable: bool) -> Self {
86        self.allow_raw_html = enable;
87        self
88    }
89
90    /// Use explicit Tailwind utility classes on each element instead of relying on prose.
91    /// When `false` (default), relies on Tailwind's `prose` classes for styling.
92    /// When `true`, applies `MarkdownClasses::*` constants directly to elements.
93    #[must_use]
94    pub fn with_explicit_classes(mut self, enable: bool) -> Self {
95        self.use_explicit_classes = enable;
96        self
97    }
98}
99
100/// Tailwind CSS class names for markdown elements
101pub struct MarkdownClasses;
102
103impl MarkdownClasses {
104    // Base wrapper
105    pub const CONTENT: &'static str =
106        "leptos-mdx-content prose prose-gray max-w-none dark:prose-invert";
107
108    // Headings
109    pub const H1: &'static str =
110        "text-3xl font-bold text-gray-900 dark:text-gray-100 mt-6 mb-4 first:mt-0";
111    pub const H2: &'static str =
112        "text-2xl font-semibold text-gray-900 dark:text-gray-100 mt-5 mb-3";
113    pub const H3: &'static str = "text-xl font-semibold text-gray-900 dark:text-gray-100 mt-4 mb-2";
114    pub const H4: &'static str = "text-lg font-medium text-gray-900 dark:text-gray-100 mt-3 mb-2";
115    pub const H5: &'static str = "text-base font-medium text-gray-900 dark:text-gray-100 mt-3 mb-2";
116    pub const H6: &'static str = "text-sm font-medium text-gray-600 dark:text-gray-400 mt-3 mb-2";
117
118    // Text elements
119    pub const PARAGRAPH: &'static str = "mb-4 leading-relaxed text-gray-700 dark:text-gray-300";
120    pub const BLOCKQUOTE: &'static str = "border-l-4 border-blue-500 pl-4 py-2 my-4 bg-blue-50 dark:bg-blue-950/30 text-gray-700 dark:text-gray-300 italic";
121
122    // Code
123    pub const INLINE_CODE: &'static str = "bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 px-1.5 py-0.5 rounded text-sm font-mono";
124    pub const CODE_BLOCK: &'static str = "bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg p-4 my-4 overflow-x-auto";
125    pub const CODE_BLOCK_CODE: &'static str =
126        "font-mono text-sm leading-relaxed text-gray-800 dark:text-gray-200";
127
128    // Lists
129    pub const UL: &'static str =
130        "list-disc list-inside mb-4 space-y-1 text-gray-700 dark:text-gray-300";
131    pub const OL: &'static str =
132        "list-decimal list-inside mb-4 space-y-1 text-gray-700 dark:text-gray-300";
133    pub const LI: &'static str = "leading-relaxed";
134
135    // Links and images
136    pub const LINK: &'static str = "text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline underline-offset-2 hover:underline-offset-4 transition-all";
137    pub const IMAGE: &'static str = "max-w-full h-auto rounded-lg shadow-sm my-4";
138
139    // Tables
140    pub const TABLE: &'static str = "min-w-full divide-y divide-gray-200 dark:divide-gray-700 my-4 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden";
141    pub const THEAD: &'static str = "bg-gray-50 dark:bg-gray-800";
142    pub const TR: &'static str =
143        "bg-white dark:bg-gray-900 even:bg-gray-50 dark:even:bg-gray-800/50";
144    pub const TD: &'static str = "px-6 py-4 text-sm text-gray-900 dark:text-gray-100";
145    pub const TH: &'static str = "px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider";
146
147    // Other elements
148    pub const HR: &'static str = "border-0 h-px bg-gradient-to-r from-transparent via-gray-300 dark:via-gray-600 to-transparent my-8";
149    pub const CHECKBOX: &'static str = "mr-2 accent-blue-600";
150
151    // Math
152    pub const MATH_INLINE: &'static str = "font-serif italic text-gray-800 dark:text-gray-200";
153    pub const MATH_DISPLAY: &'static str = "font-serif italic text-center my-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg text-gray-800 dark:text-gray-200";
154
155    // Definition lists
156    pub const DL: &'static str = "my-4";
157    pub const DT: &'static str = "font-semibold text-gray-900 dark:text-gray-100 mt-4 first:mt-0";
158    pub const DD: &'static str = "ml-6 mb-2 text-gray-700 dark:text-gray-300";
159
160    // Superscript/Subscript
161    pub const SUP: &'static str = "text-xs align-super";
162    pub const SUB: &'static str = "text-xs align-sub";
163
164    // Emphasis
165    pub const EM: &'static str = "italic";
166    pub const STRONG: &'static str = "font-bold";
167    pub const DEL: &'static str = "line-through text-gray-500 dark:text-gray-400";
168
169    // Special elements
170    pub const FOOTNOTE_REF: &'static str = "text-xs align-super text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300";
171    pub const FOOTNOTE_DEF: &'static str = "text-sm border-t border-gray-200 dark:border-gray-700 mt-8 pt-4 text-gray-600 dark:text-gray-400";
172    pub const RAW_HTML_BLOCK: &'static str = "bg-yellow-50 dark:bg-yellow-950/30 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 my-4 font-mono text-sm text-yellow-800 dark:text-yellow-200 whitespace-pre-wrap";
173    pub const INLINE_HTML: &'static str = "bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-200 px-2 py-1 rounded text-xs font-mono border border-yellow-300 dark:border-yellow-700";
174
175    // Theme-specific code block classes
176    pub const THEME_DEFAULT: &'static str = "bg-gray-50 dark:bg-gray-900";
177    pub const THEME_DARK: &'static str = "bg-gray-900 text-gray-100";
178    pub const THEME_LIGHT: &'static str = "bg-white text-gray-900 border";
179    pub const THEME_GITHUB: &'static str =
180        "bg-[#f6f8fa] dark:bg-[#0d1117] text-[#24292f] dark:text-[#f0f6fc]";
181    pub const THEME_MONOKAI: &'static str = "bg-[#272822] text-[#f8f8f2]";
182}
183
184/// Get theme-specific classes for code blocks
185pub fn get_code_theme_classes(theme: &CodeBlockTheme) -> &'static str {
186    match theme {
187        CodeBlockTheme::Default => MarkdownClasses::THEME_DEFAULT,
188        CodeBlockTheme::Dark => MarkdownClasses::THEME_DARK,
189        CodeBlockTheme::Light => MarkdownClasses::THEME_LIGHT,
190        CodeBlockTheme::GitHub => MarkdownClasses::THEME_GITHUB,
191        CodeBlockTheme::Monokai => MarkdownClasses::THEME_MONOKAI,
192    }
193}
194
195/// Enhanced Tailwind prose configuration for better markdown styling
196pub fn get_enhanced_prose_classes() -> &'static str {
197    "leptos-mdx-content prose prose-gray max-w-none dark:prose-invert prose-headings:font-bold prose-headings:text-gray-900 dark:prose-headings:text-gray-100 prose-p:text-gray-700 dark:prose-p:text-gray-300 prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-strong:text-gray-900 dark:prose-strong:text-gray-100 prose-code:text-gray-800 dark:prose-code:text-gray-200 prose-pre:bg-gray-50 dark:prose-pre:bg-gray-900"
198}
199
200/// Placeholder component - Tailwind handles all styling
201#[component]
202pub fn MarkdownStyles() -> impl IntoView {
203    // With Tailwind 4, no custom styles needed
204    ""
205}