1use super::super::{
7 GroupRenderParams, Renderer, TemplateComponentTracker, TemplateRenderParams,
8 TemplateRenderRequest, find_grouping_component, get_variable_key, has_contributor_component,
9 leading_group_affix, strip_author_component, strip_leading_group_affixes,
10};
11use super::component_predicates::{is_term_only_component, resolve_type_variant};
12use super::group_citation_items_by_author;
13use crate::error::ProcessorError;
14use crate::reference::Reference;
15use crate::render::{ProcTemplate, ProcTemplateComponent};
16use crate::values::{ComponentValues, ProcHints, RenderContext, RenderOptions};
17use citum_schema::template::TemplateComponent;
18use std::borrow::Cow;
19
20struct GroupRenderState<'a> {
21 first_item: &'a crate::reference::CitationItem,
22 first_ref: &'a Reference,
23 template: Cow<'a, [TemplateComponent]>,
24}
25
26struct ItemRenderState<'a> {
27 item: &'a crate::reference::CitationItem,
28 reference: &'a Reference,
29 template: Cow<'a, [TemplateComponent]>,
30}
31
32struct GroupItemRenderRequest<'a> {
33 item: &'a crate::reference::CitationItem,
34 template: &'a [TemplateComponent],
35 mode: &'a citum_schema::citation::CitationMode,
36 suppress_author: bool,
37 position: Option<&'a citum_schema::citation::Position>,
38 note_start_text_case: Option<citum_schema::NoteStartTextCase>,
39 delimiter: &'a str,
40}
41
42struct TemplateRenderContext<'a> {
48 reference: &'a Reference,
49 ref_type: &'a str,
50 options: &'a RenderOptions<'a>,
51 hint: &'a ProcHints,
52 template_index: usize,
53}
54
55impl Renderer<'_> {
56 fn strip_redundant_leading_group_punctuation<'a>(
57 &self,
58 value: &'a str,
59 delimiter: &str,
60 ) -> &'a str {
61 let Some(delimiter_char) = delimiter.chars().find(|ch| !ch.is_whitespace()) else {
62 return value;
63 };
64
65 let trimmed = value.trim_start();
66 if !trimmed.starts_with(delimiter_char) {
67 return value;
68 }
69
70 #[allow(clippy::string_slice, reason = "delimiter found at start")]
71 trimmed[delimiter_char.len_utf8()..].trim_start()
72 }
73
74 fn join_integral_group_item_parts(&self, item_parts: &[String], delimiter: &str) -> String {
75 let repeated_item_delimiter = if delimiter.trim().is_empty() {
76 ", "
77 } else {
78 delimiter
79 };
80
81 let mut joined = String::new();
82 for (index, part) in item_parts.iter().enumerate() {
83 if index > 0 {
84 joined.push_str(repeated_item_delimiter);
85 }
86
87 let normalized = if index == 0 {
88 part.as_str()
89 } else {
90 self.strip_redundant_leading_group_punctuation(part, repeated_item_delimiter)
91 };
92 joined.push_str(normalized);
93 }
94
95 joined
96 }
97
98 pub fn render_grouped_citation(
104 &self,
105 items: &[crate::reference::CitationItem],
106 spec: &citum_schema::CitationSpec,
107 mode: &citum_schema::citation::CitationMode,
108 intra_delimiter: &str,
109 suppress_author: bool,
110 position: Option<&citum_schema::citation::Position>,
111 ) -> Result<Vec<String>, ProcessorError> {
112 self.render_grouped_citation_with_format::<crate::render::plain::PlainText>(
113 items,
114 &GroupRenderParams {
115 spec,
116 mode,
117 intra_delimiter,
118 suppress_author,
119 position,
120 note_start_text_case: spec.note_start_text_case,
121 },
122 )
123 }
124
125 fn render_special_type_items<F>(
128 &self,
129 group: &[&crate::reference::CitationItem],
130 params: &GroupRenderParams<'_>,
131 ) -> Result<Vec<String>, ProcessorError>
132 where
133 F: crate::render::format::OutputFormat<Output = String>,
134 {
135 let fmt = F::default();
136 let mut rendered_items = Vec::new();
137 for item in group {
138 let state = self.resolve_item_render_state(item, params.spec)?;
139 if let Some(item_str) = self.render_group_item_from_template_with_format::<F>(
140 state.reference,
141 GroupItemRenderRequest {
142 item: state.item,
143 template: &state.template,
144 mode: params.mode,
145 suppress_author: params.suppress_author,
146 position: params.position,
147 note_start_text_case: params.note_start_text_case,
148 delimiter: params.intra_delimiter,
149 },
150 ) && let Some((ids, content)) = self.build_citation_chunk(
151 &fmt,
152 vec![item.id.clone()],
153 item_str,
154 item.prefix.as_deref(),
155 item.suffix.as_deref(),
156 ) {
157 rendered_items.push(fmt.citation(ids, content));
158 }
159 }
160 Ok(rendered_items)
161 }
162
163 fn render_integral_explicit_group<F>(
168 &self,
169 group: &[&crate::reference::CitationItem],
170 spec: &citum_schema::CitationSpec,
171 mode: &citum_schema::citation::CitationMode,
172 suppress_author: bool,
173 position: Option<&citum_schema::citation::Position>,
174 ) -> Result<Option<String>, ProcessorError>
175 where
176 F: crate::render::format::OutputFormat<Output = String>,
177 {
178 let fmt = F::default();
179 let component_delimiter = spec.delimiter.as_deref().unwrap_or(" ");
180 let item_join_delim = spec.multi_cite_delimiter.as_deref().unwrap_or(", ");
181 let mut group_items_str = Vec::new();
182 let mut all_ids = Vec::new();
183
184 for item in group {
185 let state = self.resolve_item_render_state(item, spec)?;
186 if let Some(item_str) = self.render_group_item_from_template_with_format::<F>(
187 state.reference,
188 GroupItemRenderRequest {
189 item: state.item,
190 template: &state.template,
191 mode,
192 suppress_author,
193 position,
194 note_start_text_case: spec.note_start_text_case,
195 delimiter: component_delimiter,
196 },
197 ) && !item_str.is_empty()
198 {
199 group_items_str.push(self.affix_content(
200 &fmt,
201 item_str,
202 item.prefix.as_deref(),
203 item.suffix.as_deref(),
204 ));
205 all_ids.push(item.id.clone());
206 }
207 }
208
209 if group_items_str.is_empty() {
210 return Ok(None);
211 }
212
213 let combined_str = group_items_str.join(item_join_delim);
214 Ok(Some(fmt.citation(all_ids, combined_str)))
215 }
216
217 pub fn render_grouped_citation_with_format<F>(
226 &self,
227 items: &[crate::reference::CitationItem],
228 params: &GroupRenderParams<'_>,
229 ) -> Result<Vec<String>, ProcessorError>
230 where
231 F: crate::render::format::OutputFormat<Output = String>,
232 {
233 let groups = group_citation_items_by_author(self, items);
234 let mut rendered_groups = Vec::new();
235 for (_author_key, group) in groups {
236 rendered_groups
237 .extend(self.render_grouped_citation_group_with_format::<F>(&group, params)?);
238 }
239
240 Ok(rendered_groups)
241 }
242
243 fn render_grouped_citation_group_with_format<F>(
244 &self,
245 group: &[&crate::reference::CitationItem],
246 params: &GroupRenderParams<'_>,
247 ) -> Result<Vec<String>, ProcessorError>
248 where
249 F: crate::render::format::OutputFormat<Output = String>,
250 {
251 let state = self.resolve_group_render_state(group, params.spec)?;
252
253 if let Some(citation) = self.try_render_integral_group_with_format::<F>(
254 group,
255 params.spec,
256 params.mode,
257 params.suppress_author,
258 params.position,
259 )? {
260 return Ok(vec![citation]);
261 }
262
263 if self.requires_full_group_item_rendering(params.mode, state.first_ref) {
264 return self.render_special_type_items::<F>(group, params);
265 }
266
267 Ok(self
268 .render_fallback_grouped_citation_with_format::<F>(
269 group,
270 state.first_ref,
271 state.first_item,
272 &state.template,
273 params,
274 )?
275 .into_iter()
276 .collect())
277 }
278
279 fn render_fallback_grouped_citation_with_format<F>(
280 &self,
281 group: &[&crate::reference::CitationItem],
282 first_ref: &Reference,
283 first_item: &crate::reference::CitationItem,
284 template: &[TemplateComponent],
285 params: &GroupRenderParams<'_>,
286 ) -> Result<Option<String>, ProcessorError>
287 where
288 F: crate::render::format::OutputFormat<Output = String>,
289 {
290 let fmt = F::default();
291 let author_part = self.render_author_for_grouping_with_format::<F>(
292 first_ref,
293 first_item,
294 template,
295 params.mode,
296 params.suppress_author,
297 params.position,
298 );
299 let (item_parts, group_delimiter) =
300 self.render_group_item_parts_with_format::<F>(&fmt, group, params)?;
301 let Some(content) = self.build_grouped_citation_content(
302 &author_part,
303 &item_parts,
304 params,
305 group_delimiter.as_deref(),
306 ) else {
307 return Ok(None);
308 };
309 let group_ids = group.iter().map(|item| item.id.clone()).collect();
310 let prefix = first_item.prefix.as_deref().unwrap_or("");
311 let suffix = if item_parts.is_empty() {
314 first_item.suffix.as_deref()
315 } else {
316 None
317 };
318
319 Ok(Some(fmt.citation(
320 group_ids,
321 self.affix_content(&fmt, content, Some(prefix), suffix),
322 )))
323 }
324
325 fn build_grouped_citation_content(
326 &self,
327 author_part: &str,
328 item_parts: &[String],
329 params: &GroupRenderParams<'_>,
330 group_delimiter: Option<&str>,
331 ) -> Option<String> {
332 if !author_part.is_empty() && !item_parts.is_empty() {
333 let author_item_delimiter = group_delimiter.unwrap_or(params.intra_delimiter);
334 let joined_items = match params.mode {
335 citum_schema::citation::CitationMode::Integral => {
336 self.join_integral_group_item_parts(item_parts, author_item_delimiter)
337 }
338 citum_schema::citation::CitationMode::NonIntegral => {
339 let repeated_item_delimiter = if author_item_delimiter.trim().is_empty() {
340 ", "
341 } else {
342 author_item_delimiter
343 };
344 item_parts.join(repeated_item_delimiter)
345 }
346 };
347 return Some(match params.mode {
348 citum_schema::citation::CitationMode::Integral => self
349 .format_integral_grouped_items(
350 author_part,
351 &joined_items,
352 params.suppress_author,
353 ),
354 citum_schema::citation::CitationMode::NonIntegral => self
355 .format_non_integral_grouped_items(
356 author_part,
357 author_item_delimiter,
358 &joined_items,
359 params.suppress_author,
360 ),
361 });
362 }
363
364 if !author_part.is_empty() {
365 return Some(author_part.to_string());
366 }
367
368 if !item_parts.is_empty() {
369 return Some(item_parts.join(params.intra_delimiter));
370 }
371
372 None
373 }
374
375 fn format_integral_grouped_items(
376 &self,
377 author_part: &str,
378 joined_items: &str,
379 suppress_author: bool,
380 ) -> String {
381 if suppress_author {
382 format!("({joined_items})")
383 } else {
384 format!("{author_part} ({joined_items})")
385 }
386 }
387
388 fn format_non_integral_grouped_items(
389 &self,
390 author_part: &str,
391 author_item_delimiter: &str,
392 joined_items: &str,
393 suppress_author: bool,
394 ) -> String {
395 if suppress_author {
396 return joined_items.to_string();
397 }
398
399 if let Some(adjusted) =
400 self.adjust_grouped_author_quote_punctuation(author_part, author_item_delimiter)
401 {
402 return format!("{adjusted}{joined_items}");
403 }
404
405 format!("{author_part}{author_item_delimiter}{joined_items}")
406 }
407
408 fn adjust_grouped_author_quote_punctuation(
409 &self,
410 author_part: &str,
411 author_item_delimiter: &str,
412 ) -> Option<String> {
413 if !self.config.punctuation_in_quote
414 || !author_item_delimiter.starts_with(',')
415 || !(author_part.ends_with('"') || author_part.ends_with('\u{201D}'))
416 {
417 return None;
418 }
419
420 let is_curly = author_part.ends_with('\u{201D}');
421 let quote_char = if is_curly { '\u{201D}' } else { '"' };
422 #[allow(clippy::string_slice, reason = "quote found at end")]
423 let trimmed = &author_part[..author_part.len() - quote_char.len_utf8()];
424 #[allow(clippy::string_slice, reason = "delimiter checked to start with ','")]
425 Some(format!(
426 "{trimmed},{quote_char}{}",
427 &author_item_delimiter[1..]
428 ))
429 }
430
431 fn render_group_item_parts_with_format<F>(
432 &self,
433 fmt: &F,
434 group: &[&crate::reference::CitationItem],
435 params: &GroupRenderParams<'_>,
436 ) -> Result<(Vec<String>, Option<String>), ProcessorError>
437 where
438 F: crate::render::format::OutputFormat<Output = String>,
439 {
440 let mut item_parts = Vec::new();
441 let mut group_delimiter: Option<String> = None;
442 for (index, item) in group.iter().enumerate() {
443 let state = self.resolve_item_render_state(item, params.spec)?;
444 let (filtered_template, leading_affix) = filter_author_from_template(&state.template);
445 if group_delimiter.is_none() {
446 group_delimiter = leading_affix
447 .as_ref()
448 .filter(|value| !value.is_empty())
449 .cloned();
450 }
451 let item_delimiter = if leading_affix.is_some() {
452 ""
453 } else {
454 params.intra_delimiter
455 };
456 if let Some(item_str) = self.render_group_item_from_template_with_format::<F>(
457 state.reference,
458 GroupItemRenderRequest {
459 item: state.item,
460 template: &filtered_template,
461 mode: params.mode,
462 suppress_author: params.suppress_author,
463 position: params.position,
464 note_start_text_case: params.note_start_text_case,
465 delimiter: item_delimiter,
466 },
467 ) && !item_str.is_empty()
468 {
469 let prefix = (index > 0).then_some(item.prefix.as_deref()).flatten();
470 item_parts.push(self.affix_content(fmt, item_str, prefix, item.suffix.as_deref()));
471 }
472 }
473 Ok((item_parts, group_delimiter))
474 }
475
476 fn resolve_group_render_state<'b>(
477 &'b self,
478 group: &'b [&'b crate::reference::CitationItem],
479 spec: &'b citum_schema::CitationSpec,
480 ) -> Result<GroupRenderState<'b>, ProcessorError> {
481 #[allow(clippy::indexing_slicing, reason = "groups are non-empty")]
482 let first_item = group[0];
483 let first_ref = self
484 .bibliography
485 .get(&first_item.id)
486 .ok_or_else(|| ProcessorError::ReferenceNotFound(first_item.id.clone()))?;
487 let first_language = crate::values::effective_item_language(first_ref);
488 let default_template = spec
489 .resolve_template_for_language(first_language.as_deref())
490 .map(Cow::Owned);
491
492 let ref_type = first_ref.ref_type();
493 let first_template = resolve_type_variant(spec.type_variants.as_ref(), &ref_type)
494 .map(Cow::Borrowed)
495 .or(default_template);
496
497 Ok(GroupRenderState {
498 first_item,
499 first_ref,
500 template: first_template.unwrap_or(Cow::Borrowed(&[])),
501 })
502 }
503
504 fn resolve_item_render_state<'b>(
505 &'b self,
506 item: &'b crate::reference::CitationItem,
507 spec: &'b citum_schema::CitationSpec,
508 ) -> Result<ItemRenderState<'b>, ProcessorError> {
509 let reference = self
510 .bibliography
511 .get(&item.id)
512 .ok_or_else(|| ProcessorError::ReferenceNotFound(item.id.clone()))?;
513 let item_language = crate::values::effective_item_language(reference);
514 let default_template = spec
515 .resolve_template_for_language(item_language.as_deref())
516 .map(Cow::Owned);
517
518 let ref_type = reference.ref_type();
519 let item_template = resolve_type_variant(spec.type_variants.as_ref(), &ref_type)
520 .map(Cow::Borrowed)
521 .or(default_template);
522
523 Ok(ItemRenderState {
524 item,
525 reference,
526 template: item_template.unwrap_or(Cow::Borrowed(&[])),
527 })
528 }
529
530 fn try_render_integral_group_with_format<F>(
531 &self,
532 group: &[&crate::reference::CitationItem],
533 spec: &citum_schema::CitationSpec,
534 mode: &citum_schema::citation::CitationMode,
535 suppress_author: bool,
536 position: Option<&citum_schema::citation::Position>,
537 ) -> Result<Option<String>, ProcessorError>
538 where
539 F: crate::render::format::OutputFormat<Output = String>,
540 {
541 if !matches!(mode, citum_schema::citation::CitationMode::Integral)
542 || !self.has_explicit_integral_template()
543 {
544 return Ok(None);
545 }
546
547 self.render_integral_explicit_group::<F>(group, spec, mode, suppress_author, position)
548 }
549
550 fn requires_full_group_item_rendering(
561 &self,
562 mode: &citum_schema::citation::CitationMode,
563 reference: &Reference,
564 ) -> bool {
565 matches!(mode, citum_schema::citation::CitationMode::NonIntegral)
566 && matches!(
567 reference.ref_type().as_str(),
568 "legal-case" | "treaty" | "hearing" | "personal-communication"
569 )
570 }
571
572 pub(crate) fn render_author_for_grouping_with_format<F>(
574 &self,
575 reference: &Reference,
576 item: &crate::reference::CitationItem,
577 template: &[TemplateComponent],
578 mode: &citum_schema::citation::CitationMode,
579 suppress_author: bool,
580 position: Option<&citum_schema::citation::Position>,
581 ) -> String
582 where
583 F: crate::render::format::OutputFormat<Output = String>,
584 {
585 let is_note_processing = self.config.processing.as_ref().is_some_and(|processing| {
586 matches!(processing, citum_schema::options::Processing::Note)
587 });
588 if is_note_processing
589 && matches!(
590 position,
591 Some(
592 citum_schema::citation::Position::Ibid
593 | citum_schema::citation::Position::IbidWithLocator
594 )
595 )
596 && !template.iter().any(has_contributor_component)
597 {
598 return String::new();
599 }
600
601 let options = self.citation_render_options(mode.clone(), suppress_author, None, None);
602
603 if let Some(comp) = template.first().and_then(find_grouping_component) {
607 let base_hints = self
608 .hints
609 .get(reference.id().as_deref().unwrap_or_default())
610 .cloned()
611 .unwrap_or_default();
612 let hints = ProcHints {
614 position: position.cloned(),
615 integral_name_state: item.integral_name_state,
616 ..base_hints
617 };
618 if let Some(vals) = comp.values::<F>(reference, &hints, &options)
619 && !vals.value.is_empty()
620 {
621 return vals.value;
622 }
623 }
624
625 if let Some(authors) = reference.author() {
627 let names_vec = self.resolve_contributor_names(&authors);
628 F::default().text(&crate::values::format_contributors_short(
629 &names_vec, &options,
630 ))
631 } else {
632 String::new()
633 }
634 }
635
636 pub(crate) fn render_integral_anchor_with_format<F>(
638 &self,
639 items: &[crate::reference::CitationItem],
640 spec: &citum_schema::CitationSpec,
641 inter_delimiter: &str,
642 suppress_author: bool,
643 position: Option<&citum_schema::citation::Position>,
644 ) -> Result<String, ProcessorError>
645 where
646 F: crate::render::format::OutputFormat<Output = String>,
647 {
648 let groups = group_citation_items_by_author(self, items);
649
650 let mut rendered_groups = Vec::new();
651 let fmt = F::default();
652 for (_author_key, group) in groups {
653 #[allow(
654 clippy::indexing_slicing,
655 reason = "group is non-empty by construction"
656 )]
657 let first_item = group[0];
658 let reference = self
659 .bibliography
660 .get(&first_item.id)
661 .ok_or_else(|| ProcessorError::ReferenceNotFound(first_item.id.clone()))?;
662 let item_language = crate::values::effective_item_language(reference);
663 let template = spec.resolve_template_for_language(item_language.as_deref());
664 let effective_template = template.as_deref().unwrap_or(&[]);
665 let author_part = self.render_author_for_grouping_with_format::<F>(
666 reference,
667 first_item,
668 effective_template,
669 &citum_schema::citation::CitationMode::Integral,
670 suppress_author,
671 position,
672 );
673 if !author_part.is_empty() {
674 rendered_groups.push(author_part);
675 }
676 }
677
678 Ok(fmt.join(rendered_groups, inter_delimiter))
679 }
680
681 #[must_use]
683 pub fn get_or_assign_citation_number(&self, ref_id: &str) -> usize {
684 let mut numbers = self.citation_numbers.borrow_mut();
685 let next_num = numbers.len() + 1;
686 *numbers.entry(ref_id.to_string()).or_insert(next_num)
687 }
688
689 #[must_use]
691 pub fn process_bibliography_entry(
692 &self,
693 reference: &Reference,
694 entry_number: usize,
695 ) -> Option<ProcTemplate> {
696 self.process_bibliography_entry_with_format::<crate::render::plain::PlainText>(
697 reference,
698 entry_number,
699 )
700 }
701
702 #[must_use]
704 pub fn process_bibliography_entry_with_format<F>(
705 &self,
706 reference: &Reference,
707 entry_number: usize,
708 ) -> Option<ProcTemplate>
709 where
710 F: crate::render::format::OutputFormat<Output = String>,
711 {
712 let bib_spec = self.style.bibliography.as_ref()?;
713
714 let item_language = crate::values::effective_item_language(reference);
715 let default_template = bib_spec
716 .resolve_template_for_language(item_language.as_deref())
717 .map(Cow::Owned);
718
719 let ref_type = reference.ref_type();
720 let template = resolve_type_variant(bib_spec.type_variants.as_ref(), &ref_type)
721 .map(Cow::Borrowed)
722 .or(default_template)?;
723
724 let template = self.apply_anonymous_entry_bibliography_policy(reference, template)?;
725 let template = self.apply_article_journal_bibliography_policy(reference, template);
726
727 self.process_template_request_with_format::<F>(
728 reference,
729 TemplateRenderRequest {
730 template: template.as_ref(),
731 context: RenderContext::Bibliography,
732 mode: citum_schema::citation::CitationMode::NonIntegral,
733 suppress_author: false,
734 locator_raw: None,
735 citation_number: entry_number,
736 position: None,
737 note_start_text_case: None,
738 integral_name_state: None,
739 },
740 )
741 }
742
743 #[must_use]
748 pub fn process_template_with_number(
749 &self,
750 reference: &Reference,
751 params: TemplateRenderParams<'_>,
752 ) -> Option<ProcTemplate> {
753 self.process_template_with_number_with_format::<crate::render::plain::PlainText>(
754 reference, params,
755 )
756 }
757
758 pub fn process_template_with_number_with_format<F>(
763 &self,
764 reference: &Reference,
765 params: TemplateRenderParams<'_>,
766 ) -> Option<ProcTemplate>
767 where
768 F: crate::render::format::OutputFormat<Output = String>,
769 {
770 self.process_template_request_with_format::<F>(
771 reference,
772 TemplateRenderRequest {
773 template: params.template,
774 context: params.context,
775 mode: params.mode,
776 suppress_author: params.suppress_author,
777 locator_raw: params.locator_raw,
778 citation_number: params.citation_number,
779 position: params.position.cloned(),
780 note_start_text_case: params.note_start_text_case,
781 integral_name_state: params.integral_name_state,
782 },
783 )
784 }
785
786 #[must_use]
788 pub fn process_template_request_with_format<F>(
789 &self,
790 reference: &Reference,
791 request: TemplateRenderRequest<'_>,
792 ) -> Option<ProcTemplate>
793 where
794 F: crate::render::format::OutputFormat<Output = String>,
795 {
796 let TemplateRenderRequest {
797 template,
798 context,
799 mode,
800 suppress_author,
801 locator_raw,
802 citation_number,
803 position,
804 note_start_text_case,
805 integral_name_state,
806 } = request;
807 let ref_type = reference.ref_type();
808 let options = RenderOptions {
809 config: self.config,
810 bibliography_config: self.bibliography_config.clone(),
811 locale: self.locale,
812 context,
813 mode,
814 suppress_author,
815 locator_raw,
816 ref_type: Some(ref_type.clone()),
817 show_semantics: self.show_semantics,
818 current_template_index: None,
819 abbreviation_map: self.abbreviation_map,
820 };
821 let hint = self.build_template_render_hint(
822 reference,
823 options.context,
824 citation_number,
825 position,
826 integral_name_state,
827 );
828 let mut components =
829 self.render_template_components::<F>(reference, &ref_type, &options, &hint, template);
830
831 self.apply_sentence_initial_context::<F>(&mut components, context, note_start_text_case);
832
833 (!components.is_empty()).then_some(components)
834 }
835
836 fn render_template_components<F>(
840 &self,
841 reference: &Reference,
842 ref_type: &str,
843 options: &RenderOptions<'_>,
844 hint: &ProcHints,
845 template: &[TemplateComponent],
846 ) -> Vec<ProcTemplateComponent>
847 where
848 F: crate::render::format::OutputFormat<Output = String>,
849 {
850 let mut tracker = TemplateComponentTracker::default();
851 let mut components = Vec::with_capacity(template.len());
852 let mut component_options = options.clone();
853 for (template_index, component) in template.iter().enumerate() {
854 component_options.current_template_index =
855 self.inject_ast_indices.then_some(template_index);
856 let ctx = TemplateRenderContext {
857 reference,
858 ref_type,
859 options: &component_options,
860 hint,
861 template_index,
862 };
863 if let Some(component) =
864 self.render_template_component_with_format::<F>(&ctx, component, &mut tracker)
865 {
866 components.push(component);
867 }
868 }
869 components
870 }
871
872 fn build_template_render_hint(
873 &self,
874 reference: &Reference,
875 context: RenderContext,
876 citation_number: usize,
877 position: Option<citum_schema::citation::Position>,
878 integral_name_state: Option<citum_schema::citation::IntegralNameState>,
879 ) -> ProcHints {
880 let default_hint = ProcHints::default();
881 let base_hint = self
882 .hints
883 .get(reference.id().as_deref().unwrap_or_default())
884 .unwrap_or(&default_hint);
885 ProcHints {
886 citation_number: (citation_number > 0).then_some(citation_number),
887 citation_sub_label: if context == RenderContext::Citation {
888 reference
889 .id()
890 .as_deref()
891 .and_then(|id| self.citation_sub_label_for_ref(id))
892 } else {
893 None
894 },
895 position,
896 integral_name_state,
897 ..base_hint.clone()
898 }
899 }
900
901 fn render_template_component_with_format<F>(
902 &self,
903 ctx: &TemplateRenderContext<'_>,
904 component: &TemplateComponent,
905 tracker: &mut TemplateComponentTracker,
906 ) -> Option<ProcTemplateComponent>
907 where
908 F: crate::render::format::OutputFormat<Output = String>,
909 {
910 if let TemplateComponent::Group(group) = component {
911 return self.render_group_component_with_format::<F>(ctx, group, tracker);
912 }
913
914 let resolved_component = component;
915 let var_key = get_variable_key(resolved_component);
916 if tracker.should_skip(var_key.as_deref()) {
917 return None;
918 }
919
920 let mut values = resolved_component.values::<F>(ctx.reference, ctx.hint, ctx.options)?;
921 if values.value.trim().is_empty() {
925 return None;
926 }
927 self.apply_issued_no_date_fallback(
928 ctx.reference,
929 ctx.options,
930 resolved_component,
931 &mut values,
932 );
933 self.apply_entry_link_fallback(ctx.reference, ctx.options, &mut values);
934
935 let item_language =
936 crate::values::effective_component_language(ctx.reference, resolved_component);
937 tracker.mark_rendered(var_key, values.substituted_key.as_deref());
938
939 Some(ProcTemplateComponent {
940 template_component: resolved_component.clone(),
941 template_index: self.inject_ast_indices.then_some(ctx.template_index),
942 value: values.value,
943 prefix: values.prefix,
944 suffix: values.suffix,
945 url: values.url,
946 ref_type: Some(ctx.ref_type.to_string()),
947 config: Some(ctx.options.config.clone()),
948 bibliography_config: ctx.options.bibliography_config.clone(),
949 item_language,
950 sentence_initial: false,
951 pre_formatted: values.pre_formatted,
952 })
953 }
954
955 fn render_group_component_with_format<F>(
956 &self,
957 ctx: &TemplateRenderContext<'_>,
958 group: &citum_schema::template::TemplateGroup,
959 tracker: &mut TemplateComponentTracker,
960 ) -> Option<ProcTemplateComponent>
961 where
962 F: crate::render::format::OutputFormat<Output = String>,
963 {
964 let fmt = F::default();
965 let values = self.render_group_child_values(&fmt, ctx, group, tracker)?;
966 let delimiter = group
967 .delimiter
968 .as_ref()
969 .unwrap_or(&citum_schema::template::DelimiterPunctuation::Comma)
970 .to_string_with_space();
971 let group_component = TemplateComponent::Group(group.clone());
972 Some(ProcTemplateComponent {
973 template_component: group_component.clone(),
974 template_index: self.inject_ast_indices.then_some(ctx.template_index),
975 value: fmt.join(values, &delimiter),
976 prefix: None,
977 suffix: None,
978 url: None,
979 ref_type: Some(ctx.ref_type.to_string()),
980 config: Some(ctx.options.config.clone()),
981 bibliography_config: ctx.options.bibliography_config.clone(),
982 item_language: crate::values::effective_component_language(
983 ctx.reference,
984 &group_component,
985 ),
986 sentence_initial: false,
987 pre_formatted: true,
988 })
989 }
990
991 fn render_group_child_values<F>(
997 &self,
998 fmt: &F,
999 ctx: &TemplateRenderContext<'_>,
1000 group: &citum_schema::template::TemplateGroup,
1001 tracker: &mut TemplateComponentTracker,
1002 ) -> Option<Vec<String>>
1003 where
1004 F: crate::render::format::OutputFormat<Output = String>,
1005 {
1006 let mut has_meaningful_content = false;
1007 let mut values = Vec::with_capacity(group.group.len());
1008
1009 for item in &group.group {
1010 let Some(rendered) =
1011 self.render_template_component_with_format::<F>(ctx, item, tracker)
1012 else {
1013 continue;
1014 };
1015 let rendered_str = crate::render::render_component_with_format_and_renderer::<F>(
1016 &rendered,
1017 fmt,
1018 ctx.options.show_semantics,
1019 );
1020 if rendered_str.trim().is_empty() {
1021 continue;
1022 }
1023 if !is_term_only_component(item) {
1024 has_meaningful_content = true;
1025 }
1026 values.push(rendered_str);
1027 }
1028
1029 (has_meaningful_content && !values.is_empty()).then_some(values)
1030 }
1031
1032 fn apply_issued_no_date_fallback(
1033 &self,
1034 reference: &Reference,
1035 options: &RenderOptions<'_>,
1036 component: &TemplateComponent,
1037 values: &mut crate::values::ProcValues<String>,
1038 ) {
1039 if !matches!(
1040 component,
1041 TemplateComponent::Date(citum_schema::template::TemplateDate {
1042 date: citum_schema::template::DateVariable::Issued,
1043 ..
1044 })
1045 ) || reference.csl_issued_date().is_some()
1046 || self.preferred_no_date_term_form() != citum_schema::locale::TermForm::Long
1047 {
1048 return;
1049 }
1050
1051 if let Some(long) = options.locale.resolved_general_term(
1052 &citum_schema::locale::GeneralTerm::NoDate,
1053 &citum_schema::locale::TermForm::Long,
1054 None,
1055 ) {
1056 values.value = long;
1057 }
1058 }
1059
1060 fn apply_entry_link_fallback(
1061 &self,
1062 reference: &Reference,
1063 options: &RenderOptions<'_>,
1064 values: &mut crate::values::ProcValues<String>,
1065 ) {
1066 if values.url.is_some() {
1067 return;
1068 }
1069
1070 let Some(links) = &options.config.links else {
1071 return;
1072 };
1073 use citum_schema::options::LinkAnchor;
1074 if matches!(links.anchor, Some(LinkAnchor::Entry)) {
1075 values.url = crate::values::resolve_url(links, reference);
1076 }
1077 }
1078
1079 pub fn apply_author_substitution(&self, proc: &mut ProcTemplate, substitute: &str) {
1081 self.apply_author_substitution_with_format::<crate::render::plain::PlainText>(
1082 proc, substitute,
1083 );
1084 }
1085
1086 pub fn apply_author_substitution_with_format<F>(
1088 &self,
1089 proc: &mut ProcTemplate,
1090 substitute: &str,
1091 ) where
1092 F: crate::render::format::OutputFormat<Output = String>,
1093 {
1094 if let Some(component) = proc
1095 .iter_mut()
1096 .find(|c| matches!(c.template_component, TemplateComponent::Contributor(_)))
1097 {
1098 let fmt = F::default();
1099 component.value = fmt.text(substitute);
1100 }
1101 }
1102
1103 fn preferred_no_date_term_form(&self) -> citum_schema::locale::TermForm {
1104 match self
1105 .style
1106 .info
1107 .source
1108 .as_ref()
1109 .map(|source| source.csl_id.as_str())
1110 {
1111 Some("http://www.zotero.org/styles/harvard-cite-them-right") => {
1112 citum_schema::locale::TermForm::Long
1113 }
1114 _ => citum_schema::locale::TermForm::Short,
1115 }
1116 }
1117
1118 fn render_group_item_from_template_with_format<F>(
1119 &self,
1120 reference: &Reference,
1121 item_request: GroupItemRenderRequest<'_>,
1122 ) -> Option<String>
1123 where
1124 F: crate::render::format::OutputFormat<Output = String>,
1125 {
1126 let request = self.citation_render_request(
1127 item_request.item,
1128 item_request.template,
1129 item_request.mode,
1130 item_request.suppress_author,
1131 item_request.position,
1132 item_request.note_start_text_case,
1133 );
1134 self.render_item_from_template_with_format::<F>(reference, request, item_request.delimiter)
1135 }
1136}
1137
1138pub(super) fn filter_author_from_template(
1139 template: &[TemplateComponent],
1140) -> (Vec<TemplateComponent>, Option<String>) {
1141 let mut filtered: Vec<TemplateComponent> =
1142 template.iter().filter_map(strip_author_component).collect();
1143 let leading_affix = filtered.first().and_then(leading_group_affix);
1144 if let Some(first) = filtered.first_mut() {
1145 strip_leading_group_affixes(first);
1146 }
1147 (filtered, leading_affix)
1148}