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, WrapConfig, WrapPunctuation};
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
55struct HintInputs<'a> {
59 reference: &'a Reference,
60 context: RenderContext,
61 citation_number: usize,
62 position: Option<citum_schema::citation::Position>,
63 integral_name_state: Option<citum_schema::citation::IntegralNameState>,
64 org_abbreviation_state: Option<citum_schema::citation::IntegralNameState>,
65 first_reference_note_number: Option<u32>,
66}
67
68impl Renderer<'_> {
69 fn strip_redundant_leading_group_punctuation<'a>(
70 &self,
71 value: &'a str,
72 delimiter: &str,
73 ) -> &'a str {
74 let Some(delimiter_char) = delimiter.chars().find(|ch| !ch.is_whitespace()) else {
75 return value;
76 };
77
78 let trimmed = value.trim_start();
79 if !trimmed.starts_with(delimiter_char) {
80 return value;
81 }
82
83 #[allow(clippy::string_slice, reason = "delimiter found at start")]
84 trimmed[delimiter_char.len_utf8()..].trim_start()
85 }
86
87 fn join_integral_group_item_parts(&self, item_parts: &[String], delimiter: &str) -> String {
88 let repeated_item_delimiter = if delimiter.trim().is_empty() {
89 ", "
90 } else {
91 delimiter
92 };
93
94 let mut joined = String::new();
95 for (index, part) in item_parts.iter().enumerate() {
96 if index > 0 {
97 joined.push_str(repeated_item_delimiter);
98 }
99
100 let normalized = if index == 0 {
101 part.as_str()
102 } else {
103 self.strip_redundant_leading_group_punctuation(part, repeated_item_delimiter)
104 };
105 joined.push_str(normalized);
106 }
107
108 joined
109 }
110
111 pub fn render_grouped_citation(
117 &self,
118 items: &[crate::reference::CitationItem],
119 spec: &citum_schema::CitationSpec,
120 mode: &citum_schema::citation::CitationMode,
121 intra_delimiter: &str,
122 suppress_author: bool,
123 position: Option<&citum_schema::citation::Position>,
124 ) -> Result<Vec<String>, ProcessorError> {
125 self.render_grouped_citation_with_format::<crate::render::plain::PlainText>(
126 items,
127 &GroupRenderParams {
128 spec,
129 mode,
130 intra_delimiter,
131 suppress_author,
132 position,
133 note_start_text_case: spec.note_start_text_case,
134 },
135 )
136 }
137
138 fn render_special_type_items<F>(
141 &self,
142 group: &[&crate::reference::CitationItem],
143 params: &GroupRenderParams<'_>,
144 ) -> Result<Vec<String>, ProcessorError>
145 where
146 F: crate::render::format::OutputFormat<Output = String>,
147 {
148 let fmt = F::default();
149 let mut rendered_items = Vec::new();
150 for item in group {
151 let state = self.resolve_item_render_state(item, params.spec)?;
152 if let Some(item_str) = self.render_group_item_from_template_with_format::<F>(
153 state.reference,
154 GroupItemRenderRequest {
155 item: state.item,
156 template: &state.template,
157 mode: params.mode,
158 suppress_author: params.suppress_author,
159 position: params.position,
160 note_start_text_case: params.note_start_text_case,
161 delimiter: params.intra_delimiter,
162 },
163 ) && let Some((ids, content)) = self.build_citation_chunk(
164 &fmt,
165 vec![item.id.clone()],
166 item_str,
167 item.prefix.as_deref(),
168 item.suffix.as_deref(),
169 ) {
170 rendered_items.push(fmt.citation(ids, content));
171 }
172 }
173 Ok(rendered_items)
174 }
175
176 fn render_integral_explicit_group<F>(
181 &self,
182 group: &[&crate::reference::CitationItem],
183 spec: &citum_schema::CitationSpec,
184 mode: &citum_schema::citation::CitationMode,
185 suppress_author: bool,
186 position: Option<&citum_schema::citation::Position>,
187 ) -> Result<Option<String>, ProcessorError>
188 where
189 F: crate::render::format::OutputFormat<Output = String>,
190 {
191 let fmt = F::default();
192 let component_delimiter = spec.delimiter.as_deref().unwrap_or(" ");
193 let item_join_delim = spec.multi_cite_delimiter.as_deref().unwrap_or(", ");
194 let mut group_items_str = Vec::new();
195 let mut all_ids = Vec::new();
196
197 for item in group {
198 let state = self.resolve_item_render_state(item, spec)?;
199 if let Some(item_str) = self.render_group_item_from_template_with_format::<F>(
200 state.reference,
201 GroupItemRenderRequest {
202 item: state.item,
203 template: &state.template,
204 mode,
205 suppress_author,
206 position,
207 note_start_text_case: spec.note_start_text_case,
208 delimiter: component_delimiter,
209 },
210 ) && !item_str.is_empty()
211 {
212 group_items_str.push(self.affix_content(
213 &fmt,
214 item_str,
215 item.prefix.as_deref(),
216 item.suffix.as_deref(),
217 ));
218 all_ids.push(item.id.clone());
219 }
220 }
221
222 if group_items_str.is_empty() {
223 return Ok(None);
224 }
225
226 let combined_str = group_items_str.join(item_join_delim);
227 Ok(Some(fmt.citation(all_ids, combined_str)))
228 }
229
230 pub fn render_grouped_citation_with_format<F>(
239 &self,
240 items: &[crate::reference::CitationItem],
241 params: &GroupRenderParams<'_>,
242 ) -> Result<Vec<String>, ProcessorError>
243 where
244 F: crate::render::format::OutputFormat<Output = String>,
245 {
246 let groups = group_citation_items_by_author(self, items);
247 let mut rendered_groups = Vec::new();
248 for (_author_key, group) in groups {
249 rendered_groups
250 .extend(self.render_grouped_citation_group_with_format::<F>(&group, params)?);
251 }
252
253 Ok(rendered_groups)
254 }
255
256 fn render_grouped_citation_group_with_format<F>(
257 &self,
258 group: &[&crate::reference::CitationItem],
259 params: &GroupRenderParams<'_>,
260 ) -> Result<Vec<String>, ProcessorError>
261 where
262 F: crate::render::format::OutputFormat<Output = String>,
263 {
264 let state = self.resolve_group_render_state(group, params.spec)?;
265
266 if group.len() == 1
271 && let Some(citation) = self.try_render_integral_group_with_format::<F>(
272 group,
273 params.spec,
274 params.mode,
275 params.suppress_author,
276 params.position,
277 )?
278 {
279 return Ok(vec![citation]);
280 }
281
282 if self.requires_full_group_item_rendering(params.mode, state.first_ref) {
283 return self.render_special_type_items::<F>(group, params);
284 }
285
286 Ok(self
287 .render_fallback_grouped_citation_with_format::<F>(
288 group,
289 state.first_ref,
290 state.first_item,
291 &state.template,
292 params,
293 )?
294 .into_iter()
295 .collect())
296 }
297
298 fn render_fallback_grouped_citation_with_format<F>(
299 &self,
300 group: &[&crate::reference::CitationItem],
301 first_ref: &Reference,
302 first_item: &crate::reference::CitationItem,
303 template: &[TemplateComponent],
304 params: &GroupRenderParams<'_>,
305 ) -> Result<Option<String>, ProcessorError>
306 where
307 F: crate::render::format::OutputFormat<Output = String>,
308 {
309 let fmt = F::default();
310 let author_part = self.render_author_for_grouping_with_format::<F>(
311 first_ref,
312 first_item,
313 template,
314 params.mode,
315 params.suppress_author,
316 params.position,
317 );
318 let (item_parts, group_delimiter, captured_year_wrap) =
319 self.render_group_item_parts_with_format::<F>(&fmt, group, params)?;
320 let pre_wrapped_years =
326 if matches!(params.mode, citum_schema::citation::CitationMode::Integral)
327 && !item_parts.is_empty()
328 {
329 let delimiter = group_delimiter.as_deref().unwrap_or(params.intra_delimiter);
330 let joined = self.join_integral_group_item_parts(&item_parts, delimiter);
331 let wrap_punct = captured_year_wrap
332 .as_ref()
333 .map(|w| &w.punctuation)
334 .unwrap_or(&WrapPunctuation::Parentheses);
335 let inner_prefix = captured_year_wrap
336 .as_ref()
337 .and_then(|w| w.inner_prefix.as_deref())
338 .unwrap_or("");
339 let inner_suffix = captured_year_wrap
340 .as_ref()
341 .and_then(|w| w.inner_suffix.as_deref())
342 .unwrap_or("");
343 let inner = fmt.inner_affix(inner_prefix, joined, inner_suffix);
344 Some(fmt.wrap_punctuation(wrap_punct, inner))
345 } else {
346 None
347 };
348 let Some(content) = self.build_grouped_citation_content(
349 &author_part,
350 &item_parts,
351 params,
352 group_delimiter.as_deref(),
353 pre_wrapped_years.as_deref(),
354 ) else {
355 return Ok(None);
356 };
357 let group_ids = group.iter().map(|item| item.id.clone()).collect();
358 let prefix = first_item.prefix.as_deref().unwrap_or("");
359 let suffix = if item_parts.is_empty() {
362 first_item.suffix.as_deref()
363 } else {
364 None
365 };
366
367 Ok(Some(fmt.citation(
368 group_ids,
369 self.affix_content(&fmt, content, Some(prefix), suffix),
370 )))
371 }
372
373 fn build_grouped_citation_content(
374 &self,
375 author_part: &str,
376 item_parts: &[String],
377 params: &GroupRenderParams<'_>,
378 group_delimiter: Option<&str>,
379 pre_wrapped_years: Option<&str>,
380 ) -> Option<String> {
381 if !author_part.is_empty() && !item_parts.is_empty() {
382 let author_item_delimiter = group_delimiter.unwrap_or(params.intra_delimiter);
383 return Some(match params.mode {
384 citum_schema::citation::CitationMode::Integral => {
385 let wrapped = pre_wrapped_years.map(str::to_string).unwrap_or_else(|| {
389 self.join_integral_group_item_parts(item_parts, author_item_delimiter)
390 });
391 self.format_integral_grouped_items(
392 author_part,
393 &wrapped,
394 params.suppress_author,
395 )
396 }
397 citum_schema::citation::CitationMode::NonIntegral => {
398 let repeated_item_delimiter = if author_item_delimiter.trim().is_empty() {
399 ", "
400 } else {
401 author_item_delimiter
402 };
403 let joined_items = item_parts.join(repeated_item_delimiter);
404 self.format_non_integral_grouped_items(
405 author_part,
406 author_item_delimiter,
407 &joined_items,
408 params.suppress_author,
409 )
410 }
411 });
412 }
413
414 if !author_part.is_empty() {
415 return Some(author_part.to_string());
416 }
417
418 if !item_parts.is_empty() {
419 return Some(item_parts.join(params.intra_delimiter));
420 }
421
422 None
423 }
424
425 fn format_integral_grouped_items(
426 &self,
427 author_part: &str,
428 wrapped_content: &str,
429 suppress_author: bool,
430 ) -> String {
431 if suppress_author {
432 wrapped_content.to_string()
433 } else {
434 format!("{author_part} {wrapped_content}")
435 }
436 }
437
438 fn format_non_integral_grouped_items(
439 &self,
440 author_part: &str,
441 author_item_delimiter: &str,
442 joined_items: &str,
443 suppress_author: bool,
444 ) -> String {
445 if suppress_author {
446 return joined_items.to_string();
447 }
448
449 if let Some(adjusted) =
450 self.adjust_grouped_author_quote_punctuation(author_part, author_item_delimiter)
451 {
452 return format!("{adjusted}{joined_items}");
453 }
454
455 format!("{author_part}{author_item_delimiter}{joined_items}")
456 }
457
458 fn adjust_grouped_author_quote_punctuation(
459 &self,
460 author_part: &str,
461 author_item_delimiter: &str,
462 ) -> Option<String> {
463 if !self.config.punctuation_in_quote
464 || !author_item_delimiter.starts_with(',')
465 || !(author_part.ends_with('"') || author_part.ends_with('\u{201D}'))
466 {
467 return None;
468 }
469
470 let is_curly = author_part.ends_with('\u{201D}');
471 let quote_char = if is_curly { '\u{201D}' } else { '"' };
472 #[allow(clippy::string_slice, reason = "quote found at end")]
473 let trimmed = &author_part[..author_part.len() - quote_char.len_utf8()];
474 #[allow(clippy::string_slice, reason = "delimiter checked to start with ','")]
475 Some(format!(
476 "{trimmed},{quote_char}{}",
477 &author_item_delimiter[1..]
478 ))
479 }
480
481 fn render_group_item_parts_with_format<F>(
482 &self,
483 fmt: &F,
484 group: &[&crate::reference::CitationItem],
485 params: &GroupRenderParams<'_>,
486 ) -> Result<(Vec<String>, Option<String>, Option<WrapConfig>), ProcessorError>
487 where
488 F: crate::render::format::OutputFormat<Output = String>,
489 {
490 let mut item_parts = Vec::new();
491 let mut group_delimiter: Option<String> = None;
492 let mut captured_year_wrap: Option<WrapConfig> = None;
499 let collapse_group = group.len() > 1
500 && matches!(params.mode, citum_schema::citation::CitationMode::Integral);
501 for (index, item) in group.iter().enumerate() {
502 let state = self.resolve_item_render_state(item, params.spec)?;
503 let (mut filtered_template, leading_affix, strip_item_delimiter) =
504 filter_author_from_template(&state.template);
505 if collapse_group {
506 if index == 0 {
507 captured_year_wrap = filtered_template
512 .first_mut()
513 .and_then(|c| c.rendering_mut().wrap.take());
514 } else {
515 if let Some(first) = filtered_template.first_mut() {
517 first.rendering_mut().wrap = None;
518 }
519 }
520 }
521 if group_delimiter.is_none() {
522 group_delimiter = leading_affix
523 .as_ref()
524 .filter(|value| !value.is_empty())
525 .cloned();
526 }
527 let item_delimiter = if strip_item_delimiter {
528 ""
529 } else {
530 params.intra_delimiter
531 };
532 if let Some(item_str) = self.render_group_item_from_template_with_format::<F>(
533 state.reference,
534 GroupItemRenderRequest {
535 item: state.item,
536 template: &filtered_template,
537 mode: params.mode,
538 suppress_author: params.suppress_author,
539 position: params.position,
540 note_start_text_case: params.note_start_text_case,
541 delimiter: item_delimiter,
542 },
543 ) && !item_str.is_empty()
544 {
545 let prefix = (index > 0).then_some(item.prefix.as_deref()).flatten();
546 item_parts.push(self.affix_content(fmt, item_str, prefix, item.suffix.as_deref()));
547 }
548 }
549 Ok((item_parts, group_delimiter, captured_year_wrap))
550 }
551
552 fn resolve_group_render_state<'b>(
553 &'b self,
554 group: &'b [&'b crate::reference::CitationItem],
555 spec: &'b citum_schema::CitationSpec,
556 ) -> Result<GroupRenderState<'b>, ProcessorError> {
557 #[allow(clippy::indexing_slicing, reason = "groups are non-empty")]
558 let first_item = group[0];
559 let first_ref = self
560 .bibliography
561 .get(&first_item.id)
562 .ok_or_else(|| ProcessorError::ReferenceNotFound(first_item.id.clone()))?;
563 let first_language = crate::values::effective_item_language(first_ref);
564 let default_template = spec
565 .resolve_template_for_language(first_language.as_deref())
566 .map(Cow::Owned);
567
568 let ref_type = first_ref.ref_type();
569 let first_template = resolve_type_variant(spec.type_variants.as_ref(), &ref_type)
570 .map(Cow::Borrowed)
571 .or(default_template);
572
573 Ok(GroupRenderState {
574 first_item,
575 first_ref,
576 template: first_template.unwrap_or(Cow::Borrowed(&[])),
577 })
578 }
579
580 fn resolve_item_render_state<'b>(
581 &'b self,
582 item: &'b crate::reference::CitationItem,
583 spec: &'b citum_schema::CitationSpec,
584 ) -> Result<ItemRenderState<'b>, ProcessorError> {
585 let reference = self
586 .bibliography
587 .get(&item.id)
588 .ok_or_else(|| ProcessorError::ReferenceNotFound(item.id.clone()))?;
589 let item_language = crate::values::effective_item_language(reference);
590 let default_template = spec
591 .resolve_template_for_language(item_language.as_deref())
592 .map(Cow::Owned);
593
594 let ref_type = reference.ref_type();
595 let item_template = resolve_type_variant(spec.type_variants.as_ref(), &ref_type)
596 .map(Cow::Borrowed)
597 .or(default_template);
598
599 Ok(ItemRenderState {
600 item,
601 reference,
602 template: item_template.unwrap_or(Cow::Borrowed(&[])),
603 })
604 }
605
606 fn try_render_integral_group_with_format<F>(
607 &self,
608 group: &[&crate::reference::CitationItem],
609 spec: &citum_schema::CitationSpec,
610 mode: &citum_schema::citation::CitationMode,
611 suppress_author: bool,
612 position: Option<&citum_schema::citation::Position>,
613 ) -> Result<Option<String>, ProcessorError>
614 where
615 F: crate::render::format::OutputFormat<Output = String>,
616 {
617 if !matches!(mode, citum_schema::citation::CitationMode::Integral)
618 || !self.has_explicit_integral_template()
619 {
620 return Ok(None);
621 }
622
623 self.render_integral_explicit_group::<F>(group, spec, mode, suppress_author, position)
624 }
625
626 fn requires_full_group_item_rendering(
637 &self,
638 mode: &citum_schema::citation::CitationMode,
639 reference: &Reference,
640 ) -> bool {
641 matches!(mode, citum_schema::citation::CitationMode::NonIntegral)
642 && matches!(
643 reference.ref_type().as_str(),
644 "legal-case" | "treaty" | "hearing" | "personal-communication"
645 )
646 }
647
648 pub(crate) fn render_author_for_grouping_with_format<F>(
650 &self,
651 reference: &Reference,
652 item: &crate::reference::CitationItem,
653 template: &[TemplateComponent],
654 mode: &citum_schema::citation::CitationMode,
655 suppress_author: bool,
656 position: Option<&citum_schema::citation::Position>,
657 ) -> String
658 where
659 F: crate::render::format::OutputFormat<Output = String>,
660 {
661 let is_note_processing = self.config.processing.as_ref().is_some_and(|processing| {
662 matches!(processing, citum_schema::options::Processing::Note)
663 });
664 if is_note_processing
665 && matches!(
666 position,
667 Some(
668 citum_schema::citation::Position::Ibid
669 | citum_schema::citation::Position::IbidWithLocator
670 )
671 )
672 && !template.iter().any(has_contributor_component)
673 {
674 return String::new();
675 }
676
677 let options = self.citation_render_options(mode.clone(), suppress_author, None, None);
678
679 if let Some(comp) = template.first().and_then(find_grouping_component) {
683 let base_hints = self
684 .hints
685 .get(reference.id().as_deref().unwrap_or_default())
686 .cloned()
687 .unwrap_or_default();
688 let hints = ProcHints {
690 position: position.cloned(),
691 integral_name_state: item.integral_name_state,
692 ..base_hints
693 };
694 if let Some(vals) = comp.values::<F>(reference, &hints, &options)
695 && !vals.value.is_empty()
696 {
697 return vals.value;
698 }
699 }
700
701 if let Some(authors) = reference.author() {
703 let names_vec = self.resolve_contributor_names(&authors);
704 F::default().text(&crate::values::format_contributors_short(
705 &names_vec, &options,
706 ))
707 } else {
708 String::new()
709 }
710 }
711
712 pub(crate) fn render_integral_anchor_with_format<F>(
714 &self,
715 items: &[crate::reference::CitationItem],
716 spec: &citum_schema::CitationSpec,
717 inter_delimiter: &str,
718 suppress_author: bool,
719 position: Option<&citum_schema::citation::Position>,
720 ) -> Result<String, ProcessorError>
721 where
722 F: crate::render::format::OutputFormat<Output = String>,
723 {
724 let groups = group_citation_items_by_author(self, items);
725
726 let mut rendered_groups = Vec::new();
727 let fmt = F::default();
728 for (_author_key, group) in groups {
729 #[allow(
730 clippy::indexing_slicing,
731 reason = "group is non-empty by construction"
732 )]
733 let first_item = group[0];
734 let reference = self
735 .bibliography
736 .get(&first_item.id)
737 .ok_or_else(|| ProcessorError::ReferenceNotFound(first_item.id.clone()))?;
738 let item_language = crate::values::effective_item_language(reference);
739 let template = spec.resolve_template_for_language(item_language.as_deref());
740 let effective_template = template.as_deref().unwrap_or(&[]);
741 let author_part = self.render_author_for_grouping_with_format::<F>(
742 reference,
743 first_item,
744 effective_template,
745 &citum_schema::citation::CitationMode::Integral,
746 suppress_author,
747 position,
748 );
749 if !author_part.is_empty() {
750 rendered_groups.push(author_part);
751 }
752 }
753
754 Ok(fmt.join(rendered_groups, inter_delimiter))
755 }
756
757 #[must_use]
759 pub fn get_or_assign_citation_number(&self, ref_id: &str) -> usize {
760 let mut numbers = self.citation_numbers.borrow_mut();
761 let next_num = numbers.len() + 1;
762 *numbers.entry(ref_id.to_string()).or_insert(next_num)
763 }
764
765 #[must_use]
767 pub fn process_bibliography_entry(
768 &self,
769 reference: &Reference,
770 entry_number: usize,
771 ) -> Option<ProcTemplate> {
772 self.process_bibliography_entry_with_format::<crate::render::plain::PlainText>(
773 reference,
774 entry_number,
775 )
776 }
777
778 #[must_use]
780 pub fn process_bibliography_entry_with_format<F>(
781 &self,
782 reference: &Reference,
783 entry_number: usize,
784 ) -> Option<ProcTemplate>
785 where
786 F: crate::render::format::OutputFormat<Output = String>,
787 {
788 let bib_spec = self.style.bibliography.as_ref()?;
789
790 let item_language = crate::values::effective_item_language(reference);
791 let default_template = bib_spec
792 .resolve_template_for_language(item_language.as_deref())
793 .map(Cow::Owned);
794
795 let ref_type = reference.ref_type();
796 let template = resolve_type_variant(bib_spec.type_variants.as_ref(), &ref_type)
797 .map(Cow::Borrowed)
798 .or(default_template)?;
799
800 let template = self.apply_anonymous_entry_bibliography_policy(reference, template)?;
801 let template = self.apply_article_journal_bibliography_policy(reference, template);
802
803 self.process_template_request_with_format::<F>(
804 reference,
805 TemplateRenderRequest {
806 template: template.as_ref(),
807 context: RenderContext::Bibliography,
808 mode: citum_schema::citation::CitationMode::NonIntegral,
809 suppress_author: false,
810 locator_raw: None,
811 citation_number: entry_number,
812 position: None,
813 note_start_text_case: None,
814 integral_name_state: None,
815 org_abbreviation_state: None,
816 first_reference_note_number: None,
817 },
818 )
819 }
820
821 #[must_use]
826 pub fn process_template_with_number(
827 &self,
828 reference: &Reference,
829 params: TemplateRenderParams<'_>,
830 ) -> Option<ProcTemplate> {
831 self.process_template_with_number_with_format::<crate::render::plain::PlainText>(
832 reference, params,
833 )
834 }
835
836 pub fn process_template_with_number_with_format<F>(
841 &self,
842 reference: &Reference,
843 params: TemplateRenderParams<'_>,
844 ) -> Option<ProcTemplate>
845 where
846 F: crate::render::format::OutputFormat<Output = String>,
847 {
848 self.process_template_request_with_format::<F>(
849 reference,
850 TemplateRenderRequest {
851 template: params.template,
852 context: params.context,
853 mode: params.mode,
854 suppress_author: params.suppress_author,
855 locator_raw: params.locator_raw,
856 citation_number: params.citation_number,
857 position: params.position.cloned(),
858 note_start_text_case: params.note_start_text_case,
859 integral_name_state: params.integral_name_state,
860 org_abbreviation_state: params.org_abbreviation_state,
861 first_reference_note_number: None,
862 },
863 )
864 }
865
866 #[must_use]
868 pub fn process_template_request_with_format<F>(
869 &self,
870 reference: &Reference,
871 request: TemplateRenderRequest<'_>,
872 ) -> Option<ProcTemplate>
873 where
874 F: crate::render::format::OutputFormat<Output = String>,
875 {
876 let TemplateRenderRequest {
877 template,
878 context,
879 mode,
880 suppress_author,
881 locator_raw,
882 citation_number,
883 position,
884 note_start_text_case,
885 integral_name_state,
886 org_abbreviation_state,
887 first_reference_note_number,
888 } = request;
889 let ref_type = reference.ref_type();
890 let options = RenderOptions {
891 config: self.config,
892 bibliography_config: self.bibliography_config.clone(),
893 locale: self.locale,
894 context,
895 mode,
896 suppress_author,
897 locator_raw,
898 ref_type: Some(ref_type.clone()),
899 show_semantics: self.show_semantics,
900 current_template_index: None,
901 abbreviation_map: self.abbreviation_map,
902 };
903 let effective_first_ref_note = if template_uses_first_ref_note_number(template) {
908 first_reference_note_number
909 } else {
910 None
911 };
912 let hint = self.build_template_render_hint(HintInputs {
913 reference,
914 context: options.context,
915 citation_number,
916 position,
917 integral_name_state,
918 org_abbreviation_state,
919 first_reference_note_number: effective_first_ref_note,
920 });
921 let mut components =
922 self.render_template_components::<F>(reference, &ref_type, &options, &hint, template);
923
924 self.apply_sentence_initial_context::<F>(&mut components, context, note_start_text_case);
925
926 (!components.is_empty()).then_some(components)
927 }
928
929 fn render_template_components<F>(
933 &self,
934 reference: &Reference,
935 ref_type: &str,
936 options: &RenderOptions<'_>,
937 hint: &ProcHints,
938 template: &[TemplateComponent],
939 ) -> Vec<ProcTemplateComponent>
940 where
941 F: crate::render::format::OutputFormat<Output = String>,
942 {
943 let mut tracker = TemplateComponentTracker::default();
944 let mut components = Vec::with_capacity(template.len());
945 let mut component_options = options.clone();
946 for (template_index, component) in template.iter().enumerate() {
947 component_options.current_template_index =
948 self.inject_ast_indices.then_some(template_index);
949 let ctx = TemplateRenderContext {
950 reference,
951 ref_type,
952 options: &component_options,
953 hint,
954 template_index,
955 };
956 if let Some(component) =
957 self.render_template_component_with_format::<F>(&ctx, component, &mut tracker)
958 {
959 components.push(component);
960 }
961 }
962 components
963 }
964
965 fn build_template_render_hint(&self, inputs: HintInputs<'_>) -> ProcHints {
966 let HintInputs {
967 reference,
968 context,
969 citation_number,
970 position,
971 integral_name_state,
972 org_abbreviation_state,
973 first_reference_note_number,
974 } = inputs;
975 let default_hint = ProcHints::default();
976 let base_hint = self
977 .hints
978 .get(reference.id().as_deref().unwrap_or_default())
979 .unwrap_or(&default_hint);
980 let is_subsequent = matches!(position, Some(citum_schema::citation::Position::Subsequent));
981 ProcHints {
982 citation_number: (citation_number > 0).then_some(citation_number),
983 citation_sub_label: if context == RenderContext::Citation {
984 reference
985 .id()
986 .as_deref()
987 .and_then(|id| self.citation_sub_label_for_ref(id))
988 } else {
989 None
990 },
991 position,
992 integral_name_state,
993 org_abbreviation_state,
994 first_reference_note_number: if is_subsequent {
995 first_reference_note_number
996 } else {
997 None
998 },
999 suppress_disambiguation_title: is_subsequent && first_reference_note_number.is_some(),
1000 ..base_hint.clone()
1001 }
1002 }
1003
1004 fn render_template_component_with_format<F>(
1005 &self,
1006 ctx: &TemplateRenderContext<'_>,
1007 component: &TemplateComponent,
1008 tracker: &mut TemplateComponentTracker,
1009 ) -> Option<ProcTemplateComponent>
1010 where
1011 F: crate::render::format::OutputFormat<Output = String>,
1012 {
1013 if let TemplateComponent::Group(group) = component {
1014 return self.render_group_component_with_format::<F>(ctx, group, tracker);
1015 }
1016
1017 let resolved_component = component;
1018 let var_key = get_variable_key(resolved_component);
1019 if tracker.should_skip(var_key.as_deref()) {
1020 return None;
1021 }
1022
1023 let mut values = resolved_component.values::<F>(ctx.reference, ctx.hint, ctx.options)?;
1024 if values.value.trim().is_empty() {
1028 return None;
1029 }
1030 self.apply_issued_no_date_fallback(
1031 ctx.reference,
1032 ctx.options,
1033 resolved_component,
1034 &mut values,
1035 );
1036 self.apply_entry_link_fallback(ctx.reference, ctx.options, &mut values);
1037
1038 let item_language =
1039 crate::values::effective_component_language(ctx.reference, resolved_component);
1040 tracker.mark_rendered(var_key, values.substituted_key.as_deref());
1041
1042 Some(ProcTemplateComponent {
1043 template_component: resolved_component.clone(),
1044 template_index: self.inject_ast_indices.then_some(ctx.template_index),
1045 value: values.value,
1046 prefix: values.prefix,
1047 suffix: values.suffix,
1048 url: values.url,
1049 ref_type: Some(ctx.ref_type.to_string()),
1050 config: Some(ctx.options.config.clone()),
1051 bibliography_config: ctx.options.bibliography_config.clone(),
1052 item_language,
1053 sentence_initial: false,
1054 pre_formatted: values.pre_formatted,
1055 })
1056 }
1057
1058 fn render_group_component_with_format<F>(
1059 &self,
1060 ctx: &TemplateRenderContext<'_>,
1061 group: &citum_schema::template::TemplateGroup,
1062 tracker: &mut TemplateComponentTracker,
1063 ) -> Option<ProcTemplateComponent>
1064 where
1065 F: crate::render::format::OutputFormat<Output = String>,
1066 {
1067 let fmt = F::default();
1068 let values = self.render_group_child_values(&fmt, ctx, group, tracker)?;
1069 let delimiter = group
1070 .delimiter
1071 .as_ref()
1072 .unwrap_or(&citum_schema::template::DelimiterPunctuation::Comma)
1073 .to_string_with_space();
1074 let group_component = TemplateComponent::Group(group.clone());
1075 Some(ProcTemplateComponent {
1076 template_component: group_component.clone(),
1077 template_index: self.inject_ast_indices.then_some(ctx.template_index),
1078 value: fmt.join(values, &delimiter),
1079 prefix: None,
1080 suffix: None,
1081 url: None,
1082 ref_type: Some(ctx.ref_type.to_string()),
1083 config: Some(ctx.options.config.clone()),
1084 bibliography_config: ctx.options.bibliography_config.clone(),
1085 item_language: crate::values::effective_component_language(
1086 ctx.reference,
1087 &group_component,
1088 ),
1089 sentence_initial: false,
1090 pre_formatted: true,
1091 })
1092 }
1093
1094 fn render_group_child_values<F>(
1100 &self,
1101 fmt: &F,
1102 ctx: &TemplateRenderContext<'_>,
1103 group: &citum_schema::template::TemplateGroup,
1104 tracker: &mut TemplateComponentTracker,
1105 ) -> Option<Vec<String>>
1106 where
1107 F: crate::render::format::OutputFormat<Output = String>,
1108 {
1109 let mut has_meaningful_content = false;
1110 let mut values = Vec::with_capacity(group.group.len());
1111
1112 for item in &group.group {
1113 let Some(rendered) =
1114 self.render_template_component_with_format::<F>(ctx, item, tracker)
1115 else {
1116 continue;
1117 };
1118 let rendered_str = crate::render::render_component_with_format_and_renderer::<F>(
1119 &rendered,
1120 fmt,
1121 ctx.options.show_semantics,
1122 );
1123 if rendered_str.trim().is_empty() {
1124 continue;
1125 }
1126 if !is_term_only_component(item) {
1127 has_meaningful_content = true;
1128 }
1129 values.push(rendered_str);
1130 }
1131
1132 (has_meaningful_content && !values.is_empty()).then_some(values)
1133 }
1134
1135 fn apply_issued_no_date_fallback(
1136 &self,
1137 reference: &Reference,
1138 options: &RenderOptions<'_>,
1139 component: &TemplateComponent,
1140 values: &mut crate::values::ProcValues<String>,
1141 ) {
1142 if !matches!(
1143 component,
1144 TemplateComponent::Date(citum_schema::template::TemplateDate {
1145 date: citum_schema::template::DateVariable::Issued,
1146 ..
1147 })
1148 ) || reference.csl_issued_date().is_some()
1149 || self.preferred_no_date_term_form() != citum_schema::locale::TermForm::Long
1150 {
1151 return;
1152 }
1153
1154 if let Some(long) = options.locale.resolved_general_term(
1155 &citum_schema::locale::GeneralTerm::NoDate,
1156 &citum_schema::locale::TermForm::Long,
1157 None,
1158 ) {
1159 values.value = long;
1160 }
1161 }
1162
1163 fn apply_entry_link_fallback(
1164 &self,
1165 reference: &Reference,
1166 options: &RenderOptions<'_>,
1167 values: &mut crate::values::ProcValues<String>,
1168 ) {
1169 if values.url.is_some() {
1170 return;
1171 }
1172
1173 let Some(links) = &options.config.links else {
1174 return;
1175 };
1176 use citum_schema::options::LinkAnchor;
1177 if matches!(links.anchor, Some(LinkAnchor::Entry)) {
1178 values.url = crate::values::resolve_url(links, reference);
1179 }
1180 }
1181
1182 pub fn apply_author_substitution(&self, proc: &mut ProcTemplate, substitute: &str) {
1184 self.apply_author_substitution_with_format::<crate::render::plain::PlainText>(
1185 proc, substitute,
1186 );
1187 }
1188
1189 pub fn apply_author_substitution_with_format<F>(
1191 &self,
1192 proc: &mut ProcTemplate,
1193 substitute: &str,
1194 ) where
1195 F: crate::render::format::OutputFormat<Output = String>,
1196 {
1197 if let Some(component) = proc
1198 .iter_mut()
1199 .find(|c| matches!(c.template_component, TemplateComponent::Contributor(_)))
1200 {
1201 let fmt = F::default();
1202 component.value = fmt.text(substitute);
1203 }
1204 }
1205
1206 fn preferred_no_date_term_form(&self) -> citum_schema::locale::TermForm {
1207 match self
1208 .style
1209 .info
1210 .source
1211 .as_ref()
1212 .map(|source| source.csl_id.as_str())
1213 {
1214 Some("http://www.zotero.org/styles/harvard-cite-them-right") => {
1215 citum_schema::locale::TermForm::Long
1216 }
1217 _ => citum_schema::locale::TermForm::Short,
1218 }
1219 }
1220
1221 fn render_group_item_from_template_with_format<F>(
1222 &self,
1223 reference: &Reference,
1224 item_request: GroupItemRenderRequest<'_>,
1225 ) -> Option<String>
1226 where
1227 F: crate::render::format::OutputFormat<Output = String>,
1228 {
1229 let request = self.citation_render_request(
1230 item_request.item,
1231 item_request.template,
1232 item_request.mode,
1233 item_request.suppress_author,
1234 item_request.position,
1235 item_request.note_start_text_case,
1236 );
1237 self.render_item_from_template_with_format::<F>(reference, request, item_request.delimiter)
1238 }
1239}
1240
1241pub(super) fn template_uses_first_ref_note_number(template: &[TemplateComponent]) -> bool {
1248 template.iter().any(|c| match c {
1249 TemplateComponent::Number(n) => {
1250 n.number == citum_schema::template::NumberVariable::FirstReferenceNoteNumber
1251 }
1252 TemplateComponent::Group(g) => template_uses_first_ref_note_number(&g.group),
1253 _ => false,
1254 })
1255}
1256
1257pub(super) fn filter_author_from_template(
1258 template: &[TemplateComponent],
1259) -> (Vec<TemplateComponent>, Option<String>, bool) {
1260 let mut filtered: Vec<TemplateComponent> =
1261 template.iter().filter_map(strip_author_component).collect();
1262 let stripped_leading_affix = filtered.first().and_then(leading_group_affix);
1263 let leading_affix = stripped_leading_affix.clone().or_else(|| {
1264 filtered
1265 .first()
1266 .and_then(|_| template.first().and_then(author_group_delimiter_affix))
1267 });
1268 if let Some(first) = filtered.first_mut() {
1269 strip_leading_group_affixes(first);
1270 }
1271 (filtered, leading_affix, stripped_leading_affix.is_some())
1272}
1273
1274fn author_group_delimiter_affix(component: &TemplateComponent) -> Option<String> {
1275 let TemplateComponent::Group(group) = component else {
1276 return None;
1277 };
1278 group
1279 .group
1280 .first()
1281 .is_some_and(component_starts_with_author)
1282 .then_some(group.delimiter.as_ref())
1283 .flatten()
1284 .map(citum_schema::template::DelimiterPunctuation::to_string_with_space)
1285 .filter(|delimiter| !delimiter.is_empty())
1286}
1287
1288fn component_starts_with_author(component: &TemplateComponent) -> bool {
1289 match component {
1290 TemplateComponent::Contributor(contributor) => {
1291 contributor.contributor == citum_schema::template::ContributorRole::Author
1292 }
1293 TemplateComponent::Group(group) => group
1294 .group
1295 .first()
1296 .is_some_and(component_starts_with_author),
1297 _ => false,
1298 }
1299}