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 org_abbreviation_state: None,
740 },
741 )
742 }
743
744 #[must_use]
749 pub fn process_template_with_number(
750 &self,
751 reference: &Reference,
752 params: TemplateRenderParams<'_>,
753 ) -> Option<ProcTemplate> {
754 self.process_template_with_number_with_format::<crate::render::plain::PlainText>(
755 reference, params,
756 )
757 }
758
759 pub fn process_template_with_number_with_format<F>(
764 &self,
765 reference: &Reference,
766 params: TemplateRenderParams<'_>,
767 ) -> Option<ProcTemplate>
768 where
769 F: crate::render::format::OutputFormat<Output = String>,
770 {
771 self.process_template_request_with_format::<F>(
772 reference,
773 TemplateRenderRequest {
774 template: params.template,
775 context: params.context,
776 mode: params.mode,
777 suppress_author: params.suppress_author,
778 locator_raw: params.locator_raw,
779 citation_number: params.citation_number,
780 position: params.position.cloned(),
781 note_start_text_case: params.note_start_text_case,
782 integral_name_state: params.integral_name_state,
783 org_abbreviation_state: params.org_abbreviation_state,
784 },
785 )
786 }
787
788 #[must_use]
790 pub fn process_template_request_with_format<F>(
791 &self,
792 reference: &Reference,
793 request: TemplateRenderRequest<'_>,
794 ) -> Option<ProcTemplate>
795 where
796 F: crate::render::format::OutputFormat<Output = String>,
797 {
798 let TemplateRenderRequest {
799 template,
800 context,
801 mode,
802 suppress_author,
803 locator_raw,
804 citation_number,
805 position,
806 note_start_text_case,
807 integral_name_state,
808 org_abbreviation_state,
809 } = request;
810 let ref_type = reference.ref_type();
811 let options = RenderOptions {
812 config: self.config,
813 bibliography_config: self.bibliography_config.clone(),
814 locale: self.locale,
815 context,
816 mode,
817 suppress_author,
818 locator_raw,
819 ref_type: Some(ref_type.clone()),
820 show_semantics: self.show_semantics,
821 current_template_index: None,
822 abbreviation_map: self.abbreviation_map,
823 };
824 let hint = self.build_template_render_hint(
825 reference,
826 options.context,
827 citation_number,
828 position,
829 integral_name_state,
830 org_abbreviation_state,
831 );
832 let mut components =
833 self.render_template_components::<F>(reference, &ref_type, &options, &hint, template);
834
835 self.apply_sentence_initial_context::<F>(&mut components, context, note_start_text_case);
836
837 (!components.is_empty()).then_some(components)
838 }
839
840 fn render_template_components<F>(
844 &self,
845 reference: &Reference,
846 ref_type: &str,
847 options: &RenderOptions<'_>,
848 hint: &ProcHints,
849 template: &[TemplateComponent],
850 ) -> Vec<ProcTemplateComponent>
851 where
852 F: crate::render::format::OutputFormat<Output = String>,
853 {
854 let mut tracker = TemplateComponentTracker::default();
855 let mut components = Vec::with_capacity(template.len());
856 let mut component_options = options.clone();
857 for (template_index, component) in template.iter().enumerate() {
858 component_options.current_template_index =
859 self.inject_ast_indices.then_some(template_index);
860 let ctx = TemplateRenderContext {
861 reference,
862 ref_type,
863 options: &component_options,
864 hint,
865 template_index,
866 };
867 if let Some(component) =
868 self.render_template_component_with_format::<F>(&ctx, component, &mut tracker)
869 {
870 components.push(component);
871 }
872 }
873 components
874 }
875
876 fn build_template_render_hint(
877 &self,
878 reference: &Reference,
879 context: RenderContext,
880 citation_number: usize,
881 position: Option<citum_schema::citation::Position>,
882 integral_name_state: Option<citum_schema::citation::IntegralNameState>,
883 org_abbreviation_state: Option<citum_schema::citation::IntegralNameState>,
884 ) -> ProcHints {
885 let default_hint = ProcHints::default();
886 let base_hint = self
887 .hints
888 .get(reference.id().as_deref().unwrap_or_default())
889 .unwrap_or(&default_hint);
890 ProcHints {
891 citation_number: (citation_number > 0).then_some(citation_number),
892 citation_sub_label: if context == RenderContext::Citation {
893 reference
894 .id()
895 .as_deref()
896 .and_then(|id| self.citation_sub_label_for_ref(id))
897 } else {
898 None
899 },
900 position,
901 integral_name_state,
902 org_abbreviation_state,
903 ..base_hint.clone()
904 }
905 }
906
907 fn render_template_component_with_format<F>(
908 &self,
909 ctx: &TemplateRenderContext<'_>,
910 component: &TemplateComponent,
911 tracker: &mut TemplateComponentTracker,
912 ) -> Option<ProcTemplateComponent>
913 where
914 F: crate::render::format::OutputFormat<Output = String>,
915 {
916 if let TemplateComponent::Group(group) = component {
917 return self.render_group_component_with_format::<F>(ctx, group, tracker);
918 }
919
920 let resolved_component = component;
921 let var_key = get_variable_key(resolved_component);
922 if tracker.should_skip(var_key.as_deref()) {
923 return None;
924 }
925
926 let mut values = resolved_component.values::<F>(ctx.reference, ctx.hint, ctx.options)?;
927 if values.value.trim().is_empty() {
931 return None;
932 }
933 self.apply_issued_no_date_fallback(
934 ctx.reference,
935 ctx.options,
936 resolved_component,
937 &mut values,
938 );
939 self.apply_entry_link_fallback(ctx.reference, ctx.options, &mut values);
940
941 let item_language =
942 crate::values::effective_component_language(ctx.reference, resolved_component);
943 tracker.mark_rendered(var_key, values.substituted_key.as_deref());
944
945 Some(ProcTemplateComponent {
946 template_component: resolved_component.clone(),
947 template_index: self.inject_ast_indices.then_some(ctx.template_index),
948 value: values.value,
949 prefix: values.prefix,
950 suffix: values.suffix,
951 url: values.url,
952 ref_type: Some(ctx.ref_type.to_string()),
953 config: Some(ctx.options.config.clone()),
954 bibliography_config: ctx.options.bibliography_config.clone(),
955 item_language,
956 sentence_initial: false,
957 pre_formatted: values.pre_formatted,
958 })
959 }
960
961 fn render_group_component_with_format<F>(
962 &self,
963 ctx: &TemplateRenderContext<'_>,
964 group: &citum_schema::template::TemplateGroup,
965 tracker: &mut TemplateComponentTracker,
966 ) -> Option<ProcTemplateComponent>
967 where
968 F: crate::render::format::OutputFormat<Output = String>,
969 {
970 let fmt = F::default();
971 let values = self.render_group_child_values(&fmt, ctx, group, tracker)?;
972 let delimiter = group
973 .delimiter
974 .as_ref()
975 .unwrap_or(&citum_schema::template::DelimiterPunctuation::Comma)
976 .to_string_with_space();
977 let group_component = TemplateComponent::Group(group.clone());
978 Some(ProcTemplateComponent {
979 template_component: group_component.clone(),
980 template_index: self.inject_ast_indices.then_some(ctx.template_index),
981 value: fmt.join(values, &delimiter),
982 prefix: None,
983 suffix: None,
984 url: None,
985 ref_type: Some(ctx.ref_type.to_string()),
986 config: Some(ctx.options.config.clone()),
987 bibliography_config: ctx.options.bibliography_config.clone(),
988 item_language: crate::values::effective_component_language(
989 ctx.reference,
990 &group_component,
991 ),
992 sentence_initial: false,
993 pre_formatted: true,
994 })
995 }
996
997 fn render_group_child_values<F>(
1003 &self,
1004 fmt: &F,
1005 ctx: &TemplateRenderContext<'_>,
1006 group: &citum_schema::template::TemplateGroup,
1007 tracker: &mut TemplateComponentTracker,
1008 ) -> Option<Vec<String>>
1009 where
1010 F: crate::render::format::OutputFormat<Output = String>,
1011 {
1012 let mut has_meaningful_content = false;
1013 let mut values = Vec::with_capacity(group.group.len());
1014
1015 for item in &group.group {
1016 let Some(rendered) =
1017 self.render_template_component_with_format::<F>(ctx, item, tracker)
1018 else {
1019 continue;
1020 };
1021 let rendered_str = crate::render::render_component_with_format_and_renderer::<F>(
1022 &rendered,
1023 fmt,
1024 ctx.options.show_semantics,
1025 );
1026 if rendered_str.trim().is_empty() {
1027 continue;
1028 }
1029 if !is_term_only_component(item) {
1030 has_meaningful_content = true;
1031 }
1032 values.push(rendered_str);
1033 }
1034
1035 (has_meaningful_content && !values.is_empty()).then_some(values)
1036 }
1037
1038 fn apply_issued_no_date_fallback(
1039 &self,
1040 reference: &Reference,
1041 options: &RenderOptions<'_>,
1042 component: &TemplateComponent,
1043 values: &mut crate::values::ProcValues<String>,
1044 ) {
1045 if !matches!(
1046 component,
1047 TemplateComponent::Date(citum_schema::template::TemplateDate {
1048 date: citum_schema::template::DateVariable::Issued,
1049 ..
1050 })
1051 ) || reference.csl_issued_date().is_some()
1052 || self.preferred_no_date_term_form() != citum_schema::locale::TermForm::Long
1053 {
1054 return;
1055 }
1056
1057 if let Some(long) = options.locale.resolved_general_term(
1058 &citum_schema::locale::GeneralTerm::NoDate,
1059 &citum_schema::locale::TermForm::Long,
1060 None,
1061 ) {
1062 values.value = long;
1063 }
1064 }
1065
1066 fn apply_entry_link_fallback(
1067 &self,
1068 reference: &Reference,
1069 options: &RenderOptions<'_>,
1070 values: &mut crate::values::ProcValues<String>,
1071 ) {
1072 if values.url.is_some() {
1073 return;
1074 }
1075
1076 let Some(links) = &options.config.links else {
1077 return;
1078 };
1079 use citum_schema::options::LinkAnchor;
1080 if matches!(links.anchor, Some(LinkAnchor::Entry)) {
1081 values.url = crate::values::resolve_url(links, reference);
1082 }
1083 }
1084
1085 pub fn apply_author_substitution(&self, proc: &mut ProcTemplate, substitute: &str) {
1087 self.apply_author_substitution_with_format::<crate::render::plain::PlainText>(
1088 proc, substitute,
1089 );
1090 }
1091
1092 pub fn apply_author_substitution_with_format<F>(
1094 &self,
1095 proc: &mut ProcTemplate,
1096 substitute: &str,
1097 ) where
1098 F: crate::render::format::OutputFormat<Output = String>,
1099 {
1100 if let Some(component) = proc
1101 .iter_mut()
1102 .find(|c| matches!(c.template_component, TemplateComponent::Contributor(_)))
1103 {
1104 let fmt = F::default();
1105 component.value = fmt.text(substitute);
1106 }
1107 }
1108
1109 fn preferred_no_date_term_form(&self) -> citum_schema::locale::TermForm {
1110 match self
1111 .style
1112 .info
1113 .source
1114 .as_ref()
1115 .map(|source| source.csl_id.as_str())
1116 {
1117 Some("http://www.zotero.org/styles/harvard-cite-them-right") => {
1118 citum_schema::locale::TermForm::Long
1119 }
1120 _ => citum_schema::locale::TermForm::Short,
1121 }
1122 }
1123
1124 fn render_group_item_from_template_with_format<F>(
1125 &self,
1126 reference: &Reference,
1127 item_request: GroupItemRenderRequest<'_>,
1128 ) -> Option<String>
1129 where
1130 F: crate::render::format::OutputFormat<Output = String>,
1131 {
1132 let request = self.citation_render_request(
1133 item_request.item,
1134 item_request.template,
1135 item_request.mode,
1136 item_request.suppress_author,
1137 item_request.position,
1138 item_request.note_start_text_case,
1139 );
1140 self.render_item_from_template_with_format::<F>(reference, request, item_request.delimiter)
1141 }
1142}
1143
1144pub(super) fn filter_author_from_template(
1145 template: &[TemplateComponent],
1146) -> (Vec<TemplateComponent>, Option<String>) {
1147 let mut filtered: Vec<TemplateComponent> =
1148 template.iter().filter_map(strip_author_component).collect();
1149 let leading_affix = filtered.first().and_then(leading_group_affix);
1150 if let Some(first) = filtered.first_mut() {
1151 strip_leading_group_affixes(first);
1152 }
1153 (filtered, leading_affix)
1154}