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
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 let Some(citation) = self.try_render_integral_group_with_format::<F>(
267 group,
268 params.spec,
269 params.mode,
270 params.suppress_author,
271 params.position,
272 )? {
273 return Ok(vec![citation]);
274 }
275
276 if self.requires_full_group_item_rendering(params.mode, state.first_ref) {
277 return self.render_special_type_items::<F>(group, params);
278 }
279
280 Ok(self
281 .render_fallback_grouped_citation_with_format::<F>(
282 group,
283 state.first_ref,
284 state.first_item,
285 &state.template,
286 params,
287 )?
288 .into_iter()
289 .collect())
290 }
291
292 fn render_fallback_grouped_citation_with_format<F>(
293 &self,
294 group: &[&crate::reference::CitationItem],
295 first_ref: &Reference,
296 first_item: &crate::reference::CitationItem,
297 template: &[TemplateComponent],
298 params: &GroupRenderParams<'_>,
299 ) -> Result<Option<String>, ProcessorError>
300 where
301 F: crate::render::format::OutputFormat<Output = String>,
302 {
303 let fmt = F::default();
304 let author_part = self.render_author_for_grouping_with_format::<F>(
305 first_ref,
306 first_item,
307 template,
308 params.mode,
309 params.suppress_author,
310 params.position,
311 );
312 let (item_parts, group_delimiter) =
313 self.render_group_item_parts_with_format::<F>(&fmt, group, params)?;
314 let Some(content) = self.build_grouped_citation_content(
315 &author_part,
316 &item_parts,
317 params,
318 group_delimiter.as_deref(),
319 ) else {
320 return Ok(None);
321 };
322 let group_ids = group.iter().map(|item| item.id.clone()).collect();
323 let prefix = first_item.prefix.as_deref().unwrap_or("");
324 let suffix = if item_parts.is_empty() {
327 first_item.suffix.as_deref()
328 } else {
329 None
330 };
331
332 Ok(Some(fmt.citation(
333 group_ids,
334 self.affix_content(&fmt, content, Some(prefix), suffix),
335 )))
336 }
337
338 fn build_grouped_citation_content(
339 &self,
340 author_part: &str,
341 item_parts: &[String],
342 params: &GroupRenderParams<'_>,
343 group_delimiter: Option<&str>,
344 ) -> Option<String> {
345 if !author_part.is_empty() && !item_parts.is_empty() {
346 let author_item_delimiter = group_delimiter.unwrap_or(params.intra_delimiter);
347 let joined_items = match params.mode {
348 citum_schema::citation::CitationMode::Integral => {
349 self.join_integral_group_item_parts(item_parts, author_item_delimiter)
350 }
351 citum_schema::citation::CitationMode::NonIntegral => {
352 let repeated_item_delimiter = if author_item_delimiter.trim().is_empty() {
353 ", "
354 } else {
355 author_item_delimiter
356 };
357 item_parts.join(repeated_item_delimiter)
358 }
359 };
360 return Some(match params.mode {
361 citum_schema::citation::CitationMode::Integral => self
362 .format_integral_grouped_items(
363 author_part,
364 &joined_items,
365 params.suppress_author,
366 ),
367 citum_schema::citation::CitationMode::NonIntegral => self
368 .format_non_integral_grouped_items(
369 author_part,
370 author_item_delimiter,
371 &joined_items,
372 params.suppress_author,
373 ),
374 });
375 }
376
377 if !author_part.is_empty() {
378 return Some(author_part.to_string());
379 }
380
381 if !item_parts.is_empty() {
382 return Some(item_parts.join(params.intra_delimiter));
383 }
384
385 None
386 }
387
388 fn format_integral_grouped_items(
389 &self,
390 author_part: &str,
391 joined_items: &str,
392 suppress_author: bool,
393 ) -> String {
394 if suppress_author {
395 format!("({joined_items})")
396 } else {
397 format!("{author_part} ({joined_items})")
398 }
399 }
400
401 fn format_non_integral_grouped_items(
402 &self,
403 author_part: &str,
404 author_item_delimiter: &str,
405 joined_items: &str,
406 suppress_author: bool,
407 ) -> String {
408 if suppress_author {
409 return joined_items.to_string();
410 }
411
412 if let Some(adjusted) =
413 self.adjust_grouped_author_quote_punctuation(author_part, author_item_delimiter)
414 {
415 return format!("{adjusted}{joined_items}");
416 }
417
418 format!("{author_part}{author_item_delimiter}{joined_items}")
419 }
420
421 fn adjust_grouped_author_quote_punctuation(
422 &self,
423 author_part: &str,
424 author_item_delimiter: &str,
425 ) -> Option<String> {
426 if !self.config.punctuation_in_quote
427 || !author_item_delimiter.starts_with(',')
428 || !(author_part.ends_with('"') || author_part.ends_with('\u{201D}'))
429 {
430 return None;
431 }
432
433 let is_curly = author_part.ends_with('\u{201D}');
434 let quote_char = if is_curly { '\u{201D}' } else { '"' };
435 #[allow(clippy::string_slice, reason = "quote found at end")]
436 let trimmed = &author_part[..author_part.len() - quote_char.len_utf8()];
437 #[allow(clippy::string_slice, reason = "delimiter checked to start with ','")]
438 Some(format!(
439 "{trimmed},{quote_char}{}",
440 &author_item_delimiter[1..]
441 ))
442 }
443
444 fn render_group_item_parts_with_format<F>(
445 &self,
446 fmt: &F,
447 group: &[&crate::reference::CitationItem],
448 params: &GroupRenderParams<'_>,
449 ) -> Result<(Vec<String>, Option<String>), ProcessorError>
450 where
451 F: crate::render::format::OutputFormat<Output = String>,
452 {
453 let mut item_parts = Vec::new();
454 let mut group_delimiter: Option<String> = None;
455 for (index, item) in group.iter().enumerate() {
456 let state = self.resolve_item_render_state(item, params.spec)?;
457 let (filtered_template, leading_affix, strip_item_delimiter) =
458 filter_author_from_template(&state.template);
459 if group_delimiter.is_none() {
460 group_delimiter = leading_affix
461 .as_ref()
462 .filter(|value| !value.is_empty())
463 .cloned();
464 }
465 let item_delimiter = if strip_item_delimiter {
466 ""
467 } else {
468 params.intra_delimiter
469 };
470 if let Some(item_str) = self.render_group_item_from_template_with_format::<F>(
471 state.reference,
472 GroupItemRenderRequest {
473 item: state.item,
474 template: &filtered_template,
475 mode: params.mode,
476 suppress_author: params.suppress_author,
477 position: params.position,
478 note_start_text_case: params.note_start_text_case,
479 delimiter: item_delimiter,
480 },
481 ) && !item_str.is_empty()
482 {
483 let prefix = (index > 0).then_some(item.prefix.as_deref()).flatten();
484 item_parts.push(self.affix_content(fmt, item_str, prefix, item.suffix.as_deref()));
485 }
486 }
487 Ok((item_parts, group_delimiter))
488 }
489
490 fn resolve_group_render_state<'b>(
491 &'b self,
492 group: &'b [&'b crate::reference::CitationItem],
493 spec: &'b citum_schema::CitationSpec,
494 ) -> Result<GroupRenderState<'b>, ProcessorError> {
495 #[allow(clippy::indexing_slicing, reason = "groups are non-empty")]
496 let first_item = group[0];
497 let first_ref = self
498 .bibliography
499 .get(&first_item.id)
500 .ok_or_else(|| ProcessorError::ReferenceNotFound(first_item.id.clone()))?;
501 let first_language = crate::values::effective_item_language(first_ref);
502 let default_template = spec
503 .resolve_template_for_language(first_language.as_deref())
504 .map(Cow::Owned);
505
506 let ref_type = first_ref.ref_type();
507 let first_template = resolve_type_variant(spec.type_variants.as_ref(), &ref_type)
508 .map(Cow::Borrowed)
509 .or(default_template);
510
511 Ok(GroupRenderState {
512 first_item,
513 first_ref,
514 template: first_template.unwrap_or(Cow::Borrowed(&[])),
515 })
516 }
517
518 fn resolve_item_render_state<'b>(
519 &'b self,
520 item: &'b crate::reference::CitationItem,
521 spec: &'b citum_schema::CitationSpec,
522 ) -> Result<ItemRenderState<'b>, ProcessorError> {
523 let reference = self
524 .bibliography
525 .get(&item.id)
526 .ok_or_else(|| ProcessorError::ReferenceNotFound(item.id.clone()))?;
527 let item_language = crate::values::effective_item_language(reference);
528 let default_template = spec
529 .resolve_template_for_language(item_language.as_deref())
530 .map(Cow::Owned);
531
532 let ref_type = reference.ref_type();
533 let item_template = resolve_type_variant(spec.type_variants.as_ref(), &ref_type)
534 .map(Cow::Borrowed)
535 .or(default_template);
536
537 Ok(ItemRenderState {
538 item,
539 reference,
540 template: item_template.unwrap_or(Cow::Borrowed(&[])),
541 })
542 }
543
544 fn try_render_integral_group_with_format<F>(
545 &self,
546 group: &[&crate::reference::CitationItem],
547 spec: &citum_schema::CitationSpec,
548 mode: &citum_schema::citation::CitationMode,
549 suppress_author: bool,
550 position: Option<&citum_schema::citation::Position>,
551 ) -> Result<Option<String>, ProcessorError>
552 where
553 F: crate::render::format::OutputFormat<Output = String>,
554 {
555 if !matches!(mode, citum_schema::citation::CitationMode::Integral)
556 || !self.has_explicit_integral_template()
557 {
558 return Ok(None);
559 }
560
561 self.render_integral_explicit_group::<F>(group, spec, mode, suppress_author, position)
562 }
563
564 fn requires_full_group_item_rendering(
575 &self,
576 mode: &citum_schema::citation::CitationMode,
577 reference: &Reference,
578 ) -> bool {
579 matches!(mode, citum_schema::citation::CitationMode::NonIntegral)
580 && matches!(
581 reference.ref_type().as_str(),
582 "legal-case" | "treaty" | "hearing" | "personal-communication"
583 )
584 }
585
586 pub(crate) fn render_author_for_grouping_with_format<F>(
588 &self,
589 reference: &Reference,
590 item: &crate::reference::CitationItem,
591 template: &[TemplateComponent],
592 mode: &citum_schema::citation::CitationMode,
593 suppress_author: bool,
594 position: Option<&citum_schema::citation::Position>,
595 ) -> String
596 where
597 F: crate::render::format::OutputFormat<Output = String>,
598 {
599 let is_note_processing = self.config.processing.as_ref().is_some_and(|processing| {
600 matches!(processing, citum_schema::options::Processing::Note)
601 });
602 if is_note_processing
603 && matches!(
604 position,
605 Some(
606 citum_schema::citation::Position::Ibid
607 | citum_schema::citation::Position::IbidWithLocator
608 )
609 )
610 && !template.iter().any(has_contributor_component)
611 {
612 return String::new();
613 }
614
615 let options = self.citation_render_options(mode.clone(), suppress_author, None, None);
616
617 if let Some(comp) = template.first().and_then(find_grouping_component) {
621 let base_hints = self
622 .hints
623 .get(reference.id().as_deref().unwrap_or_default())
624 .cloned()
625 .unwrap_or_default();
626 let hints = ProcHints {
628 position: position.cloned(),
629 integral_name_state: item.integral_name_state,
630 ..base_hints
631 };
632 if let Some(vals) = comp.values::<F>(reference, &hints, &options)
633 && !vals.value.is_empty()
634 {
635 return vals.value;
636 }
637 }
638
639 if let Some(authors) = reference.author() {
641 let names_vec = self.resolve_contributor_names(&authors);
642 F::default().text(&crate::values::format_contributors_short(
643 &names_vec, &options,
644 ))
645 } else {
646 String::new()
647 }
648 }
649
650 pub(crate) fn render_integral_anchor_with_format<F>(
652 &self,
653 items: &[crate::reference::CitationItem],
654 spec: &citum_schema::CitationSpec,
655 inter_delimiter: &str,
656 suppress_author: bool,
657 position: Option<&citum_schema::citation::Position>,
658 ) -> Result<String, ProcessorError>
659 where
660 F: crate::render::format::OutputFormat<Output = String>,
661 {
662 let groups = group_citation_items_by_author(self, items);
663
664 let mut rendered_groups = Vec::new();
665 let fmt = F::default();
666 for (_author_key, group) in groups {
667 #[allow(
668 clippy::indexing_slicing,
669 reason = "group is non-empty by construction"
670 )]
671 let first_item = group[0];
672 let reference = self
673 .bibliography
674 .get(&first_item.id)
675 .ok_or_else(|| ProcessorError::ReferenceNotFound(first_item.id.clone()))?;
676 let item_language = crate::values::effective_item_language(reference);
677 let template = spec.resolve_template_for_language(item_language.as_deref());
678 let effective_template = template.as_deref().unwrap_or(&[]);
679 let author_part = self.render_author_for_grouping_with_format::<F>(
680 reference,
681 first_item,
682 effective_template,
683 &citum_schema::citation::CitationMode::Integral,
684 suppress_author,
685 position,
686 );
687 if !author_part.is_empty() {
688 rendered_groups.push(author_part);
689 }
690 }
691
692 Ok(fmt.join(rendered_groups, inter_delimiter))
693 }
694
695 #[must_use]
697 pub fn get_or_assign_citation_number(&self, ref_id: &str) -> usize {
698 let mut numbers = self.citation_numbers.borrow_mut();
699 let next_num = numbers.len() + 1;
700 *numbers.entry(ref_id.to_string()).or_insert(next_num)
701 }
702
703 #[must_use]
705 pub fn process_bibliography_entry(
706 &self,
707 reference: &Reference,
708 entry_number: usize,
709 ) -> Option<ProcTemplate> {
710 self.process_bibliography_entry_with_format::<crate::render::plain::PlainText>(
711 reference,
712 entry_number,
713 )
714 }
715
716 #[must_use]
718 pub fn process_bibliography_entry_with_format<F>(
719 &self,
720 reference: &Reference,
721 entry_number: usize,
722 ) -> Option<ProcTemplate>
723 where
724 F: crate::render::format::OutputFormat<Output = String>,
725 {
726 let bib_spec = self.style.bibliography.as_ref()?;
727
728 let item_language = crate::values::effective_item_language(reference);
729 let default_template = bib_spec
730 .resolve_template_for_language(item_language.as_deref())
731 .map(Cow::Owned);
732
733 let ref_type = reference.ref_type();
734 let template = resolve_type_variant(bib_spec.type_variants.as_ref(), &ref_type)
735 .map(Cow::Borrowed)
736 .or(default_template)?;
737
738 let template = self.apply_anonymous_entry_bibliography_policy(reference, template)?;
739 let template = self.apply_article_journal_bibliography_policy(reference, template);
740
741 self.process_template_request_with_format::<F>(
742 reference,
743 TemplateRenderRequest {
744 template: template.as_ref(),
745 context: RenderContext::Bibliography,
746 mode: citum_schema::citation::CitationMode::NonIntegral,
747 suppress_author: false,
748 locator_raw: None,
749 citation_number: entry_number,
750 position: None,
751 note_start_text_case: None,
752 integral_name_state: None,
753 org_abbreviation_state: None,
754 first_reference_note_number: None,
755 },
756 )
757 }
758
759 #[must_use]
764 pub fn process_template_with_number(
765 &self,
766 reference: &Reference,
767 params: TemplateRenderParams<'_>,
768 ) -> Option<ProcTemplate> {
769 self.process_template_with_number_with_format::<crate::render::plain::PlainText>(
770 reference, params,
771 )
772 }
773
774 pub fn process_template_with_number_with_format<F>(
779 &self,
780 reference: &Reference,
781 params: TemplateRenderParams<'_>,
782 ) -> Option<ProcTemplate>
783 where
784 F: crate::render::format::OutputFormat<Output = String>,
785 {
786 self.process_template_request_with_format::<F>(
787 reference,
788 TemplateRenderRequest {
789 template: params.template,
790 context: params.context,
791 mode: params.mode,
792 suppress_author: params.suppress_author,
793 locator_raw: params.locator_raw,
794 citation_number: params.citation_number,
795 position: params.position.cloned(),
796 note_start_text_case: params.note_start_text_case,
797 integral_name_state: params.integral_name_state,
798 org_abbreviation_state: params.org_abbreviation_state,
799 first_reference_note_number: None,
800 },
801 )
802 }
803
804 #[must_use]
806 pub fn process_template_request_with_format<F>(
807 &self,
808 reference: &Reference,
809 request: TemplateRenderRequest<'_>,
810 ) -> Option<ProcTemplate>
811 where
812 F: crate::render::format::OutputFormat<Output = String>,
813 {
814 let TemplateRenderRequest {
815 template,
816 context,
817 mode,
818 suppress_author,
819 locator_raw,
820 citation_number,
821 position,
822 note_start_text_case,
823 integral_name_state,
824 org_abbreviation_state,
825 first_reference_note_number,
826 } = request;
827 let ref_type = reference.ref_type();
828 let options = RenderOptions {
829 config: self.config,
830 bibliography_config: self.bibliography_config.clone(),
831 locale: self.locale,
832 context,
833 mode,
834 suppress_author,
835 locator_raw,
836 ref_type: Some(ref_type.clone()),
837 show_semantics: self.show_semantics,
838 current_template_index: None,
839 abbreviation_map: self.abbreviation_map,
840 };
841 let effective_first_ref_note = if template_uses_first_ref_note_number(template) {
846 first_reference_note_number
847 } else {
848 None
849 };
850 let hint = self.build_template_render_hint(HintInputs {
851 reference,
852 context: options.context,
853 citation_number,
854 position,
855 integral_name_state,
856 org_abbreviation_state,
857 first_reference_note_number: effective_first_ref_note,
858 });
859 let mut components =
860 self.render_template_components::<F>(reference, &ref_type, &options, &hint, template);
861
862 self.apply_sentence_initial_context::<F>(&mut components, context, note_start_text_case);
863
864 (!components.is_empty()).then_some(components)
865 }
866
867 fn render_template_components<F>(
871 &self,
872 reference: &Reference,
873 ref_type: &str,
874 options: &RenderOptions<'_>,
875 hint: &ProcHints,
876 template: &[TemplateComponent],
877 ) -> Vec<ProcTemplateComponent>
878 where
879 F: crate::render::format::OutputFormat<Output = String>,
880 {
881 let mut tracker = TemplateComponentTracker::default();
882 let mut components = Vec::with_capacity(template.len());
883 let mut component_options = options.clone();
884 for (template_index, component) in template.iter().enumerate() {
885 component_options.current_template_index =
886 self.inject_ast_indices.then_some(template_index);
887 let ctx = TemplateRenderContext {
888 reference,
889 ref_type,
890 options: &component_options,
891 hint,
892 template_index,
893 };
894 if let Some(component) =
895 self.render_template_component_with_format::<F>(&ctx, component, &mut tracker)
896 {
897 components.push(component);
898 }
899 }
900 components
901 }
902
903 fn build_template_render_hint(&self, inputs: HintInputs<'_>) -> ProcHints {
904 let HintInputs {
905 reference,
906 context,
907 citation_number,
908 position,
909 integral_name_state,
910 org_abbreviation_state,
911 first_reference_note_number,
912 } = inputs;
913 let default_hint = ProcHints::default();
914 let base_hint = self
915 .hints
916 .get(reference.id().as_deref().unwrap_or_default())
917 .unwrap_or(&default_hint);
918 let is_subsequent = matches!(position, Some(citum_schema::citation::Position::Subsequent));
919 ProcHints {
920 citation_number: (citation_number > 0).then_some(citation_number),
921 citation_sub_label: if context == RenderContext::Citation {
922 reference
923 .id()
924 .as_deref()
925 .and_then(|id| self.citation_sub_label_for_ref(id))
926 } else {
927 None
928 },
929 position,
930 integral_name_state,
931 org_abbreviation_state,
932 first_reference_note_number: if is_subsequent {
933 first_reference_note_number
934 } else {
935 None
936 },
937 suppress_disambiguation_title: is_subsequent && first_reference_note_number.is_some(),
938 ..base_hint.clone()
939 }
940 }
941
942 fn render_template_component_with_format<F>(
943 &self,
944 ctx: &TemplateRenderContext<'_>,
945 component: &TemplateComponent,
946 tracker: &mut TemplateComponentTracker,
947 ) -> Option<ProcTemplateComponent>
948 where
949 F: crate::render::format::OutputFormat<Output = String>,
950 {
951 if let TemplateComponent::Group(group) = component {
952 return self.render_group_component_with_format::<F>(ctx, group, tracker);
953 }
954
955 let resolved_component = component;
956 let var_key = get_variable_key(resolved_component);
957 if tracker.should_skip(var_key.as_deref()) {
958 return None;
959 }
960
961 let mut values = resolved_component.values::<F>(ctx.reference, ctx.hint, ctx.options)?;
962 if values.value.trim().is_empty() {
966 return None;
967 }
968 self.apply_issued_no_date_fallback(
969 ctx.reference,
970 ctx.options,
971 resolved_component,
972 &mut values,
973 );
974 self.apply_entry_link_fallback(ctx.reference, ctx.options, &mut values);
975
976 let item_language =
977 crate::values::effective_component_language(ctx.reference, resolved_component);
978 tracker.mark_rendered(var_key, values.substituted_key.as_deref());
979
980 Some(ProcTemplateComponent {
981 template_component: resolved_component.clone(),
982 template_index: self.inject_ast_indices.then_some(ctx.template_index),
983 value: values.value,
984 prefix: values.prefix,
985 suffix: values.suffix,
986 url: values.url,
987 ref_type: Some(ctx.ref_type.to_string()),
988 config: Some(ctx.options.config.clone()),
989 bibliography_config: ctx.options.bibliography_config.clone(),
990 item_language,
991 sentence_initial: false,
992 pre_formatted: values.pre_formatted,
993 })
994 }
995
996 fn render_group_component_with_format<F>(
997 &self,
998 ctx: &TemplateRenderContext<'_>,
999 group: &citum_schema::template::TemplateGroup,
1000 tracker: &mut TemplateComponentTracker,
1001 ) -> Option<ProcTemplateComponent>
1002 where
1003 F: crate::render::format::OutputFormat<Output = String>,
1004 {
1005 let fmt = F::default();
1006 let values = self.render_group_child_values(&fmt, ctx, group, tracker)?;
1007 let delimiter = group
1008 .delimiter
1009 .as_ref()
1010 .unwrap_or(&citum_schema::template::DelimiterPunctuation::Comma)
1011 .to_string_with_space();
1012 let group_component = TemplateComponent::Group(group.clone());
1013 Some(ProcTemplateComponent {
1014 template_component: group_component.clone(),
1015 template_index: self.inject_ast_indices.then_some(ctx.template_index),
1016 value: fmt.join(values, &delimiter),
1017 prefix: None,
1018 suffix: None,
1019 url: None,
1020 ref_type: Some(ctx.ref_type.to_string()),
1021 config: Some(ctx.options.config.clone()),
1022 bibliography_config: ctx.options.bibliography_config.clone(),
1023 item_language: crate::values::effective_component_language(
1024 ctx.reference,
1025 &group_component,
1026 ),
1027 sentence_initial: false,
1028 pre_formatted: true,
1029 })
1030 }
1031
1032 fn render_group_child_values<F>(
1038 &self,
1039 fmt: &F,
1040 ctx: &TemplateRenderContext<'_>,
1041 group: &citum_schema::template::TemplateGroup,
1042 tracker: &mut TemplateComponentTracker,
1043 ) -> Option<Vec<String>>
1044 where
1045 F: crate::render::format::OutputFormat<Output = String>,
1046 {
1047 let mut has_meaningful_content = false;
1048 let mut values = Vec::with_capacity(group.group.len());
1049
1050 for item in &group.group {
1051 let Some(rendered) =
1052 self.render_template_component_with_format::<F>(ctx, item, tracker)
1053 else {
1054 continue;
1055 };
1056 let rendered_str = crate::render::render_component_with_format_and_renderer::<F>(
1057 &rendered,
1058 fmt,
1059 ctx.options.show_semantics,
1060 );
1061 if rendered_str.trim().is_empty() {
1062 continue;
1063 }
1064 if !is_term_only_component(item) {
1065 has_meaningful_content = true;
1066 }
1067 values.push(rendered_str);
1068 }
1069
1070 (has_meaningful_content && !values.is_empty()).then_some(values)
1071 }
1072
1073 fn apply_issued_no_date_fallback(
1074 &self,
1075 reference: &Reference,
1076 options: &RenderOptions<'_>,
1077 component: &TemplateComponent,
1078 values: &mut crate::values::ProcValues<String>,
1079 ) {
1080 if !matches!(
1081 component,
1082 TemplateComponent::Date(citum_schema::template::TemplateDate {
1083 date: citum_schema::template::DateVariable::Issued,
1084 ..
1085 })
1086 ) || reference.csl_issued_date().is_some()
1087 || self.preferred_no_date_term_form() != citum_schema::locale::TermForm::Long
1088 {
1089 return;
1090 }
1091
1092 if let Some(long) = options.locale.resolved_general_term(
1093 &citum_schema::locale::GeneralTerm::NoDate,
1094 &citum_schema::locale::TermForm::Long,
1095 None,
1096 ) {
1097 values.value = long;
1098 }
1099 }
1100
1101 fn apply_entry_link_fallback(
1102 &self,
1103 reference: &Reference,
1104 options: &RenderOptions<'_>,
1105 values: &mut crate::values::ProcValues<String>,
1106 ) {
1107 if values.url.is_some() {
1108 return;
1109 }
1110
1111 let Some(links) = &options.config.links else {
1112 return;
1113 };
1114 use citum_schema::options::LinkAnchor;
1115 if matches!(links.anchor, Some(LinkAnchor::Entry)) {
1116 values.url = crate::values::resolve_url(links, reference);
1117 }
1118 }
1119
1120 pub fn apply_author_substitution(&self, proc: &mut ProcTemplate, substitute: &str) {
1122 self.apply_author_substitution_with_format::<crate::render::plain::PlainText>(
1123 proc, substitute,
1124 );
1125 }
1126
1127 pub fn apply_author_substitution_with_format<F>(
1129 &self,
1130 proc: &mut ProcTemplate,
1131 substitute: &str,
1132 ) where
1133 F: crate::render::format::OutputFormat<Output = String>,
1134 {
1135 if let Some(component) = proc
1136 .iter_mut()
1137 .find(|c| matches!(c.template_component, TemplateComponent::Contributor(_)))
1138 {
1139 let fmt = F::default();
1140 component.value = fmt.text(substitute);
1141 }
1142 }
1143
1144 fn preferred_no_date_term_form(&self) -> citum_schema::locale::TermForm {
1145 match self
1146 .style
1147 .info
1148 .source
1149 .as_ref()
1150 .map(|source| source.csl_id.as_str())
1151 {
1152 Some("http://www.zotero.org/styles/harvard-cite-them-right") => {
1153 citum_schema::locale::TermForm::Long
1154 }
1155 _ => citum_schema::locale::TermForm::Short,
1156 }
1157 }
1158
1159 fn render_group_item_from_template_with_format<F>(
1160 &self,
1161 reference: &Reference,
1162 item_request: GroupItemRenderRequest<'_>,
1163 ) -> Option<String>
1164 where
1165 F: crate::render::format::OutputFormat<Output = String>,
1166 {
1167 let request = self.citation_render_request(
1168 item_request.item,
1169 item_request.template,
1170 item_request.mode,
1171 item_request.suppress_author,
1172 item_request.position,
1173 item_request.note_start_text_case,
1174 );
1175 self.render_item_from_template_with_format::<F>(reference, request, item_request.delimiter)
1176 }
1177}
1178
1179pub(super) fn template_uses_first_ref_note_number(template: &[TemplateComponent]) -> bool {
1186 template.iter().any(|c| match c {
1187 TemplateComponent::Number(n) => {
1188 n.number == citum_schema::template::NumberVariable::FirstReferenceNoteNumber
1189 }
1190 TemplateComponent::Group(g) => template_uses_first_ref_note_number(&g.group),
1191 _ => false,
1192 })
1193}
1194
1195pub(super) fn filter_author_from_template(
1196 template: &[TemplateComponent],
1197) -> (Vec<TemplateComponent>, Option<String>, bool) {
1198 let mut filtered: Vec<TemplateComponent> =
1199 template.iter().filter_map(strip_author_component).collect();
1200 let stripped_leading_affix = filtered.first().and_then(leading_group_affix);
1201 let leading_affix = stripped_leading_affix.clone().or_else(|| {
1202 filtered
1203 .first()
1204 .and_then(|_| template.first().and_then(author_group_delimiter_affix))
1205 });
1206 if let Some(first) = filtered.first_mut() {
1207 strip_leading_group_affixes(first);
1208 }
1209 (filtered, leading_affix, stripped_leading_affix.is_some())
1210}
1211
1212fn author_group_delimiter_affix(component: &TemplateComponent) -> Option<String> {
1213 let TemplateComponent::Group(group) = component else {
1214 return None;
1215 };
1216 group
1217 .group
1218 .first()
1219 .is_some_and(component_starts_with_author)
1220 .then_some(group.delimiter.as_ref())
1221 .flatten()
1222 .map(citum_schema::template::DelimiterPunctuation::to_string_with_space)
1223 .filter(|delimiter| !delimiter.is_empty())
1224}
1225
1226fn component_starts_with_author(component: &TemplateComponent) -> bool {
1227 match component {
1228 TemplateComponent::Contributor(contributor) => {
1229 contributor.contributor == citum_schema::template::ContributorRole::Author
1230 }
1231 TemplateComponent::Group(group) => group
1232 .group
1233 .first()
1234 .is_some_and(component_starts_with_author),
1235 _ => false,
1236 }
1237}