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) = filter_author_from_template(&state.template);
458 if group_delimiter.is_none() {
459 group_delimiter = leading_affix
460 .as_ref()
461 .filter(|value| !value.is_empty())
462 .cloned();
463 }
464 let item_delimiter = if leading_affix.is_some() {
465 ""
466 } else {
467 params.intra_delimiter
468 };
469 if let Some(item_str) = self.render_group_item_from_template_with_format::<F>(
470 state.reference,
471 GroupItemRenderRequest {
472 item: state.item,
473 template: &filtered_template,
474 mode: params.mode,
475 suppress_author: params.suppress_author,
476 position: params.position,
477 note_start_text_case: params.note_start_text_case,
478 delimiter: item_delimiter,
479 },
480 ) && !item_str.is_empty()
481 {
482 let prefix = (index > 0).then_some(item.prefix.as_deref()).flatten();
483 item_parts.push(self.affix_content(fmt, item_str, prefix, item.suffix.as_deref()));
484 }
485 }
486 Ok((item_parts, group_delimiter))
487 }
488
489 fn resolve_group_render_state<'b>(
490 &'b self,
491 group: &'b [&'b crate::reference::CitationItem],
492 spec: &'b citum_schema::CitationSpec,
493 ) -> Result<GroupRenderState<'b>, ProcessorError> {
494 #[allow(clippy::indexing_slicing, reason = "groups are non-empty")]
495 let first_item = group[0];
496 let first_ref = self
497 .bibliography
498 .get(&first_item.id)
499 .ok_or_else(|| ProcessorError::ReferenceNotFound(first_item.id.clone()))?;
500 let first_language = crate::values::effective_item_language(first_ref);
501 let default_template = spec
502 .resolve_template_for_language(first_language.as_deref())
503 .map(Cow::Owned);
504
505 let ref_type = first_ref.ref_type();
506 let first_template = resolve_type_variant(spec.type_variants.as_ref(), &ref_type)
507 .map(Cow::Borrowed)
508 .or(default_template);
509
510 Ok(GroupRenderState {
511 first_item,
512 first_ref,
513 template: first_template.unwrap_or(Cow::Borrowed(&[])),
514 })
515 }
516
517 fn resolve_item_render_state<'b>(
518 &'b self,
519 item: &'b crate::reference::CitationItem,
520 spec: &'b citum_schema::CitationSpec,
521 ) -> Result<ItemRenderState<'b>, ProcessorError> {
522 let reference = self
523 .bibliography
524 .get(&item.id)
525 .ok_or_else(|| ProcessorError::ReferenceNotFound(item.id.clone()))?;
526 let item_language = crate::values::effective_item_language(reference);
527 let default_template = spec
528 .resolve_template_for_language(item_language.as_deref())
529 .map(Cow::Owned);
530
531 let ref_type = reference.ref_type();
532 let item_template = resolve_type_variant(spec.type_variants.as_ref(), &ref_type)
533 .map(Cow::Borrowed)
534 .or(default_template);
535
536 Ok(ItemRenderState {
537 item,
538 reference,
539 template: item_template.unwrap_or(Cow::Borrowed(&[])),
540 })
541 }
542
543 fn try_render_integral_group_with_format<F>(
544 &self,
545 group: &[&crate::reference::CitationItem],
546 spec: &citum_schema::CitationSpec,
547 mode: &citum_schema::citation::CitationMode,
548 suppress_author: bool,
549 position: Option<&citum_schema::citation::Position>,
550 ) -> Result<Option<String>, ProcessorError>
551 where
552 F: crate::render::format::OutputFormat<Output = String>,
553 {
554 if !matches!(mode, citum_schema::citation::CitationMode::Integral)
555 || !self.has_explicit_integral_template()
556 {
557 return Ok(None);
558 }
559
560 self.render_integral_explicit_group::<F>(group, spec, mode, suppress_author, position)
561 }
562
563 fn requires_full_group_item_rendering(
574 &self,
575 mode: &citum_schema::citation::CitationMode,
576 reference: &Reference,
577 ) -> bool {
578 matches!(mode, citum_schema::citation::CitationMode::NonIntegral)
579 && matches!(
580 reference.ref_type().as_str(),
581 "legal-case" | "treaty" | "hearing" | "personal-communication"
582 )
583 }
584
585 pub(crate) fn render_author_for_grouping_with_format<F>(
587 &self,
588 reference: &Reference,
589 item: &crate::reference::CitationItem,
590 template: &[TemplateComponent],
591 mode: &citum_schema::citation::CitationMode,
592 suppress_author: bool,
593 position: Option<&citum_schema::citation::Position>,
594 ) -> String
595 where
596 F: crate::render::format::OutputFormat<Output = String>,
597 {
598 let is_note_processing = self.config.processing.as_ref().is_some_and(|processing| {
599 matches!(processing, citum_schema::options::Processing::Note)
600 });
601 if is_note_processing
602 && matches!(
603 position,
604 Some(
605 citum_schema::citation::Position::Ibid
606 | citum_schema::citation::Position::IbidWithLocator
607 )
608 )
609 && !template.iter().any(has_contributor_component)
610 {
611 return String::new();
612 }
613
614 let options = self.citation_render_options(mode.clone(), suppress_author, None, None);
615
616 if let Some(comp) = template.first().and_then(find_grouping_component) {
620 let base_hints = self
621 .hints
622 .get(reference.id().as_deref().unwrap_or_default())
623 .cloned()
624 .unwrap_or_default();
625 let hints = ProcHints {
627 position: position.cloned(),
628 integral_name_state: item.integral_name_state,
629 ..base_hints
630 };
631 if let Some(vals) = comp.values::<F>(reference, &hints, &options)
632 && !vals.value.is_empty()
633 {
634 return vals.value;
635 }
636 }
637
638 if let Some(authors) = reference.author() {
640 let names_vec = self.resolve_contributor_names(&authors);
641 F::default().text(&crate::values::format_contributors_short(
642 &names_vec, &options,
643 ))
644 } else {
645 String::new()
646 }
647 }
648
649 pub(crate) fn render_integral_anchor_with_format<F>(
651 &self,
652 items: &[crate::reference::CitationItem],
653 spec: &citum_schema::CitationSpec,
654 inter_delimiter: &str,
655 suppress_author: bool,
656 position: Option<&citum_schema::citation::Position>,
657 ) -> Result<String, ProcessorError>
658 where
659 F: crate::render::format::OutputFormat<Output = String>,
660 {
661 let groups = group_citation_items_by_author(self, items);
662
663 let mut rendered_groups = Vec::new();
664 let fmt = F::default();
665 for (_author_key, group) in groups {
666 #[allow(
667 clippy::indexing_slicing,
668 reason = "group is non-empty by construction"
669 )]
670 let first_item = group[0];
671 let reference = self
672 .bibliography
673 .get(&first_item.id)
674 .ok_or_else(|| ProcessorError::ReferenceNotFound(first_item.id.clone()))?;
675 let item_language = crate::values::effective_item_language(reference);
676 let template = spec.resolve_template_for_language(item_language.as_deref());
677 let effective_template = template.as_deref().unwrap_or(&[]);
678 let author_part = self.render_author_for_grouping_with_format::<F>(
679 reference,
680 first_item,
681 effective_template,
682 &citum_schema::citation::CitationMode::Integral,
683 suppress_author,
684 position,
685 );
686 if !author_part.is_empty() {
687 rendered_groups.push(author_part);
688 }
689 }
690
691 Ok(fmt.join(rendered_groups, inter_delimiter))
692 }
693
694 #[must_use]
696 pub fn get_or_assign_citation_number(&self, ref_id: &str) -> usize {
697 let mut numbers = self.citation_numbers.borrow_mut();
698 let next_num = numbers.len() + 1;
699 *numbers.entry(ref_id.to_string()).or_insert(next_num)
700 }
701
702 #[must_use]
704 pub fn process_bibliography_entry(
705 &self,
706 reference: &Reference,
707 entry_number: usize,
708 ) -> Option<ProcTemplate> {
709 self.process_bibliography_entry_with_format::<crate::render::plain::PlainText>(
710 reference,
711 entry_number,
712 )
713 }
714
715 #[must_use]
717 pub fn process_bibliography_entry_with_format<F>(
718 &self,
719 reference: &Reference,
720 entry_number: usize,
721 ) -> Option<ProcTemplate>
722 where
723 F: crate::render::format::OutputFormat<Output = String>,
724 {
725 let bib_spec = self.style.bibliography.as_ref()?;
726
727 let item_language = crate::values::effective_item_language(reference);
728 let default_template = bib_spec
729 .resolve_template_for_language(item_language.as_deref())
730 .map(Cow::Owned);
731
732 let ref_type = reference.ref_type();
733 let template = resolve_type_variant(bib_spec.type_variants.as_ref(), &ref_type)
734 .map(Cow::Borrowed)
735 .or(default_template)?;
736
737 let template = self.apply_anonymous_entry_bibliography_policy(reference, template)?;
738 let template = self.apply_article_journal_bibliography_policy(reference, template);
739
740 self.process_template_request_with_format::<F>(
741 reference,
742 TemplateRenderRequest {
743 template: template.as_ref(),
744 context: RenderContext::Bibliography,
745 mode: citum_schema::citation::CitationMode::NonIntegral,
746 suppress_author: false,
747 locator_raw: None,
748 citation_number: entry_number,
749 position: None,
750 note_start_text_case: None,
751 integral_name_state: None,
752 org_abbreviation_state: None,
753 first_reference_note_number: None,
754 },
755 )
756 }
757
758 #[must_use]
763 pub fn process_template_with_number(
764 &self,
765 reference: &Reference,
766 params: TemplateRenderParams<'_>,
767 ) -> Option<ProcTemplate> {
768 self.process_template_with_number_with_format::<crate::render::plain::PlainText>(
769 reference, params,
770 )
771 }
772
773 pub fn process_template_with_number_with_format<F>(
778 &self,
779 reference: &Reference,
780 params: TemplateRenderParams<'_>,
781 ) -> Option<ProcTemplate>
782 where
783 F: crate::render::format::OutputFormat<Output = String>,
784 {
785 self.process_template_request_with_format::<F>(
786 reference,
787 TemplateRenderRequest {
788 template: params.template,
789 context: params.context,
790 mode: params.mode,
791 suppress_author: params.suppress_author,
792 locator_raw: params.locator_raw,
793 citation_number: params.citation_number,
794 position: params.position.cloned(),
795 note_start_text_case: params.note_start_text_case,
796 integral_name_state: params.integral_name_state,
797 org_abbreviation_state: params.org_abbreviation_state,
798 first_reference_note_number: None,
799 },
800 )
801 }
802
803 #[must_use]
805 pub fn process_template_request_with_format<F>(
806 &self,
807 reference: &Reference,
808 request: TemplateRenderRequest<'_>,
809 ) -> Option<ProcTemplate>
810 where
811 F: crate::render::format::OutputFormat<Output = String>,
812 {
813 let TemplateRenderRequest {
814 template,
815 context,
816 mode,
817 suppress_author,
818 locator_raw,
819 citation_number,
820 position,
821 note_start_text_case,
822 integral_name_state,
823 org_abbreviation_state,
824 first_reference_note_number,
825 } = request;
826 let ref_type = reference.ref_type();
827 let options = RenderOptions {
828 config: self.config,
829 bibliography_config: self.bibliography_config.clone(),
830 locale: self.locale,
831 context,
832 mode,
833 suppress_author,
834 locator_raw,
835 ref_type: Some(ref_type.clone()),
836 show_semantics: self.show_semantics,
837 current_template_index: None,
838 abbreviation_map: self.abbreviation_map,
839 };
840 let effective_first_ref_note = if template_uses_first_ref_note_number(template) {
845 first_reference_note_number
846 } else {
847 None
848 };
849 let hint = self.build_template_render_hint(HintInputs {
850 reference,
851 context: options.context,
852 citation_number,
853 position,
854 integral_name_state,
855 org_abbreviation_state,
856 first_reference_note_number: effective_first_ref_note,
857 });
858 let mut components =
859 self.render_template_components::<F>(reference, &ref_type, &options, &hint, template);
860
861 self.apply_sentence_initial_context::<F>(&mut components, context, note_start_text_case);
862
863 (!components.is_empty()).then_some(components)
864 }
865
866 fn render_template_components<F>(
870 &self,
871 reference: &Reference,
872 ref_type: &str,
873 options: &RenderOptions<'_>,
874 hint: &ProcHints,
875 template: &[TemplateComponent],
876 ) -> Vec<ProcTemplateComponent>
877 where
878 F: crate::render::format::OutputFormat<Output = String>,
879 {
880 let mut tracker = TemplateComponentTracker::default();
881 let mut components = Vec::with_capacity(template.len());
882 let mut component_options = options.clone();
883 for (template_index, component) in template.iter().enumerate() {
884 component_options.current_template_index =
885 self.inject_ast_indices.then_some(template_index);
886 let ctx = TemplateRenderContext {
887 reference,
888 ref_type,
889 options: &component_options,
890 hint,
891 template_index,
892 };
893 if let Some(component) =
894 self.render_template_component_with_format::<F>(&ctx, component, &mut tracker)
895 {
896 components.push(component);
897 }
898 }
899 components
900 }
901
902 fn build_template_render_hint(&self, inputs: HintInputs<'_>) -> ProcHints {
903 let HintInputs {
904 reference,
905 context,
906 citation_number,
907 position,
908 integral_name_state,
909 org_abbreviation_state,
910 first_reference_note_number,
911 } = inputs;
912 let default_hint = ProcHints::default();
913 let base_hint = self
914 .hints
915 .get(reference.id().as_deref().unwrap_or_default())
916 .unwrap_or(&default_hint);
917 let is_subsequent = matches!(position, Some(citum_schema::citation::Position::Subsequent));
918 ProcHints {
919 citation_number: (citation_number > 0).then_some(citation_number),
920 citation_sub_label: if context == RenderContext::Citation {
921 reference
922 .id()
923 .as_deref()
924 .and_then(|id| self.citation_sub_label_for_ref(id))
925 } else {
926 None
927 },
928 position,
929 integral_name_state,
930 org_abbreviation_state,
931 first_reference_note_number: if is_subsequent {
932 first_reference_note_number
933 } else {
934 None
935 },
936 suppress_disambiguation_title: is_subsequent && first_reference_note_number.is_some(),
937 ..base_hint.clone()
938 }
939 }
940
941 fn render_template_component_with_format<F>(
942 &self,
943 ctx: &TemplateRenderContext<'_>,
944 component: &TemplateComponent,
945 tracker: &mut TemplateComponentTracker,
946 ) -> Option<ProcTemplateComponent>
947 where
948 F: crate::render::format::OutputFormat<Output = String>,
949 {
950 if let TemplateComponent::Group(group) = component {
951 return self.render_group_component_with_format::<F>(ctx, group, tracker);
952 }
953
954 let resolved_component = component;
955 let var_key = get_variable_key(resolved_component);
956 if tracker.should_skip(var_key.as_deref()) {
957 return None;
958 }
959
960 let mut values = resolved_component.values::<F>(ctx.reference, ctx.hint, ctx.options)?;
961 if values.value.trim().is_empty() {
965 return None;
966 }
967 self.apply_issued_no_date_fallback(
968 ctx.reference,
969 ctx.options,
970 resolved_component,
971 &mut values,
972 );
973 self.apply_entry_link_fallback(ctx.reference, ctx.options, &mut values);
974
975 let item_language =
976 crate::values::effective_component_language(ctx.reference, resolved_component);
977 tracker.mark_rendered(var_key, values.substituted_key.as_deref());
978
979 Some(ProcTemplateComponent {
980 template_component: resolved_component.clone(),
981 template_index: self.inject_ast_indices.then_some(ctx.template_index),
982 value: values.value,
983 prefix: values.prefix,
984 suffix: values.suffix,
985 url: values.url,
986 ref_type: Some(ctx.ref_type.to_string()),
987 config: Some(ctx.options.config.clone()),
988 bibliography_config: ctx.options.bibliography_config.clone(),
989 item_language,
990 sentence_initial: false,
991 pre_formatted: values.pre_formatted,
992 })
993 }
994
995 fn render_group_component_with_format<F>(
996 &self,
997 ctx: &TemplateRenderContext<'_>,
998 group: &citum_schema::template::TemplateGroup,
999 tracker: &mut TemplateComponentTracker,
1000 ) -> Option<ProcTemplateComponent>
1001 where
1002 F: crate::render::format::OutputFormat<Output = String>,
1003 {
1004 let fmt = F::default();
1005 let values = self.render_group_child_values(&fmt, ctx, group, tracker)?;
1006 let delimiter = group
1007 .delimiter
1008 .as_ref()
1009 .unwrap_or(&citum_schema::template::DelimiterPunctuation::Comma)
1010 .to_string_with_space();
1011 let group_component = TemplateComponent::Group(group.clone());
1012 Some(ProcTemplateComponent {
1013 template_component: group_component.clone(),
1014 template_index: self.inject_ast_indices.then_some(ctx.template_index),
1015 value: fmt.join(values, &delimiter),
1016 prefix: None,
1017 suffix: None,
1018 url: None,
1019 ref_type: Some(ctx.ref_type.to_string()),
1020 config: Some(ctx.options.config.clone()),
1021 bibliography_config: ctx.options.bibliography_config.clone(),
1022 item_language: crate::values::effective_component_language(
1023 ctx.reference,
1024 &group_component,
1025 ),
1026 sentence_initial: false,
1027 pre_formatted: true,
1028 })
1029 }
1030
1031 fn render_group_child_values<F>(
1037 &self,
1038 fmt: &F,
1039 ctx: &TemplateRenderContext<'_>,
1040 group: &citum_schema::template::TemplateGroup,
1041 tracker: &mut TemplateComponentTracker,
1042 ) -> Option<Vec<String>>
1043 where
1044 F: crate::render::format::OutputFormat<Output = String>,
1045 {
1046 let mut has_meaningful_content = false;
1047 let mut values = Vec::with_capacity(group.group.len());
1048
1049 for item in &group.group {
1050 let Some(rendered) =
1051 self.render_template_component_with_format::<F>(ctx, item, tracker)
1052 else {
1053 continue;
1054 };
1055 let rendered_str = crate::render::render_component_with_format_and_renderer::<F>(
1056 &rendered,
1057 fmt,
1058 ctx.options.show_semantics,
1059 );
1060 if rendered_str.trim().is_empty() {
1061 continue;
1062 }
1063 if !is_term_only_component(item) {
1064 has_meaningful_content = true;
1065 }
1066 values.push(rendered_str);
1067 }
1068
1069 (has_meaningful_content && !values.is_empty()).then_some(values)
1070 }
1071
1072 fn apply_issued_no_date_fallback(
1073 &self,
1074 reference: &Reference,
1075 options: &RenderOptions<'_>,
1076 component: &TemplateComponent,
1077 values: &mut crate::values::ProcValues<String>,
1078 ) {
1079 if !matches!(
1080 component,
1081 TemplateComponent::Date(citum_schema::template::TemplateDate {
1082 date: citum_schema::template::DateVariable::Issued,
1083 ..
1084 })
1085 ) || reference.csl_issued_date().is_some()
1086 || self.preferred_no_date_term_form() != citum_schema::locale::TermForm::Long
1087 {
1088 return;
1089 }
1090
1091 if let Some(long) = options.locale.resolved_general_term(
1092 &citum_schema::locale::GeneralTerm::NoDate,
1093 &citum_schema::locale::TermForm::Long,
1094 None,
1095 ) {
1096 values.value = long;
1097 }
1098 }
1099
1100 fn apply_entry_link_fallback(
1101 &self,
1102 reference: &Reference,
1103 options: &RenderOptions<'_>,
1104 values: &mut crate::values::ProcValues<String>,
1105 ) {
1106 if values.url.is_some() {
1107 return;
1108 }
1109
1110 let Some(links) = &options.config.links else {
1111 return;
1112 };
1113 use citum_schema::options::LinkAnchor;
1114 if matches!(links.anchor, Some(LinkAnchor::Entry)) {
1115 values.url = crate::values::resolve_url(links, reference);
1116 }
1117 }
1118
1119 pub fn apply_author_substitution(&self, proc: &mut ProcTemplate, substitute: &str) {
1121 self.apply_author_substitution_with_format::<crate::render::plain::PlainText>(
1122 proc, substitute,
1123 );
1124 }
1125
1126 pub fn apply_author_substitution_with_format<F>(
1128 &self,
1129 proc: &mut ProcTemplate,
1130 substitute: &str,
1131 ) where
1132 F: crate::render::format::OutputFormat<Output = String>,
1133 {
1134 if let Some(component) = proc
1135 .iter_mut()
1136 .find(|c| matches!(c.template_component, TemplateComponent::Contributor(_)))
1137 {
1138 let fmt = F::default();
1139 component.value = fmt.text(substitute);
1140 }
1141 }
1142
1143 fn preferred_no_date_term_form(&self) -> citum_schema::locale::TermForm {
1144 match self
1145 .style
1146 .info
1147 .source
1148 .as_ref()
1149 .map(|source| source.csl_id.as_str())
1150 {
1151 Some("http://www.zotero.org/styles/harvard-cite-them-right") => {
1152 citum_schema::locale::TermForm::Long
1153 }
1154 _ => citum_schema::locale::TermForm::Short,
1155 }
1156 }
1157
1158 fn render_group_item_from_template_with_format<F>(
1159 &self,
1160 reference: &Reference,
1161 item_request: GroupItemRenderRequest<'_>,
1162 ) -> Option<String>
1163 where
1164 F: crate::render::format::OutputFormat<Output = String>,
1165 {
1166 let request = self.citation_render_request(
1167 item_request.item,
1168 item_request.template,
1169 item_request.mode,
1170 item_request.suppress_author,
1171 item_request.position,
1172 item_request.note_start_text_case,
1173 );
1174 self.render_item_from_template_with_format::<F>(reference, request, item_request.delimiter)
1175 }
1176}
1177
1178pub(super) fn template_uses_first_ref_note_number(template: &[TemplateComponent]) -> bool {
1185 template.iter().any(|c| match c {
1186 TemplateComponent::Number(n) => {
1187 n.number == citum_schema::template::NumberVariable::FirstReferenceNoteNumber
1188 }
1189 TemplateComponent::Group(g) => template_uses_first_ref_note_number(&g.group),
1190 _ => false,
1191 })
1192}
1193
1194pub(super) fn filter_author_from_template(
1195 template: &[TemplateComponent],
1196) -> (Vec<TemplateComponent>, Option<String>) {
1197 let mut filtered: Vec<TemplateComponent> =
1198 template.iter().filter_map(strip_author_component).collect();
1199 let leading_affix = filtered.first().and_then(leading_group_affix);
1200 if let Some(first) = filtered.first_mut() {
1201 strip_leading_group_affixes(first);
1202 }
1203 (filtered, leading_affix)
1204}