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 PlateEngine {
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 TemplatePlate {
136 template: String,
137 filters: HashMap<String, FilterFn>,
138}
139
140pub struct AutoPlate {
142 filters: HashMap<String, FilterFn>,
143}
144
145pub enum Plate {
147 Template(TemplatePlate),
149 Auto(AutoPlate),
151}
152
153impl TemplatePlate {
154 pub fn new(template: String) -> Self {
156 Self {
157 template,
158 filters: HashMap::new(),
159 }
160 }
161}
162
163impl PlateEngine for TemplatePlate {
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 AutoPlate {
221 pub fn new() -> Self {
223 Self {
224 filters: HashMap::new(),
225 }
226 }
227}
228
229impl Default for AutoPlate {
230 fn default() -> Self {
231 Self::new()
232 }
233}
234
235impl PlateEngine for AutoPlate {
236 fn register_filter(&mut self, name: &str, func: FilterFn) {
238 self.filters.insert(name.to_string(), func);
241 }
242
243 fn compose(&mut self, context: HashMap<String, QuillValue>) -> Result<String, TemplateError> {
245 let mut json_map = serde_json::Map::new();
247 let mut metadata_json = serde_json::Map::new();
248
249 for (key, value) in &context {
250 let json_value = value.as_json().clone();
251 json_map.insert(key.clone(), json_value.clone());
252
253 if key.as_str() != BODY_FIELD {
255 metadata_json.insert(key.clone(), json_value);
256 }
257 }
258
259 json_map.insert(
261 "__metadata__".to_string(),
262 serde_json::Value::Object(metadata_json),
263 );
264
265 let json_value = serde_json::Value::Object(json_map);
266 let result = serde_json::to_string_pretty(&json_value).map_err(|e| {
267 TemplateError::FilterError(format!("Failed to serialize to JSON: {}", e))
268 })?;
269
270 if result.len() > crate::error::MAX_TEMPLATE_OUTPUT {
272 return Err(TemplateError::FilterError(format!(
273 "JSON output too large: {} bytes (max: {} bytes)",
274 result.len(),
275 crate::error::MAX_TEMPLATE_OUTPUT
276 )));
277 }
278
279 Ok(result)
280 }
281}
282
283impl Plate {
284 pub fn new(template: String) -> Self {
286 Plate::Template(TemplatePlate::new(template))
287 }
288
289 pub fn new_auto() -> Self {
291 Plate::Auto(AutoPlate::new())
292 }
293
294 pub fn register_filter(&mut self, name: &str, func: FilterFn) {
296 match self {
297 Plate::Template(engine) => engine.register_filter(name, func),
298 Plate::Auto(engine) => engine.register_filter(name, func),
299 }
300 }
301
302 pub fn compose(
304 &mut self,
305 context: HashMap<String, QuillValue>,
306 ) -> Result<String, TemplateError> {
307 match self {
308 Plate::Template(engine) => engine.compose(context),
309 Plate::Auto(engine) => engine.compose(context),
310 }
311 }
312}
313
314fn separate_metadata_fields(context: &HashMap<String, QuillValue>) -> HashMap<String, QuillValue> {
316 context
317 .iter()
318 .filter(|(key, _)| key.as_str() != BODY_FIELD)
319 .map(|(k, v)| (k.clone(), v.clone()))
320 .collect()
321}
322
323fn convert_quillvalue_to_minijinja(
325 fields: HashMap<String, QuillValue>,
326) -> Result<HashMap<String, minijinja::value::Value>, TemplateError> {
327 let mut result = HashMap::new();
328
329 for (key, value) in fields {
330 let minijinja_value = value.to_minijinja().map_err(|e| {
331 TemplateError::FilterError(format!("Failed to convert QuillValue to MiniJinja: {}", e))
332 })?;
333 result.insert(key, minijinja_value);
334 }
335
336 Ok(result)
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use std::collections::HashMap;
343
344 #[test]
345 fn test_plate_creation() {
346 let _plate = Plate::new("Hello {{ name }}".to_string());
347 }
348
349 #[test]
350 fn test_compose_simple_template() {
351 let mut plate = Plate::new("Hello {{ name }}! Body: {{ body }}".to_string());
352 let mut context = HashMap::new();
353 context.insert(
354 "name".to_string(),
355 QuillValue::from_json(serde_json::Value::String("World".to_string())),
356 );
357 context.insert(
358 "body".to_string(),
359 QuillValue::from_json(serde_json::Value::String("Hello content".to_string())),
360 );
361
362 let result = plate.compose(context).unwrap();
363 assert!(result.contains("Hello World!"));
364 assert!(result.contains("Body: Hello content"));
365 }
366
367 #[test]
368 fn test_field_with_dash() {
369 let mut plate = Plate::new("Field: {{ letterhead_title }}".to_string());
370 let mut context = HashMap::new();
371 context.insert(
372 "letterhead_title".to_string(),
373 QuillValue::from_json(serde_json::Value::String("TEST VALUE".to_string())),
374 );
375 context.insert(
376 "body".to_string(),
377 QuillValue::from_json(serde_json::Value::String("body".to_string())),
378 );
379
380 let result = plate.compose(context).unwrap();
381 assert!(result.contains("TEST VALUE"));
382 }
383
384 #[test]
385 fn test_compose_with_dash_in_template() {
386 let mut plate = Plate::new("Field: {{ letterhead_title }}".to_string());
388 let mut context = HashMap::new();
389 context.insert(
390 "letterhead_title".to_string(),
391 QuillValue::from_json(serde_json::Value::String("DASHED".to_string())),
392 );
393 context.insert(
394 "body".to_string(),
395 QuillValue::from_json(serde_json::Value::String("body".to_string())),
396 );
397
398 let result = plate.compose(context).unwrap();
399 assert!(result.contains("DASHED"));
400 }
401
402 #[test]
403 fn test_template_output_size_limit() {
404 let template = "{{ content }}".to_string();
408 let mut plate = Plate::new(template);
409
410 let mut context = HashMap::new();
411 context.insert(
415 "content".to_string(),
416 QuillValue::from_json(serde_json::Value::String("test".to_string())),
417 );
418
419 let result = plate.compose(context);
420 assert!(result.is_ok());
422 }
423
424 #[test]
425 fn test_auto_plate_basic() {
426 let mut plate = Plate::new_auto();
427 let mut context = HashMap::new();
428 context.insert(
429 "name".to_string(),
430 QuillValue::from_json(serde_json::Value::String("World".to_string())),
431 );
432 context.insert(
433 "body".to_string(),
434 QuillValue::from_json(serde_json::Value::String("Hello content".to_string())),
435 );
436
437 let result = plate.compose(context).unwrap();
438
439 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
441 assert_eq!(json["name"], "World");
442 assert_eq!(json["body"], "Hello content");
443 }
444
445 #[test]
446 fn test_auto_plate_with_nested_data() {
447 let mut plate = Plate::new_auto();
448 let mut context = HashMap::new();
449
450 let nested_obj = serde_json::json!({
452 "first": "John",
453 "last": "Doe"
454 });
455 context.insert("author".to_string(), QuillValue::from_json(nested_obj));
456
457 let tags = serde_json::json!(["tag1", "tag2", "tag3"]);
459 context.insert("tags".to_string(), QuillValue::from_json(tags));
460
461 let result = plate.compose(context).unwrap();
462
463 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
465 assert_eq!(json["author"]["first"], "John");
466 assert_eq!(json["author"]["last"], "Doe");
467 assert_eq!(json["tags"][0], "tag1");
468 assert_eq!(json["tags"].as_array().unwrap().len(), 3);
469 }
470
471 #[test]
472 fn test_auto_plate_filter_registration() {
473 let mut plate = Plate::new_auto();
475
476 fn dummy_filter(
477 _state: &filter_api::State,
478 value: filter_api::Value,
479 _kwargs: filter_api::Kwargs,
480 ) -> Result<filter_api::Value, MjError> {
481 Ok(value)
482 }
483
484 plate.register_filter("dummy", dummy_filter);
486
487 let mut context = HashMap::new();
488 context.insert(
489 "test".to_string(),
490 QuillValue::from_json(serde_json::Value::String("value".to_string())),
491 );
492
493 let result = plate.compose(context).unwrap();
494 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
495 assert_eq!(json["test"], "value");
496 }
497
498 #[test]
499 fn test_metadata_field_excludes_body() {
500 let template = "{% for key in __metadata__ %}{{ key }},{% endfor %}";
501 let mut plate = Plate::new(template.to_string());
502
503 let mut context = HashMap::new();
504 context.insert(
505 "title".to_string(),
506 QuillValue::from_json(serde_json::json!("Test")),
507 );
508 context.insert(
509 "author".to_string(),
510 QuillValue::from_json(serde_json::json!("John")),
511 );
512 context.insert(
513 "body".to_string(),
514 QuillValue::from_json(serde_json::json!("Body content")),
515 );
516
517 let result = plate.compose(context).unwrap();
518
519 assert!(result.contains("title"));
521 assert!(result.contains("author"));
522 assert!(!result.contains("body"));
523 }
524
525 #[test]
526 fn test_metadata_field_includes_frontmatter() {
527 let template = r#"
528{%- for key in __metadata__ -%}
529{{ key }}
530{% endfor -%}
531"#;
532 let mut plate = Plate::new(template.to_string());
533
534 let mut context = HashMap::new();
535 context.insert(
536 "title".to_string(),
537 QuillValue::from_json(serde_json::json!("Test Document")),
538 );
539 context.insert(
540 "author".to_string(),
541 QuillValue::from_json(serde_json::json!("Jane Doe")),
542 );
543 context.insert(
544 "date".to_string(),
545 QuillValue::from_json(serde_json::json!("2024-01-01")),
546 );
547 context.insert(
548 "body".to_string(),
549 QuillValue::from_json(serde_json::json!("Document body")),
550 );
551
552 let result = plate.compose(context).unwrap();
553
554 assert!(result.contains("title"));
556 assert!(result.contains("author"));
557 assert!(result.contains("date"));
558 assert!(!result.contains("body"));
560 }
561
562 #[test]
563 fn test_metadata_field_empty_when_only_body() {
564 let template = "Metadata count: {{ __metadata__ | length }}";
565 let mut plate = Plate::new(template.to_string());
566
567 let mut context = HashMap::new();
568 context.insert(
569 "body".to_string(),
570 QuillValue::from_json(serde_json::json!("Only body content")),
571 );
572
573 let result = plate.compose(context).unwrap();
574
575 assert!(result.contains("Metadata count: 0"));
577 }
578
579 #[test]
580 fn test_backward_compatibility_top_level_access() {
581 let template = "Title: {{ title }}, Author: {{ author }}, Body: {{ body }}";
582 let mut plate = Plate::new(template.to_string());
583
584 let mut context = HashMap::new();
585 context.insert(
586 "title".to_string(),
587 QuillValue::from_json(serde_json::json!("My Title")),
588 );
589 context.insert(
590 "author".to_string(),
591 QuillValue::from_json(serde_json::json!("Author Name")),
592 );
593 context.insert(
594 "body".to_string(),
595 QuillValue::from_json(serde_json::json!("Body text")),
596 );
597
598 let result = plate.compose(context).unwrap();
599
600 assert!(result.contains("Title: My Title"));
602 assert!(result.contains("Author: Author Name"));
603 assert!(result.contains("Body: Body text"));
604 }
605
606 #[test]
607 fn test_metadata_iteration_in_template() {
608 let template = r#"
609{%- set metadata_count = __metadata__ | length -%}
610Metadata fields: {{ metadata_count }}
611{%- for key in __metadata__ %}
612- {{ key }}: {{ __metadata__[key] }}
613{%- endfor %}
614Body present: {{ body | length > 0 }}
615"#;
616 let mut plate = Plate::new(template.to_string());
617
618 let mut context = HashMap::new();
619 context.insert(
620 "title".to_string(),
621 QuillValue::from_json(serde_json::json!("Test")),
622 );
623 context.insert(
624 "version".to_string(),
625 QuillValue::from_json(serde_json::json!("1.0")),
626 );
627 context.insert(
628 "body".to_string(),
629 QuillValue::from_json(serde_json::json!("Content")),
630 );
631
632 let result = plate.compose(context).unwrap();
633
634 assert!(result.contains("Metadata fields: 2"));
636 assert!(result.contains("Body present: true"));
638 }
639
640 #[test]
641 fn test_auto_plate_metadata_field() {
642 let mut plate = Plate::new_auto();
643
644 let mut context = HashMap::new();
645 context.insert(
646 "title".to_string(),
647 QuillValue::from_json(serde_json::json!("Document")),
648 );
649 context.insert(
650 "author".to_string(),
651 QuillValue::from_json(serde_json::json!("Writer")),
652 );
653 context.insert(
654 "body".to_string(),
655 QuillValue::from_json(serde_json::json!("Content here")),
656 );
657
658 let result = plate.compose(context).unwrap();
659
660 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
662
663 assert!(json["__metadata__"].is_object());
665 assert_eq!(json["__metadata__"]["title"], "Document");
666 assert_eq!(json["__metadata__"]["author"], "Writer");
667
668 assert!(json["__metadata__"]["body"].is_null());
670
671 assert_eq!(json["body"], "Content here");
673 }
674
675 #[test]
676 fn test_metadata_with_nested_objects() {
677 let template = "{{ __metadata__.author.name }}";
678 let mut plate = Plate::new(template.to_string());
679
680 let mut context = HashMap::new();
681 context.insert(
682 "author".to_string(),
683 QuillValue::from_json(serde_json::json!({
684 "name": "John Doe",
685 "email": "john@example.com"
686 })),
687 );
688 context.insert(
689 "body".to_string(),
690 QuillValue::from_json(serde_json::json!("Text")),
691 );
692
693 let result = plate.compose(context).unwrap();
694
695 assert!(result.contains("John Doe"));
697 }
698
699 #[test]
700 fn test_metadata_with_arrays() {
701 let template = "Tags: {{ __metadata__.tags | length }}";
702 let mut plate = Plate::new(template.to_string());
703
704 let mut context = HashMap::new();
705 context.insert(
706 "tags".to_string(),
707 QuillValue::from_json(serde_json::json!(["rust", "markdown", "template"])),
708 );
709 context.insert(
710 "body".to_string(),
711 QuillValue::from_json(serde_json::json!("Content")),
712 );
713
714 let result = plate.compose(context).unwrap();
715
716 assert!(result.contains("Tags: 3"));
718 }
719}