1use crate::error::Error;
2use crate::parser::{Alignment, InlineNode, Node};
3use std::collections::HashMap;
4
5pub trait Component {
7 fn name(&self) -> &str;
9
10 fn render(
12 &self,
13 attributes: &HashMap<String, String>,
14 children: &[Node],
15 ) -> Result<String, Error>;
16
17 fn css(&self) -> Option<String> {
19 None
20 }
21}
22
23pub struct ComponentRegistry {
25 components: HashMap<String, Box<dyn Component>>,
26}
27
28impl ComponentRegistry {
29 pub fn new() -> Self {
31 Self {
32 components: HashMap::new(),
33 }
34 }
35
36 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 pub fn get(&self, name: &str) -> Option<&dyn Component> {
44 self.components.get(name).map(|c| c.as_ref())
45 }
46
47 pub fn contains(&self, name: &str) -> bool {
49 self.components.contains_key(name)
50 }
51
52 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
62fn 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 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 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 html.push_str(&render_nodes_to_html(children)?);
186 }
187 }
188 }
189
190 Ok(html)
191}
192
193fn 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 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
270pub 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 let content = if !children.is_empty() {
302 render_nodes_to_html(children)?
303 } else {
304 "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
349pub 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 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 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
481pub 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 let content = if !children.is_empty() {
511 render_nodes_to_html(children)?
512 } else {
513 "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
555pub 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 let content = if !children.is_empty() {
585 render_nodes_to_html(children)?
586 } else {
587 "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
635pub 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 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 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
776pub 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 let content = if !children.is_empty() {
809 render_nodes_to_html(children)?
810 } else {
811 "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
873pub 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 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 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}