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 assert!(true);
348 }
349
350 #[test]
351 fn test_compose_simple_template() {
352 let mut plate = Plate::new("Hello {{ name }}! Body: {{ body }}".to_string());
353 let mut context = HashMap::new();
354 context.insert(
355 "name".to_string(),
356 QuillValue::from_json(serde_json::Value::String("World".to_string())),
357 );
358 context.insert(
359 "body".to_string(),
360 QuillValue::from_json(serde_json::Value::String("Hello content".to_string())),
361 );
362
363 let result = plate.compose(context).unwrap();
364 assert!(result.contains("Hello World!"));
365 assert!(result.contains("Body: Hello content"));
366 }
367
368 #[test]
369 fn test_field_with_dash() {
370 let mut plate = Plate::new("Field: {{ letterhead_title }}".to_string());
371 let mut context = HashMap::new();
372 context.insert(
373 "letterhead_title".to_string(),
374 QuillValue::from_json(serde_json::Value::String("TEST VALUE".to_string())),
375 );
376 context.insert(
377 "body".to_string(),
378 QuillValue::from_json(serde_json::Value::String("body".to_string())),
379 );
380
381 let result = plate.compose(context).unwrap();
382 assert!(result.contains("TEST VALUE"));
383 }
384
385 #[test]
386 fn test_compose_with_dash_in_template() {
387 let mut plate = Plate::new("Field: {{ letterhead_title }}".to_string());
389 let mut context = HashMap::new();
390 context.insert(
391 "letterhead_title".to_string(),
392 QuillValue::from_json(serde_json::Value::String("DASHED".to_string())),
393 );
394 context.insert(
395 "body".to_string(),
396 QuillValue::from_json(serde_json::Value::String("body".to_string())),
397 );
398
399 let result = plate.compose(context).unwrap();
400 assert!(result.contains("DASHED"));
401 }
402
403 #[test]
404 fn test_template_output_size_limit() {
405 let template = "{{ content }}".to_string();
409 let mut plate = Plate::new(template);
410
411 let mut context = HashMap::new();
412 context.insert(
416 "content".to_string(),
417 QuillValue::from_json(serde_json::Value::String("test".to_string())),
418 );
419
420 let result = plate.compose(context);
421 assert!(result.is_ok());
423 }
424
425 #[test]
426 fn test_auto_plate_basic() {
427 let mut plate = Plate::new_auto();
428 let mut context = HashMap::new();
429 context.insert(
430 "name".to_string(),
431 QuillValue::from_json(serde_json::Value::String("World".to_string())),
432 );
433 context.insert(
434 "body".to_string(),
435 QuillValue::from_json(serde_json::Value::String("Hello content".to_string())),
436 );
437
438 let result = plate.compose(context).unwrap();
439
440 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
442 assert_eq!(json["name"], "World");
443 assert_eq!(json["body"], "Hello content");
444 }
445
446 #[test]
447 fn test_auto_plate_with_nested_data() {
448 let mut plate = Plate::new_auto();
449 let mut context = HashMap::new();
450
451 let nested_obj = serde_json::json!({
453 "first": "John",
454 "last": "Doe"
455 });
456 context.insert("author".to_string(), QuillValue::from_json(nested_obj));
457
458 let tags = serde_json::json!(["tag1", "tag2", "tag3"]);
460 context.insert("tags".to_string(), QuillValue::from_json(tags));
461
462 let result = plate.compose(context).unwrap();
463
464 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
466 assert_eq!(json["author"]["first"], "John");
467 assert_eq!(json["author"]["last"], "Doe");
468 assert_eq!(json["tags"][0], "tag1");
469 assert_eq!(json["tags"].as_array().unwrap().len(), 3);
470 }
471
472 #[test]
473 fn test_auto_plate_filter_registration() {
474 let mut plate = Plate::new_auto();
476
477 fn dummy_filter(
478 _state: &filter_api::State,
479 value: filter_api::Value,
480 _kwargs: filter_api::Kwargs,
481 ) -> Result<filter_api::Value, MjError> {
482 Ok(value)
483 }
484
485 plate.register_filter("dummy", dummy_filter);
487
488 let mut context = HashMap::new();
489 context.insert(
490 "test".to_string(),
491 QuillValue::from_json(serde_json::Value::String("value".to_string())),
492 );
493
494 let result = plate.compose(context).unwrap();
495 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
496 assert_eq!(json["test"], "value");
497 }
498
499 #[test]
500 fn test_metadata_field_excludes_body() {
501 let template = "{% for key in __metadata__ %}{{ key }},{% endfor %}";
502 let mut plate = Plate::new(template.to_string());
503
504 let mut context = HashMap::new();
505 context.insert(
506 "title".to_string(),
507 QuillValue::from_json(serde_json::json!("Test")),
508 );
509 context.insert(
510 "author".to_string(),
511 QuillValue::from_json(serde_json::json!("John")),
512 );
513 context.insert(
514 "body".to_string(),
515 QuillValue::from_json(serde_json::json!("Body content")),
516 );
517
518 let result = plate.compose(context).unwrap();
519
520 assert!(result.contains("title"));
522 assert!(result.contains("author"));
523 assert!(!result.contains("body"));
524 }
525
526 #[test]
527 fn test_metadata_field_includes_frontmatter() {
528 let template = r#"
529{%- for key in __metadata__ -%}
530{{ key }}
531{% endfor -%}
532"#;
533 let mut plate = Plate::new(template.to_string());
534
535 let mut context = HashMap::new();
536 context.insert(
537 "title".to_string(),
538 QuillValue::from_json(serde_json::json!("Test Document")),
539 );
540 context.insert(
541 "author".to_string(),
542 QuillValue::from_json(serde_json::json!("Jane Doe")),
543 );
544 context.insert(
545 "date".to_string(),
546 QuillValue::from_json(serde_json::json!("2024-01-01")),
547 );
548 context.insert(
549 "body".to_string(),
550 QuillValue::from_json(serde_json::json!("Document body")),
551 );
552
553 let result = plate.compose(context).unwrap();
554
555 assert!(result.contains("title"));
557 assert!(result.contains("author"));
558 assert!(result.contains("date"));
559 assert!(!result.contains("body"));
561 }
562
563 #[test]
564 fn test_metadata_field_empty_when_only_body() {
565 let template = "Metadata count: {{ __metadata__ | length }}";
566 let mut plate = Plate::new(template.to_string());
567
568 let mut context = HashMap::new();
569 context.insert(
570 "body".to_string(),
571 QuillValue::from_json(serde_json::json!("Only body content")),
572 );
573
574 let result = plate.compose(context).unwrap();
575
576 assert!(result.contains("Metadata count: 0"));
578 }
579
580 #[test]
581 fn test_backward_compatibility_top_level_access() {
582 let template = "Title: {{ title }}, Author: {{ author }}, Body: {{ body }}";
583 let mut plate = Plate::new(template.to_string());
584
585 let mut context = HashMap::new();
586 context.insert(
587 "title".to_string(),
588 QuillValue::from_json(serde_json::json!("My Title")),
589 );
590 context.insert(
591 "author".to_string(),
592 QuillValue::from_json(serde_json::json!("Author Name")),
593 );
594 context.insert(
595 "body".to_string(),
596 QuillValue::from_json(serde_json::json!("Body text")),
597 );
598
599 let result = plate.compose(context).unwrap();
600
601 assert!(result.contains("Title: My Title"));
603 assert!(result.contains("Author: Author Name"));
604 assert!(result.contains("Body: Body text"));
605 }
606
607 #[test]
608 fn test_metadata_iteration_in_template() {
609 let template = r#"
610{%- set metadata_count = __metadata__ | length -%}
611Metadata fields: {{ metadata_count }}
612{%- for key in __metadata__ %}
613- {{ key }}: {{ __metadata__[key] }}
614{%- endfor %}
615Body present: {{ body | length > 0 }}
616"#;
617 let mut plate = Plate::new(template.to_string());
618
619 let mut context = HashMap::new();
620 context.insert(
621 "title".to_string(),
622 QuillValue::from_json(serde_json::json!("Test")),
623 );
624 context.insert(
625 "version".to_string(),
626 QuillValue::from_json(serde_json::json!("1.0")),
627 );
628 context.insert(
629 "body".to_string(),
630 QuillValue::from_json(serde_json::json!("Content")),
631 );
632
633 let result = plate.compose(context).unwrap();
634
635 assert!(result.contains("Metadata fields: 2"));
637 assert!(result.contains("Body present: true"));
639 }
640
641 #[test]
642 fn test_auto_plate_metadata_field() {
643 let mut plate = Plate::new_auto();
644
645 let mut context = HashMap::new();
646 context.insert(
647 "title".to_string(),
648 QuillValue::from_json(serde_json::json!("Document")),
649 );
650 context.insert(
651 "author".to_string(),
652 QuillValue::from_json(serde_json::json!("Writer")),
653 );
654 context.insert(
655 "body".to_string(),
656 QuillValue::from_json(serde_json::json!("Content here")),
657 );
658
659 let result = plate.compose(context).unwrap();
660
661 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
663
664 assert!(json["__metadata__"].is_object());
666 assert_eq!(json["__metadata__"]["title"], "Document");
667 assert_eq!(json["__metadata__"]["author"], "Writer");
668
669 assert!(json["__metadata__"]["body"].is_null());
671
672 assert_eq!(json["body"], "Content here");
674 }
675
676 #[test]
677 fn test_metadata_with_nested_objects() {
678 let template = "{{ __metadata__.author.name }}";
679 let mut plate = Plate::new(template.to_string());
680
681 let mut context = HashMap::new();
682 context.insert(
683 "author".to_string(),
684 QuillValue::from_json(serde_json::json!({
685 "name": "John Doe",
686 "email": "john@example.com"
687 })),
688 );
689 context.insert(
690 "body".to_string(),
691 QuillValue::from_json(serde_json::json!("Text")),
692 );
693
694 let result = plate.compose(context).unwrap();
695
696 assert!(result.contains("John Doe"));
698 }
699
700 #[test]
701 fn test_metadata_with_arrays() {
702 let template = "Tags: {{ __metadata__.tags | length }}";
703 let mut plate = Plate::new(template.to_string());
704
705 let mut context = HashMap::new();
706 context.insert(
707 "tags".to_string(),
708 QuillValue::from_json(serde_json::json!(["rust", "markdown", "template"])),
709 );
710 context.insert(
711 "body".to_string(),
712 QuillValue::from_json(serde_json::json!("Content")),
713 );
714
715 let result = plate.compose(context).unwrap();
716
717 assert!(result.contains("Tags: 3"));
719 }
720}