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