1use crate::error::MdxError;
21use crate::models::{
22 ComponentDefinition, OutputFormat, RenderSettings, RenderedMdx, TsxTransformConfig,
23};
24use crate::renderer::JsRenderer;
25use crate::transform::{transform_tsx_to_js_for_output, transform_tsx_to_js_with_config};
26use gray_matter::{engine::YAML, Matter};
27use markdown::{to_html_with_options, CompileOptions, Constructs, Options, ParseOptions};
28use serde_json::json;
29use std::collections::HashMap;
30
31struct RenderContext<'a> {
32 renderer: &'a JsRenderer,
33 components: Option<&'a HashMap<String, ComponentDefinition>>,
34 props_json: &'a str,
35 settings: &'a RenderSettings,
36}
37
38fn markdown_options() -> Options {
39 Options {
40 parse: ParseOptions {
41 constructs: Constructs {
42 html_flow: true, html_text: true, ..Constructs::default()
45 },
46 ..ParseOptions::default()
47 },
48 compile: CompileOptions {
49 allow_dangerous_html: true, ..CompileOptions::default()
51 },
52 }
53}
54
55fn unwrap_fragment(html: &str) -> String {
69 let trimmed = html.trim();
70
71 let fragment_start_patterns = ["<Fragment", "<fragment"];
73 let mut fragment_start: Option<usize> = None;
74
75 for pattern in &fragment_start_patterns {
76 if let Some(pos) = trimmed.find(pattern) {
77 fragment_start = Some(pos);
78 break;
79 }
80 }
81
82 if let Some(start) = fragment_start {
83 if let Some(tag_end) = trimmed[start..].find('>') {
85 let tag_end = start + tag_end + 1;
86 let content_start = tag_end;
87
88 let remaining = &trimmed[content_start..];
90 let fragment_end_patterns = ["</Fragment>", "</fragment>"];
91 let mut fragment_end: Option<usize> = None;
92
93 for pattern in &fragment_end_patterns {
94 if let Some(pos) = remaining.rfind(pattern) {
95 fragment_end = Some(pos);
96 break;
97 }
98 }
99
100 if let Some(end_pos) = fragment_end {
101 return remaining[..end_pos].trim().to_string();
103 }
104 }
105 }
106
107 html.to_string()
109}
110
111fn render_markdown(content: &str) -> Result<String, MdxError> {
112 let options = markdown_options();
113 to_html_with_options(content, &options).map_err(|e| MdxError::MarkdownRender(e.to_string()))
114}
115
116fn log_render_error(e: &anyhow::Error, js_output: &str, context: &str) {
119 eprintln!("{context} render error details: {:#}", e);
120 eprintln!("JavaScript output: {js_output}");
121}
122
123pub fn mdx_to_html_with_frontmatter(
134 mdx_content: &str,
135 renderer: &JsRenderer,
136 components: Option<&HashMap<String, ComponentDefinition>>,
137 settings: &RenderSettings,
138) -> Result<RenderedMdx, MdxError> {
139 let matter = Matter::<YAML>::new();
141 let parsed = matter
142 .parse::<serde_json::Value>(mdx_content)
143 .map_err(|e| MdxError::FrontmatterParse(e.to_string()))?;
144
145 let frontmatter = parsed
146 .data
147 .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::with_capacity(0)));
148
149 let html_output = render_markdown(&parsed.content)?;
151
152 let props_json = serde_json::to_string(&frontmatter)
154 .map_err(|e| MdxError::FrontmatterParse(format!("Failed to serialize frontmatter: {e}")))?;
155
156 let context = RenderContext {
157 renderer,
158 components,
159 props_json: &props_json,
160 settings,
161 };
162
163 let output = render_with_engine_pipeline(&context, &html_output)?;
164
165 Ok(RenderedMdx {
166 metadata: frontmatter,
167 output: Some(output),
168 })
169}
170
171pub fn create_error_response(error: &anyhow::Error) -> RenderedMdx {
183 let error_chain = format!("{:#}", error);
185 let error_message = error.to_string();
186
187 eprintln!("MDX rendering error: {error_chain}");
189
190 let error_html = format!("<p>Error rendering MDX: {error_message}</p>");
191 RenderedMdx {
192 metadata: json!({
193 "error": error_message,
194 "error_chain": error_chain
195 }),
196 output: Some(error_html),
197 }
198}
199
200fn render_with_engine_pipeline(
201 context: &RenderContext<'_>,
202 html_output: &str,
203) -> Result<String, MdxError> {
204 let mut transform_config = TsxTransformConfig::for_engine(false);
206
207 if matches!(context.settings.output, OutputFormat::Schema) {
209 if let Some(components) = context.components {
210 let component_names: std::collections::HashSet<String> = components
211 .iter()
212 .map(|(key, comp_def)| {
213 comp_def
214 .name
215 .as_ref()
216 .cloned()
217 .unwrap_or_else(|| key.clone())
218 })
219 .collect();
220 if !component_names.is_empty() {
221 transform_config.component_names = Some(component_names);
222 }
223 }
224 }
225
226 let javascript_output = transform_tsx_to_js_with_config(html_output, transform_config)
227 .map_err(|e| {
228 MdxError::TsxTransform(format!("Failed to transform TSX to JavaScript: {e}"))
229 })?;
230
231 let template_output = render_template(context, &javascript_output)?;
233
234 match context.settings.output {
235 OutputFormat::Html => {
236 Ok(unwrap_fragment(&template_output))
238 }
239 OutputFormat::Javascript => {
240 transform_tsx_to_js_for_output(&template_output, context.settings.minify).map_err(|e| {
241 MdxError::TsxTransform(format!("Failed to transform template to JavaScript: {e}"))
242 })
243 }
244 OutputFormat::Schema => {
245 render_template_to_schema(context, &javascript_output)
247 }
248 }
249}
250
251fn render_template(
252 context: &RenderContext<'_>,
253 javascript_output: &str,
254) -> Result<String, MdxError> {
255 context
256 .renderer
257 .render_transformed_component(
258 javascript_output,
259 Some(context.props_json),
260 context.components,
261 )
262 .map_err(|e| {
263 log_render_error(&e, javascript_output, "Component");
264 MdxError::TsxTransform(format!("Failed to render component template: {:#}", e))
265 })
266}
267
268fn render_template_to_schema(
269 context: &RenderContext<'_>,
270 javascript_output: &str,
271) -> Result<String, MdxError> {
272 context
273 .renderer
274 .render_transformed_component_to_schema(
275 javascript_output,
276 Some(context.props_json),
277 context.components,
278 )
279 .map_err(|e| {
280 log_render_error(&e, javascript_output, "Schema");
281 MdxError::TsxTransform(format!("Failed to render component to schema: {:#}", e))
282 })
283}