1use std::collections::{BTreeMap, HashMap};
87use std::error::Error as StdError;
88
89use minijinja::{Environment, Error as MjError};
90
91use crate::parse::BODY_FIELD;
92use crate::value::QuillValue;
93
94#[derive(thiserror::Error, Debug)]
96pub enum TemplateError {
97 #[error("{0}")]
99 RenderError(#[from] minijinja::Error),
100 #[error("{0}")]
102 InvalidTemplate(String, #[source] Box<dyn StdError + Send + Sync>),
103 #[error("{0}")]
105 FilterError(String),
106}
107
108pub mod filter_api {
110 pub use minijinja::value::{Kwargs, Value};
111 pub use minijinja::{Error, ErrorKind, State};
112
113 pub trait DynFilter: Send + Sync + 'static {}
115 impl<T> DynFilter for T where T: Send + Sync + 'static {}
116}
117
118type FilterFn = fn(
120 &filter_api::State,
121 filter_api::Value,
122 filter_api::Kwargs,
123) -> Result<filter_api::Value, MjError>;
124
125pub trait GlueEngine {
127 fn register_filter(&mut self, name: &str, func: FilterFn);
129
130 fn compose(&mut self, context: HashMap<String, QuillValue>) -> Result<String, TemplateError>;
132}
133
134pub struct TemplateGlue {
136 template: String,
137 filters: HashMap<String, FilterFn>,
138}
139
140pub struct AutoGlue {
142 filters: HashMap<String, FilterFn>,
143}
144
145pub enum Glue {
147 Template(TemplateGlue),
149 Auto(AutoGlue),
151}
152
153impl TemplateGlue {
154 pub fn new(template: String) -> Self {
156 Self {
157 template,
158 filters: HashMap::new(),
159 }
160 }
161}
162
163impl GlueEngine for TemplateGlue {
164 fn register_filter(&mut self, name: &str, func: FilterFn) {
166 self.filters.insert(name.to_string(), func);
167 }
168
169 fn compose(&mut self, context: HashMap<String, QuillValue>) -> Result<String, TemplateError> {
171 let metadata_fields = separate_metadata_fields(&context);
173
174 let mut minijinja_context = convert_quillvalue_to_minijinja(context)?;
176 let metadata_minijinja = convert_quillvalue_to_minijinja(metadata_fields)?;
177
178 let metadata_btree: BTreeMap<String, minijinja::value::Value> =
181 metadata_minijinja.into_iter().collect();
182 minijinja_context.insert(
183 "__metadata__".to_string(),
184 minijinja::value::Value::from_object(metadata_btree),
185 );
186
187 let mut env = Environment::new();
189
190 for (name, filter_fn) in &self.filters {
192 let filter_fn = *filter_fn; env.add_filter(name, filter_fn);
194 }
195
196 env.add_template("main", &self.template).map_err(|e| {
197 TemplateError::InvalidTemplate("Failed to add template".to_string(), Box::new(e))
198 })?;
199
200 let tmpl = env.get_template("main").map_err(|e| {
202 TemplateError::InvalidTemplate("Failed to get template".to_string(), Box::new(e))
203 })?;
204
205 let result = tmpl.render(&minijinja_context)?;
206
207 if result.len() > crate::error::MAX_TEMPLATE_OUTPUT {
209 return Err(TemplateError::FilterError(format!(
210 "Template output too large: {} bytes (max: {} bytes)",
211 result.len(),
212 crate::error::MAX_TEMPLATE_OUTPUT
213 )));
214 }
215
216 Ok(result)
217 }
218}
219
220impl AutoGlue {
221 pub fn new() -> Self {
223 Self {
224 filters: HashMap::new(),
225 }
226 }
227}
228
229impl GlueEngine for AutoGlue {
230 fn register_filter(&mut self, name: &str, func: FilterFn) {
232 self.filters.insert(name.to_string(), func);
235 }
236
237 fn compose(&mut self, context: HashMap<String, QuillValue>) -> Result<String, TemplateError> {
239 let mut json_map = serde_json::Map::new();
241 let mut metadata_json = serde_json::Map::new();
242
243 for (key, value) in &context {
244 let json_value = value.as_json().clone();
245 json_map.insert(key.clone(), json_value.clone());
246
247 if key.as_str() != BODY_FIELD {
249 metadata_json.insert(key.clone(), json_value);
250 }
251 }
252
253 json_map.insert(
255 "__metadata__".to_string(),
256 serde_json::Value::Object(metadata_json),
257 );
258
259 let json_value = serde_json::Value::Object(json_map);
260 let result = serde_json::to_string_pretty(&json_value).map_err(|e| {
261 TemplateError::FilterError(format!("Failed to serialize to JSON: {}", e))
262 })?;
263
264 if result.len() > crate::error::MAX_TEMPLATE_OUTPUT {
266 return Err(TemplateError::FilterError(format!(
267 "JSON output too large: {} bytes (max: {} bytes)",
268 result.len(),
269 crate::error::MAX_TEMPLATE_OUTPUT
270 )));
271 }
272
273 Ok(result)
274 }
275}
276
277impl Glue {
278 pub fn new(template: String) -> Self {
280 Glue::Template(TemplateGlue::new(template))
281 }
282
283 pub fn new_auto() -> Self {
285 Glue::Auto(AutoGlue::new())
286 }
287
288 pub fn register_filter(&mut self, name: &str, func: FilterFn) {
290 match self {
291 Glue::Template(engine) => engine.register_filter(name, func),
292 Glue::Auto(engine) => engine.register_filter(name, func),
293 }
294 }
295
296 pub fn compose(
298 &mut self,
299 context: HashMap<String, QuillValue>,
300 ) -> Result<String, TemplateError> {
301 match self {
302 Glue::Template(engine) => engine.compose(context),
303 Glue::Auto(engine) => engine.compose(context),
304 }
305 }
306}
307
308fn separate_metadata_fields(context: &HashMap<String, QuillValue>) -> HashMap<String, QuillValue> {
310 context
311 .iter()
312 .filter(|(key, _)| key.as_str() != BODY_FIELD)
313 .map(|(k, v)| (k.clone(), v.clone()))
314 .collect()
315}
316
317fn convert_quillvalue_to_minijinja(
319 fields: HashMap<String, QuillValue>,
320) -> Result<HashMap<String, minijinja::value::Value>, TemplateError> {
321 let mut result = HashMap::new();
322
323 for (key, value) in fields {
324 let minijinja_value = value.to_minijinja().map_err(|e| {
325 TemplateError::FilterError(format!("Failed to convert QuillValue to MiniJinja: {}", e))
326 })?;
327 result.insert(key, minijinja_value);
328 }
329
330 Ok(result)
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use std::collections::HashMap;
337
338 #[test]
339 fn test_glue_creation() {
340 let _glue = Glue::new("Hello {{ name }}".to_string());
341 assert!(true);
342 }
343
344 #[test]
345 fn test_compose_simple_template() {
346 let mut glue = Glue::new("Hello {{ name }}! Body: {{ body }}".to_string());
347 let mut context = HashMap::new();
348 context.insert(
349 "name".to_string(),
350 QuillValue::from_json(serde_json::Value::String("World".to_string())),
351 );
352 context.insert(
353 "body".to_string(),
354 QuillValue::from_json(serde_json::Value::String("Hello content".to_string())),
355 );
356
357 let result = glue.compose(context).unwrap();
358 assert!(result.contains("Hello World!"));
359 assert!(result.contains("Body: Hello content"));
360 }
361
362 #[test]
363 fn test_field_with_dash() {
364 let mut glue = Glue::new("Field: {{ letterhead_title }}".to_string());
365 let mut context = HashMap::new();
366 context.insert(
367 "letterhead_title".to_string(),
368 QuillValue::from_json(serde_json::Value::String("TEST VALUE".to_string())),
369 );
370 context.insert(
371 "body".to_string(),
372 QuillValue::from_json(serde_json::Value::String("body".to_string())),
373 );
374
375 let result = glue.compose(context).unwrap();
376 assert!(result.contains("TEST VALUE"));
377 }
378
379 #[test]
380 fn test_compose_with_dash_in_template() {
381 let mut glue = Glue::new("Field: {{ letterhead_title }}".to_string());
383 let mut context = HashMap::new();
384 context.insert(
385 "letterhead_title".to_string(),
386 QuillValue::from_json(serde_json::Value::String("DASHED".to_string())),
387 );
388 context.insert(
389 "body".to_string(),
390 QuillValue::from_json(serde_json::Value::String("body".to_string())),
391 );
392
393 let result = glue.compose(context).unwrap();
394 assert!(result.contains("DASHED"));
395 }
396
397 #[test]
398 fn test_template_output_size_limit() {
399 let template = "{{ content }}".to_string();
403 let mut glue = Glue::new(template);
404
405 let mut context = HashMap::new();
406 context.insert(
410 "content".to_string(),
411 QuillValue::from_json(serde_json::Value::String("test".to_string())),
412 );
413
414 let result = glue.compose(context);
415 assert!(result.is_ok());
417 }
418
419 #[test]
420 fn test_auto_glue_basic() {
421 let mut glue = Glue::new_auto();
422 let mut context = HashMap::new();
423 context.insert(
424 "name".to_string(),
425 QuillValue::from_json(serde_json::Value::String("World".to_string())),
426 );
427 context.insert(
428 "body".to_string(),
429 QuillValue::from_json(serde_json::Value::String("Hello content".to_string())),
430 );
431
432 let result = glue.compose(context).unwrap();
433
434 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
436 assert_eq!(json["name"], "World");
437 assert_eq!(json["body"], "Hello content");
438 }
439
440 #[test]
441 fn test_auto_glue_with_nested_data() {
442 let mut glue = Glue::new_auto();
443 let mut context = HashMap::new();
444
445 let nested_obj = serde_json::json!({
447 "first": "John",
448 "last": "Doe"
449 });
450 context.insert("author".to_string(), QuillValue::from_json(nested_obj));
451
452 let tags = serde_json::json!(["tag1", "tag2", "tag3"]);
454 context.insert("tags".to_string(), QuillValue::from_json(tags));
455
456 let result = glue.compose(context).unwrap();
457
458 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
460 assert_eq!(json["author"]["first"], "John");
461 assert_eq!(json["author"]["last"], "Doe");
462 assert_eq!(json["tags"][0], "tag1");
463 assert_eq!(json["tags"].as_array().unwrap().len(), 3);
464 }
465
466 #[test]
467 fn test_auto_glue_filter_registration() {
468 let mut glue = Glue::new_auto();
470
471 fn dummy_filter(
472 _state: &filter_api::State,
473 value: filter_api::Value,
474 _kwargs: filter_api::Kwargs,
475 ) -> Result<filter_api::Value, MjError> {
476 Ok(value)
477 }
478
479 glue.register_filter("dummy", dummy_filter);
481
482 let mut context = HashMap::new();
483 context.insert(
484 "test".to_string(),
485 QuillValue::from_json(serde_json::Value::String("value".to_string())),
486 );
487
488 let result = glue.compose(context).unwrap();
489 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
490 assert_eq!(json["test"], "value");
491 }
492
493 #[test]
494 fn test_metadata_field_excludes_body() {
495 let template = "{% for key in __metadata__ %}{{ key }},{% endfor %}";
496 let mut glue = Glue::new(template.to_string());
497
498 let mut context = HashMap::new();
499 context.insert(
500 "title".to_string(),
501 QuillValue::from_json(serde_json::json!("Test")),
502 );
503 context.insert(
504 "author".to_string(),
505 QuillValue::from_json(serde_json::json!("John")),
506 );
507 context.insert(
508 "body".to_string(),
509 QuillValue::from_json(serde_json::json!("Body content")),
510 );
511
512 let result = glue.compose(context).unwrap();
513
514 assert!(result.contains("title"));
516 assert!(result.contains("author"));
517 assert!(!result.contains("body"));
518 }
519
520 #[test]
521 fn test_metadata_field_includes_frontmatter() {
522 let template = r#"
523{%- for key in __metadata__ -%}
524{{ key }}
525{% endfor -%}
526"#;
527 let mut glue = Glue::new(template.to_string());
528
529 let mut context = HashMap::new();
530 context.insert(
531 "title".to_string(),
532 QuillValue::from_json(serde_json::json!("Test Document")),
533 );
534 context.insert(
535 "author".to_string(),
536 QuillValue::from_json(serde_json::json!("Jane Doe")),
537 );
538 context.insert(
539 "date".to_string(),
540 QuillValue::from_json(serde_json::json!("2024-01-01")),
541 );
542 context.insert(
543 "body".to_string(),
544 QuillValue::from_json(serde_json::json!("Document body")),
545 );
546
547 let result = glue.compose(context).unwrap();
548
549 assert!(result.contains("title"));
551 assert!(result.contains("author"));
552 assert!(result.contains("date"));
553 assert!(!result.contains("body"));
555 }
556
557 #[test]
558 fn test_metadata_field_empty_when_only_body() {
559 let template = "Metadata count: {{ __metadata__ | length }}";
560 let mut glue = Glue::new(template.to_string());
561
562 let mut context = HashMap::new();
563 context.insert(
564 "body".to_string(),
565 QuillValue::from_json(serde_json::json!("Only body content")),
566 );
567
568 let result = glue.compose(context).unwrap();
569
570 assert!(result.contains("Metadata count: 0"));
572 }
573
574 #[test]
575 fn test_backward_compatibility_top_level_access() {
576 let template = "Title: {{ title }}, Author: {{ author }}, Body: {{ body }}";
577 let mut glue = Glue::new(template.to_string());
578
579 let mut context = HashMap::new();
580 context.insert(
581 "title".to_string(),
582 QuillValue::from_json(serde_json::json!("My Title")),
583 );
584 context.insert(
585 "author".to_string(),
586 QuillValue::from_json(serde_json::json!("Author Name")),
587 );
588 context.insert(
589 "body".to_string(),
590 QuillValue::from_json(serde_json::json!("Body text")),
591 );
592
593 let result = glue.compose(context).unwrap();
594
595 assert!(result.contains("Title: My Title"));
597 assert!(result.contains("Author: Author Name"));
598 assert!(result.contains("Body: Body text"));
599 }
600
601 #[test]
602 fn test_metadata_iteration_in_template() {
603 let template = r#"
604{%- set metadata_count = __metadata__ | length -%}
605Metadata fields: {{ metadata_count }}
606{%- for key in __metadata__ %}
607- {{ key }}: {{ __metadata__[key] }}
608{%- endfor %}
609Body present: {{ body | length > 0 }}
610"#;
611 let mut glue = Glue::new(template.to_string());
612
613 let mut context = HashMap::new();
614 context.insert(
615 "title".to_string(),
616 QuillValue::from_json(serde_json::json!("Test")),
617 );
618 context.insert(
619 "version".to_string(),
620 QuillValue::from_json(serde_json::json!("1.0")),
621 );
622 context.insert(
623 "body".to_string(),
624 QuillValue::from_json(serde_json::json!("Content")),
625 );
626
627 let result = glue.compose(context).unwrap();
628
629 assert!(result.contains("Metadata fields: 2"));
631 assert!(result.contains("Body present: true"));
633 }
634
635 #[test]
636 fn test_auto_glue_metadata_field() {
637 let mut glue = Glue::new_auto();
638
639 let mut context = HashMap::new();
640 context.insert(
641 "title".to_string(),
642 QuillValue::from_json(serde_json::json!("Document")),
643 );
644 context.insert(
645 "author".to_string(),
646 QuillValue::from_json(serde_json::json!("Writer")),
647 );
648 context.insert(
649 "body".to_string(),
650 QuillValue::from_json(serde_json::json!("Content here")),
651 );
652
653 let result = glue.compose(context).unwrap();
654
655 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
657
658 assert!(json["__metadata__"].is_object());
660 assert_eq!(json["__metadata__"]["title"], "Document");
661 assert_eq!(json["__metadata__"]["author"], "Writer");
662
663 assert!(json["__metadata__"]["body"].is_null());
665
666 assert_eq!(json["body"], "Content here");
668 }
669
670 #[test]
671 fn test_metadata_with_nested_objects() {
672 let template = "{{ __metadata__.author.name }}";
673 let mut glue = Glue::new(template.to_string());
674
675 let mut context = HashMap::new();
676 context.insert(
677 "author".to_string(),
678 QuillValue::from_json(serde_json::json!({
679 "name": "John Doe",
680 "email": "john@example.com"
681 })),
682 );
683 context.insert(
684 "body".to_string(),
685 QuillValue::from_json(serde_json::json!("Text")),
686 );
687
688 let result = glue.compose(context).unwrap();
689
690 assert!(result.contains("John Doe"));
692 }
693
694 #[test]
695 fn test_metadata_with_arrays() {
696 let template = "Tags: {{ __metadata__.tags | length }}";
697 let mut glue = Glue::new(template.to_string());
698
699 let mut context = HashMap::new();
700 context.insert(
701 "tags".to_string(),
702 QuillValue::from_json(serde_json::json!(["rust", "markdown", "template"])),
703 );
704 context.insert(
705 "body".to_string(),
706 QuillValue::from_json(serde_json::json!("Content")),
707 );
708
709 let result = glue.compose(context).unwrap();
710
711 assert!(result.contains("Tags: 3"));
713 }
714}