mdx/
component.rs

1use crate::error::Error;
2use crate::parser::{Alignment, InlineNode, Node};
3use std::collections::HashMap;
4
5/// Trait for custom markdown components
6pub trait Component {
7    /// The name of the component as used in markdown
8    fn name(&self) -> &str;
9
10    /// Render the component to HTML
11    fn render(
12        &self,
13        attributes: &HashMap<String, String>,
14        children: &[Node],
15    ) -> Result<String, Error>;
16
17    /// Process any CSS needed for the component
18    fn css(&self) -> Option<String> {
19        None
20    }
21}
22
23/// Registry for custom components
24pub struct ComponentRegistry {
25    components: HashMap<String, Box<dyn Component>>,
26}
27
28impl ComponentRegistry {
29    /// Create a new component registry
30    pub fn new() -> Self {
31        Self {
32            components: HashMap::new(),
33        }
34    }
35
36    /// Register a component
37    pub fn register<C: Component + 'static>(&mut self, component: C) {
38        self.components
39            .insert(component.name().to_string(), Box::new(component));
40    }
41
42    /// Get a component by name
43    pub fn get(&self, name: &str) -> Option<&dyn Component> {
44        self.components.get(name).map(|c| c.as_ref())
45    }
46
47    /// Check if a component exists
48    pub fn contains(&self, name: &str) -> bool {
49        self.components.contains_key(name)
50    }
51
52    /// Get all CSS for registered components
53    pub fn get_all_css(&self) -> String {
54        self.components
55            .values()
56            .filter_map(|c| c.css())
57            .collect::<Vec<_>>()
58            .join("\n\n")
59    }
60}
61
62// Helper function to render nodes to HTML
63fn render_nodes_to_html(nodes: &[Node]) -> Result<String, Error> {
64    let mut html = String::new();
65
66    for node in nodes {
67        match node {
68            Node::Paragraph(inline_nodes) => {
69                html.push_str("<p>");
70                for inline_node in inline_nodes {
71                    html.push_str(&render_inline_node(inline_node)?);
72                }
73                html.push_str("</p>\n");
74            }
75            Node::Heading { level, content, id } => {
76                html.push_str(&format!(
77                    "<h{0} id=\"{1}\">{2}</h{0}>\n",
78                    level, id, content
79                ));
80            }
81            Node::BlockQuote(children) => {
82                html.push_str("<blockquote>\n");
83                html.push_str(&render_nodes_to_html(children)?);
84                html.push_str("</blockquote>\n");
85            }
86            Node::CodeBlock {
87                language, content, ..
88            } => {
89                if let Some(lang) = language {
90                    html.push_str(&format!(
91                        "<pre><code class=\"language-{}\">{}\n</code></pre>\n",
92                        lang, content
93                    ));
94                } else {
95                    html.push_str(&format!("<pre><code>{}\n</code></pre>\n", content));
96                }
97            }
98            Node::List { ordered, items } => {
99                if *ordered {
100                    html.push_str("<ol>\n");
101                } else {
102                    html.push_str("<ul>\n");
103                }
104
105                for item in items {
106                    html.push_str("<li>");
107                    html.push_str(&render_nodes_to_html(item)?);
108                    html.push_str("</li>\n");
109                }
110
111                if *ordered {
112                    html.push_str("</ol>\n");
113                } else {
114                    html.push_str("</ul>\n");
115                }
116            }
117            Node::ThematicBreak => {
118                html.push_str("<hr>\n");
119            }
120            Node::Html(content) => {
121                html.push_str(content);
122                html.push('\n');
123            }
124            Node::Table {
125                headers,
126                rows,
127                alignments,
128            } => {
129                html.push_str("<table class=\"markrust-table\">\n");
130
131                // Table header
132                html.push_str("<thead>\n<tr>\n");
133                for (i, header) in headers.iter().enumerate() {
134                    let align_class = if i < alignments.len() {
135                        match alignments[i] {
136                            Alignment::Left => " class=\"align-left\"",
137                            Alignment::Center => " class=\"align-center\"",
138                            Alignment::Right => " class=\"align-right\"",
139                            Alignment::None => "",
140                        }
141                    } else {
142                        ""
143                    };
144
145                    html.push_str(&format!("<th{}>\n", align_class));
146                    for inline_node in header {
147                        html.push_str(&render_inline_node(inline_node)?);
148                    }
149                    html.push_str("</th>\n");
150                }
151                html.push_str("</tr>\n</thead>\n");
152
153                // Table body
154                html.push_str("<tbody>\n");
155                for row in rows {
156                    html.push_str("<tr>\n");
157                    for (i, cell) in row.iter().enumerate() {
158                        let align_class = if i < alignments.len() {
159                            match alignments[i] {
160                                Alignment::Left => " class=\"align-left\"",
161                                Alignment::Center => " class=\"align-center\"",
162                                Alignment::Right => " class=\"align-right\"",
163                                Alignment::None => "",
164                            }
165                        } else {
166                            ""
167                        };
168
169                        html.push_str(&format!("<td{}>\n", align_class));
170                        for inline_node in cell {
171                            html.push_str(&render_inline_node(inline_node)?);
172                        }
173                        html.push_str("</td>\n");
174                    }
175                    html.push_str("</tr>\n");
176                }
177                html.push_str("</tbody>\n</table>\n");
178            }
179            Node::Component {
180                name: _,
181                attributes: _,
182                children,
183            } => {
184                // This typically would use the component registry but here we'll just render the children
185                html.push_str(&render_nodes_to_html(children)?);
186            }
187        }
188    }
189
190    Ok(html)
191}
192
193// Helper function to render inline nodes to HTML
194fn render_inline_node(node: &InlineNode) -> Result<String, Error> {
195    match node {
196        InlineNode::Text(text) => Ok(text.to_string()),
197        InlineNode::Emphasis(children) => {
198            let mut html = String::from("<em>");
199            for child in children {
200                html.push_str(&render_inline_node(child)?);
201            }
202            html.push_str("</em>");
203            Ok(html)
204        }
205        InlineNode::Strong(children) => {
206            let mut html = String::from("<strong>");
207            for child in children {
208                html.push_str(&render_inline_node(child)?);
209            }
210            html.push_str("</strong>");
211            Ok(html)
212        }
213        InlineNode::Strikethrough(children) => {
214            let mut html = String::from("<del>");
215            for child in children {
216                html.push_str(&render_inline_node(child)?);
217            }
218            html.push_str("</del>");
219            Ok(html)
220        }
221        InlineNode::Link { text, url, title } => {
222            let title_attr = if let Some(title) = title {
223                format!(" title=\"{}\"", title)
224            } else {
225                String::new()
226            };
227
228            let mut html = format!("<a href=\"{}\"{}>", url, title_attr);
229            for child in text {
230                html.push_str(&render_inline_node(child)?);
231            }
232            html.push_str("</a>");
233            Ok(html)
234        }
235        InlineNode::Image { alt, url, title } => {
236            let title_attr = if let Some(title) = title {
237                format!(" title=\"{}\"", title)
238            } else {
239                String::new()
240            };
241
242            Ok(format!(
243                "<img src=\"{}\" alt=\"{}\"{}>",
244                url, alt, title_attr
245            ))
246        }
247        InlineNode::Code(code) => Ok(format!("<code>{}</code>", code)),
248        InlineNode::LineBreak => Ok(String::from("<br>")),
249        InlineNode::Html(html) => Ok(html.to_string()),
250    }
251}
252
253impl Default for ComponentRegistry {
254    fn default() -> Self {
255        let mut registry = Self::new();
256
257        // Register built-in components
258        registry.register(AlertComponent::new());
259        registry.register(TabsComponent::new());
260        registry.register(DetailsComponent::new());
261        registry.register(CardComponent::new());
262        registry.register(TimelineComponent::new());
263        registry.register(CalloutComponent::new());
264        registry.register(GridComponent::new());
265
266        registry
267    }
268}
269
270// Built-in component implementations
271
272/// Alert component for displaying info, warning, success or error messages
273pub struct AlertComponent;
274
275impl Default for AlertComponent {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281impl AlertComponent {
282    pub fn new() -> Self {
283        Self
284    }
285}
286
287impl Component for AlertComponent {
288    fn name(&self) -> &str {
289        "alert"
290    }
291
292    fn render(
293        &self,
294        attributes: &HashMap<String, String>,
295        children: &[Node],
296    ) -> Result<String, Error> {
297        let binding = "info".to_string();
298        let alert_type = attributes.get("type").unwrap_or(&binding);
299
300        // Render the children to HTML using a simple renderer
301        let content = if !children.is_empty() {
302            render_nodes_to_html(children)?
303        } else {
304            // For HTML comment-based components where children can't be properly passed
305            "This is an alert. In a real implementation, this would contain content.".to_string()
306        };
307
308        Ok(format!(
309            r#"<div class="markrust-alert markrust-alert-{}">{}</div>"#,
310            alert_type, content
311        ))
312    }
313
314    fn css(&self) -> Option<String> {
315        Some(
316            r#"
317/* Alert component styles */
318.markrust-alert {
319  padding: 1rem;
320  border-radius: 0.375rem;
321  margin-bottom: 1rem;
322}
323
324.markrust-alert-info {
325  background-color: var(--info-light-color, #e0f7fa);
326  border-left: 4px solid var(--info-color, #03a9f4);
327}
328
329.markrust-alert-warning {
330  background-color: var(--warning-light-color, #fff8e1);
331  border-left: 4px solid var(--warning-color, #ffc107);
332}
333
334.markrust-alert-error {
335  background-color: var(--error-light-color, #ffebee);
336  border-left: 4px solid var(--error-color, #f44336);
337}
338
339.markrust-alert-success {
340  background-color: var(--success-light-color, #e8f5e9);
341  border-left: 4px solid var(--success-color, #4caf50);
342}
343        "#
344            .to_string(),
345        )
346    }
347}
348
349/// Tabs component for tabbed content
350pub struct TabsComponent;
351
352impl Default for TabsComponent {
353    fn default() -> Self {
354        Self::new()
355    }
356}
357
358impl TabsComponent {
359    pub fn new() -> Self {
360        Self
361    }
362}
363
364impl Component for TabsComponent {
365    fn name(&self) -> &str {
366        "tabs"
367    }
368
369    fn render(
370        &self,
371        _attributes: &HashMap<String, String>,
372        children: &[Node],
373    ) -> Result<String, Error> {
374        // Process children to extract tab contents
375        let mut tab_headers = Vec::new();
376        let mut tab_contents = Vec::new();
377        let mut tab_id = 1;
378
379        if !children.is_empty() {
380            for child in children {
381                if let Node::Component {
382                    name,
383                    attributes,
384                    children,
385                } = child
386                {
387                    if name == "tab" {
388                        let tab_label = format!("Tab {}", tab_id);
389                        let label = attributes.get("label").unwrap_or(&tab_label);
390                        let tab_id_str = format!("tab{}", tab_id);
391
392                        let active_class = if tab_id == 1 { "active" } else { "" };
393                        tab_headers.push(format!(
394                            r#"<button class="markrust-tab-button {}" data-tab="{}">{}"#,
395                            active_class, tab_id_str, label
396                        ));
397
398                        let content = render_nodes_to_html(children)?;
399                        tab_contents.push(format!(
400                            r#"<div class="markrust-tab-panel {}" id="{}">{}"#,
401                            active_class, tab_id_str, content
402                        ));
403
404                        tab_id += 1;
405                    }
406                }
407            }
408        } else {
409            // Default tabs when no children are provided
410            tab_headers.push(
411                r#"<button class="markrust-tab-button active" data-tab="tab1">Overview</button>"#
412                    .to_string(),
413            );
414            tab_headers.push(
415                r#"<button class="markrust-tab-button" data-tab="tab2">Details</button>"#
416                    .to_string(),
417            );
418
419            tab_contents.push(r#"<div class="markrust-tab-panel active" id="tab1">Tab content for overview.</div>"#.to_string());
420            tab_contents.push(
421                r#"<div class="markrust-tab-panel" id="tab2">Tab content for details.</div>"#
422                    .to_string(),
423            );
424        }
425
426        Ok(format!(
427            r#"<div class="markrust-tabs">
428  <div class="markrust-tabs-header">
429    {}
430  </div>
431  <div class="markrust-tabs-content">
432    {}
433  </div>
434</div>"#,
435            tab_headers.join("\n    "),
436            tab_contents.join("\n    ")
437        ))
438    }
439
440    fn css(&self) -> Option<String> {
441        Some(
442            r#"
443/* Tabs component styles */
444.markrust-tabs {
445  margin-bottom: 1.5rem;
446}
447
448.markrust-tabs-header {
449  display: flex;
450  border-bottom: 1px solid var(--border-color, #e5e7eb);
451}
452
453.markrust-tab-button {
454  padding: 0.5rem 1rem;
455  border: none;
456  background: none;
457  cursor: pointer;
458  font-weight: 500;
459  color: var(--muted-color, #6b7280);
460}
461
462.markrust-tab-button.active {
463  color: var(--primary-color, #3b82f6);
464  border-bottom: 2px solid var(--primary-color, #3b82f6);
465}
466
467.markrust-tab-panel {
468  display: none;
469  padding: 1rem 0;
470}
471
472.markrust-tab-panel.active {
473  display: block;
474}
475        "#
476            .to_string(),
477        )
478    }
479}
480
481/// Details component for collapsible content
482pub struct DetailsComponent;
483
484impl Default for DetailsComponent {
485    fn default() -> Self {
486        Self::new()
487    }
488}
489
490impl DetailsComponent {
491    pub fn new() -> Self {
492        Self
493    }
494}
495
496impl Component for DetailsComponent {
497    fn name(&self) -> &str {
498        "details"
499    }
500
501    fn render(
502        &self,
503        attributes: &HashMap<String, String>,
504        children: &[Node],
505    ) -> Result<String, Error> {
506        let binding = "Details".to_string();
507        let summary = attributes.get("summary").unwrap_or(&binding);
508
509        // Render the children to HTML
510        let content = if !children.is_empty() {
511            render_nodes_to_html(children)?
512        } else {
513            // For HTML comment-based components where children can't be properly passed
514            "This content is initially hidden and can be expanded by clicking the summary."
515                .to_string()
516        };
517
518        Ok(format!(
519            r#"<details class="markrust-details">
520  <summary>{}</summary>
521  <div class="markrust-details-content">
522    {}
523  </div>
524</details>"#,
525            summary, content
526        ))
527    }
528
529    fn css(&self) -> Option<String> {
530        Some(
531            r#"
532/* Details component styles */
533.markrust-details {
534  margin-bottom: 1rem;
535  border: 1px solid var(--border-color, #e5e7eb);
536  border-radius: 0.375rem;
537}
538
539.markrust-details summary {
540  padding: 0.75rem 1rem;
541  cursor: pointer;
542  font-weight: 500;
543}
544
545.markrust-details-content {
546  padding: 1rem;
547  border-top: 1px solid var(--border-color, #e5e7eb);
548}
549        "#
550            .to_string(),
551        )
552    }
553}
554
555/// Card component for displaying content in a card layout
556pub struct CardComponent;
557
558impl Default for CardComponent {
559    fn default() -> Self {
560        Self::new()
561    }
562}
563
564impl CardComponent {
565    pub fn new() -> Self {
566        Self
567    }
568}
569
570impl Component for CardComponent {
571    fn name(&self) -> &str {
572        "card"
573    }
574
575    fn render(
576        &self,
577        attributes: &HashMap<String, String>,
578        children: &[Node],
579    ) -> Result<String, Error> {
580        let binding = "".to_string();
581        let title = attributes.get("title").unwrap_or(&binding);
582
583        // Render the children to HTML
584        let content = if !children.is_empty() {
585            render_nodes_to_html(children)?
586        } else {
587            // For HTML comment-based components where children can't be properly passed
588            "This card highlights a key feature of the project.".to_string()
589        };
590
591        let title_html = if !title.is_empty() {
592            format!(r#"<div class="markrust-card-header">{}</div>"#, title)
593        } else {
594            String::new()
595        };
596
597        Ok(format!(
598            r#"<div class="markrust-card">
599  {}
600  <div class="markrust-card-body">
601    {}
602  </div>
603</div>"#,
604            title_html, content
605        ))
606    }
607
608    fn css(&self) -> Option<String> {
609        Some(
610            r#"
611/* Card component styles */
612.markrust-card {
613  border: 1px solid var(--border-color, #e5e7eb);
614  border-radius: 0.375rem;
615  margin-bottom: 1.5rem;
616  overflow: hidden;
617}
618
619.markrust-card-header {
620  padding: 1rem;
621  background-color: var(--card-header-bg, #f9fafb);
622  border-bottom: 1px solid var(--border-color, #e5e7eb);
623  font-weight: 600;
624}
625
626.markrust-card-body {
627  padding: 1rem;
628}
629        "#
630            .to_string(),
631        )
632    }
633}
634
635/// Timeline component for displaying events in chronological order
636pub struct TimelineComponent;
637
638impl Default for TimelineComponent {
639    fn default() -> Self {
640        Self::new()
641    }
642}
643
644impl TimelineComponent {
645    pub fn new() -> Self {
646        Self
647    }
648}
649
650impl Component for TimelineComponent {
651    fn name(&self) -> &str {
652        "timeline"
653    }
654
655    fn render(
656        &self,
657        _attributes: &HashMap<String, String>,
658        children: &[Node],
659    ) -> Result<String, Error> {
660        // Process children to extract timeline items
661        let mut timeline_items = Vec::new();
662
663        if !children.is_empty() {
664            for child in children {
665                if let Node::Component {
666                    name,
667                    attributes,
668                    children,
669                } = child
670                {
671                    if name == "timeline-item" {
672                        let default_empty = String::new();
673                        let date = attributes.get("date").unwrap_or(&default_empty);
674                        let title = attributes.get("title").unwrap_or(&default_empty);
675
676                        let content = render_nodes_to_html(children)?;
677
678                        timeline_items.push(format!(
679                            r#"<div class="markrust-timeline-item">
680    <div class="markrust-timeline-date">{}</div>
681    <div class="markrust-timeline-content">
682      <h4 class="markrust-timeline-title">{}</h4>
683      <div class="markrust-timeline-body">{}</div>
684    </div>
685  </div>"#,
686                            date, title, content
687                        ));
688                    }
689                }
690            }
691        } else {
692            // Default items when no children are provided
693            timeline_items.push(
694                r#"<div class="markrust-timeline-item">
695    <div class="markrust-timeline-date">January 2023</div>
696    <div class="markrust-timeline-content">
697      <h4 class="markrust-timeline-title">Project Started</h4>
698      <div class="markrust-timeline-body">Initial development of the project began.</div>
699    </div>
700  </div>"#
701                    .to_string(),
702            );
703
704            timeline_items.push(
705                r#"<div class="markrust-timeline-item">
706    <div class="markrust-timeline-date">March 2023</div>
707    <div class="markrust-timeline-content">
708      <h4 class="markrust-timeline-title">Beta Release</h4>
709      <div class="markrust-timeline-body">First beta version released to testers.</div>
710    </div>
711  </div>"#
712                    .to_string(),
713            );
714        }
715
716        Ok(format!(
717            r#"<div class="markrust-timeline">
718  {}
719</div>"#,
720            timeline_items.join("\n  ")
721        ))
722    }
723
724    fn css(&self) -> Option<String> {
725        Some(
726            r#"
727/* Timeline component styles */
728.markrust-timeline {
729  position: relative;
730  padding-left: 2rem;
731}
732
733.markrust-timeline::before {
734  content: '';
735  position: absolute;
736  top: 0;
737  bottom: 0;
738  left: 0;
739  width: 2px;
740  background-color: var(--border-color, #e5e7eb);
741}
742
743.markrust-timeline-item {
744  position: relative;
745  padding-bottom: 2rem;
746}
747
748.markrust-timeline-item::before {
749  content: '';
750  position: absolute;
751  left: -2rem;
752  top: 0.25rem;
753  width: 1rem;
754  height: 1rem;
755  border-radius: 50%;
756  background-color: var(--primary-color, #3b82f6);
757  border: 2px solid #fff;
758}
759
760.markrust-timeline-date {
761  color: var(--muted-color, #6b7280);
762  font-size: 0.875rem;
763  margin-bottom: 0.25rem;
764}
765
766.markrust-timeline-title {
767  margin-top: 0;
768  margin-bottom: 0.5rem;
769}
770        "#
771            .to_string(),
772        )
773    }
774}
775
776/// Callout component for highlighting important information
777pub struct CalloutComponent;
778
779impl Default for CalloutComponent {
780    fn default() -> Self {
781        Self::new()
782    }
783}
784
785impl CalloutComponent {
786    pub fn new() -> Self {
787        Self
788    }
789}
790
791impl Component for CalloutComponent {
792    fn name(&self) -> &str {
793        "callout"
794    }
795
796    fn render(
797        &self,
798        attributes: &HashMap<String, String>,
799        children: &[Node],
800    ) -> Result<String, Error> {
801        let title_binding = "".to_string();
802        let title = attributes.get("title").unwrap_or(&title_binding);
803
804        let variant_binding = "info".to_string();
805        let variant = attributes.get("variant").unwrap_or(&variant_binding);
806
807        // Render the children to HTML
808        let content = if !children.is_empty() {
809            render_nodes_to_html(children)?
810        } else {
811            // For HTML comment-based components where children can't be properly passed
812            "This is an important note about the functionality.".to_string()
813        };
814
815        let title_html = if !title.is_empty() {
816            format!(r#"<div class="markrust-callout-title">{}</div>"#, title)
817        } else {
818            String::new()
819        };
820
821        Ok(format!(
822            r#"<div class="markrust-callout markrust-callout-{}">
823  {}
824  <div class="markrust-callout-content">
825    {}
826  </div>
827</div>"#,
828            variant, title_html, content
829        ))
830    }
831
832    fn css(&self) -> Option<String> {
833        Some(
834            r#"
835/* Callout component styles */
836.markrust-callout {
837  padding: 1rem;
838  border-radius: 0.375rem;
839  margin-bottom: 1rem;
840  border-left: 4px solid var(--border-color, #e5e7eb);
841}
842
843.markrust-callout-title {
844  font-weight: 600;
845  margin-bottom: 0.5rem;
846}
847
848.markrust-callout-info {
849  background-color: var(--info-light-color, #e0f7fa);
850  border-left-color: var(--info-color, #03a9f4);
851}
852
853.markrust-callout-warning {
854  background-color: var(--warning-light-color, #fff8e1);
855  border-left-color: var(--warning-color, #ffc107);
856}
857
858.markrust-callout-error {
859  background-color: var(--error-light-color, #ffebee);
860  border-left-color: var(--error-color, #f44336);
861}
862
863.markrust-callout-success {
864  background-color: var(--success-light-color, #e8f5e9);
865  border-left-color: var(--success-color, #4caf50);
866}
867        "#
868            .to_string(),
869        )
870    }
871}
872
873/// Grid component for creating responsive grid layouts
874pub struct GridComponent;
875
876impl Default for GridComponent {
877    fn default() -> Self {
878        Self::new()
879    }
880}
881
882impl GridComponent {
883    pub fn new() -> Self {
884        Self
885    }
886}
887
888impl Component for GridComponent {
889    fn name(&self) -> &str {
890        "grid"
891    }
892
893    fn render(
894        &self,
895        attributes: &HashMap<String, String>,
896        children: &[Node],
897    ) -> Result<String, Error> {
898        let columns_binding = "2".to_string();
899        let columns = attributes.get("columns").unwrap_or(&columns_binding);
900
901        let gap_binding = "1rem".to_string();
902        let gap = attributes.get("gap").unwrap_or(&gap_binding);
903
904        // Process children to extract grid items
905        let mut grid_items = Vec::new();
906
907        if !children.is_empty() {
908            for child in children {
909                if let Node::Component {
910                    name,
911                    attributes: _,
912                    children: item_children,
913                } = child
914                {
915                    if name == "grid-item" {
916                        let content = render_nodes_to_html(item_children)?;
917                        grid_items.push(format!(
918                            r#"<div class="markrust-grid-item">{}</div>"#,
919                            content
920                        ));
921                    }
922                }
923            }
924        } else {
925            // Default grid items when no children are provided
926            grid_items.push(
927                r#"<div class="markrust-grid-item">
928      <h4>Feature 1</h4>
929      <p>Description of feature 1.</p>
930    </div>"#
931                    .to_string(),
932            );
933
934            grid_items.push(
935                r#"<div class="markrust-grid-item">
936      <h4>Feature 2</h4>
937      <p>Description of feature 2.</p>
938    </div>"#
939                    .to_string(),
940            );
941
942            if columns.parse::<i32>().unwrap_or(2) > 2 {
943                grid_items.push(
944                    r#"<div class="markrust-grid-item">
945      <h4>Feature 3</h4>
946      <p>Description of feature 3.</p>
947    </div>"#
948                        .to_string(),
949                );
950            }
951        }
952
953        Ok(format!(
954            r#"<div class="markrust-grid" style="--grid-columns: {}; --grid-gap: {};">
955    {}
956</div>"#,
957            columns,
958            gap,
959            grid_items.join("\n    ")
960        ))
961    }
962
963    fn css(&self) -> Option<String> {
964        Some(
965            r#"
966/* Grid component styles */
967.markrust-grid {
968  display: grid;
969  grid-template-columns: repeat(var(--grid-columns, 2), 1fr);
970  gap: var(--grid-gap, 1rem);
971  margin-bottom: 1.5rem;
972}
973
974@media (max-width: 768px) {
975  .markrust-grid {
976    grid-template-columns: 1fr;
977  }
978}
979
980.markrust-grid-item {
981  min-width: 0;
982}
983        "#
984            .to_string(),
985        )
986    }
987}