1use crate::api::AnnotationStyle;
9use crate::error::ProcessorError;
10use crate::processor::Processor;
11use crate::reference::{Bibliography, Citation};
12use crate::render::djot::Djot;
13use crate::render::format::OutputFormat;
14use crate::render::html::Html;
15use crate::render::latex::Latex;
16use crate::render::markdown::Markdown;
17use crate::render::plain::PlainText;
18use crate::render::typst::Typst;
19use citum_schema::Style;
20use citum_schema::locale::{GeneralTerm, TermForm};
21use citum_schema::reference::{
22 ClassExtension, CollectionType, ContributorRole as ReferenceRole, MonographComponentType,
23 MonographType, ReferenceClass, SerialComponentType,
24};
25use citum_schema::template::ContributorRole as TemplateRole;
26
27use serde::{Deserialize, Serialize};
28use std::collections::HashMap;
29
30use super::{
31 BibliographyEntry, CitationOccurrence, DocumentOptions, EntryMetadata, FormattedBibliography,
32 FormattedCitation, OutputFormatKind, RefsInput, StyleInput, Warning, WarningLevel,
33};
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct FormatDocumentRequest {
38 pub style: StyleInput,
40 pub locale: Option<String>,
45 #[serde(default)]
48 pub output_format: OutputFormatKind,
49 pub refs: RefsInput,
51 pub citations: Vec<CitationOccurrence>,
53 pub document_options: Option<DocumentOptions>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct FormatDocumentResult {
60 pub formatted_citations: Vec<FormattedCitation>,
62 pub bibliography: FormattedBibliography,
64 pub warnings: Vec<Warning>,
66}
67
68#[derive(Debug)]
70pub enum FormatDocumentError {
71 UnresolvedInput(String),
73 StyleParse(String),
75 StylePath(String),
77 RefsInputPath(String),
79 RefsInputParse(String),
81 Processing(ProcessorError),
83 StyleResolution(String),
85}
86
87impl std::fmt::Display for FormatDocumentError {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 Self::UnresolvedInput(msg) => write!(f, "Unresolved style input: {}", msg),
91 Self::StyleParse(msg) => write!(f, "Style parse error: {}", msg),
92 Self::StylePath(msg) => write!(f, "Style path error: {}", msg),
93 Self::RefsInputPath(msg) => write!(f, "Refs input path error: {}", msg),
94 Self::RefsInputParse(msg) => write!(f, "Refs input parse error: {}", msg),
95 Self::Processing(err) => write!(f, "Processing error: {}", err),
96 Self::StyleResolution(msg) => write!(f, "Style resolution error: {}", msg),
97 }
98 }
99}
100
101impl std::error::Error for FormatDocumentError {}
102
103impl From<ProcessorError> for FormatDocumentError {
104 fn from(err: ProcessorError) -> Self {
105 Self::Processing(err)
106 }
107}
108
109pub fn format_document(
119 request: FormatDocumentRequest,
120) -> Result<FormatDocumentResult, FormatDocumentError> {
121 let style = request.style.resolve_local()?;
122 format_document_with_style(style, request)
123}
124
125pub fn format_document_with_resolver(
136 request: FormatDocumentRequest,
137 resolver: &citum_schema::StyleResolver,
138) -> Result<FormatDocumentResult, FormatDocumentError> {
139 let style = match &request.style {
140 StyleInput::Yaml(_) => request.style.resolve_local()?,
141 StyleInput::Id(value) | StyleInput::Uri(value) | StyleInput::Path(value) => resolver
142 .resolve_style(value)
143 .map_err(|e| FormatDocumentError::UnresolvedInput(e.to_string()))?,
144 };
145 let mut resolved = style
149 .try_into_resolved_with(Some(resolver))
150 .map_err(|e| FormatDocumentError::StyleResolution(e.to_string()))?;
151 resolved.extends = None;
152 format_document_with_style(resolved, request)
153}
154
155#[allow(
164 clippy::too_many_lines,
165 reason = "match arms grow one-to-one with format variants"
166)]
167pub fn format_document_with_style(
168 style: Style,
169 request: FormatDocumentRequest,
170) -> Result<FormatDocumentResult, FormatDocumentError> {
171 let mut warnings = Vec::new();
172
173 if let Some(tag) = &request.locale
178 && !tag.is_empty()
179 && !tag.eq_ignore_ascii_case("en-us")
180 {
181 warnings.push(Warning {
182 level: WarningLevel::Warning,
183 code: "locale_fallback".to_string(),
184 citation_id: None,
185 ref_id: None,
186 message: format!(
187 "Requested locale '{tag}' could not be loaded by the engine; falling back to en-US. Adapter-side locale resolution is not yet wired through."
188 ),
189 });
190 }
191
192 let bibliography = request.refs.resolve_local()?;
193 let mut processor = Processor::new(style, bibliography);
194 warnings.extend(unknown_reference_class_warnings(&processor.bibliography));
195 warnings.extend(unknown_enum_warnings(&processor));
196
197 if let Some(opts) = &request.document_options {
198 if let Some(show_semantics) = opts.show_semantics {
199 processor.show_semantics = show_semantics;
200 }
201 if let Some(inject_ast) = opts.inject_ast_indices {
202 processor.set_inject_ast_indices(inject_ast);
203 }
204 if let Some(abbr_map) = opts.abbreviation_map.clone() {
205 processor.abbreviation_map = Some(abbr_map);
206 }
207 if opts.integral_name_memory.is_some() {
208 warnings.push(Warning {
209 level: WarningLevel::Warning,
210 code: "integral_name_memory_not_applied".to_string(),
211 citation_id: None,
212 ref_id: None,
213 message: "document_options.integral_name_memory is accepted but not yet wired through the processor; tracked in csl26-wq0y.".to_string(),
214 });
215 }
216 }
217
218 let mut citations: Vec<Citation> = Vec::new();
223 for occ in request.citations {
224 let mut citation: Citation = occ.into();
225 citation.items.retain(|item| {
226 if processor.bibliography.contains_key(&item.id) {
227 true
228 } else {
229 warnings.push(Warning {
230 level: WarningLevel::Warning,
231 code: "missing_ref".to_string(),
232 citation_id: citation.id.clone(),
233 ref_id: Some(item.id.clone()),
234 message: format!("Reference '{}' not found in bibliography", item.id),
235 });
236 false
237 }
238 });
239 citations.push(citation);
240 }
241
242 let formatted_citations = match request.output_format {
244 OutputFormatKind::Plain => format_by_kind::<PlainText>(&processor, &citations)?,
245 OutputFormatKind::Html => format_by_kind::<Html>(&processor, &citations)?,
246 OutputFormatKind::Djot => format_by_kind::<Djot>(&processor, &citations)?,
247 OutputFormatKind::Latex => format_by_kind::<Latex>(&processor, &citations)?,
248 OutputFormatKind::Typst => format_by_kind::<Typst>(&processor, &citations)?,
249 OutputFormatKind::Markdown => format_by_kind::<Markdown>(&processor, &citations)?,
250 };
251
252 let bibliography = match request.output_format {
254 OutputFormatKind::Plain => format_bibliography::<PlainText>(
255 &processor,
256 request.output_format,
257 request.document_options.as_ref(),
258 )?,
259 OutputFormatKind::Html => format_bibliography::<Html>(
260 &processor,
261 request.output_format,
262 request.document_options.as_ref(),
263 )?,
264 OutputFormatKind::Djot => format_bibliography::<Djot>(
265 &processor,
266 request.output_format,
267 request.document_options.as_ref(),
268 )?,
269 OutputFormatKind::Latex => format_bibliography::<Latex>(
270 &processor,
271 request.output_format,
272 request.document_options.as_ref(),
273 )?,
274 OutputFormatKind::Typst => format_bibliography::<Typst>(
275 &processor,
276 request.output_format,
277 request.document_options.as_ref(),
278 )?,
279 OutputFormatKind::Markdown => format_bibliography::<Markdown>(
280 &processor,
281 request.output_format,
282 request.document_options.as_ref(),
283 )?,
284 };
285
286 Ok(FormatDocumentResult {
287 formatted_citations,
288 bibliography,
289 warnings,
290 })
291}
292
293pub fn unknown_reference_class_warnings(bibliography: &Bibliography) -> Vec<Warning> {
295 bibliography
296 .iter()
297 .filter_map(|(ref_id, reference)| {
298 let ReferenceClass::Unknown(class) = reference.class() else {
299 return None;
300 };
301 Some(Warning {
302 level: WarningLevel::Warning,
303 code: "unknown_reference_class".to_string(),
304 citation_id: None,
305 ref_id: Some(ref_id.clone()),
306 message: format!(
307 "Reference '{ref_id}' uses unknown class '{class}'; rendering will use only fields this engine understands."
308 ),
309 })
310 })
311 .collect()
312}
313
314pub fn unknown_enum_warnings(processor: &Processor) -> Vec<Warning> {
319 let mut warnings = Vec::new();
320
321 for (ref_id, reference) in &processor.bibliography {
323 match reference.extension() {
324 ClassExtension::Monograph(r) => {
325 if let MonographType::Unknown(s) = &r.r#type {
326 warnings.push(Warning {
327 level: WarningLevel::Warning,
328 code: "unknown_enum_variant".to_string(),
329 citation_id: None,
330 ref_id: Some(ref_id.clone()),
331 message: format!("Reference '{ref_id}' uses unknown monograph type '{s}'; rendering will use default monograph formatting."),
332 });
333 }
334 }
335 ClassExtension::Collection(r) => {
336 if let CollectionType::Unknown(s) = &r.r#type {
337 warnings.push(Warning {
338 level: WarningLevel::Warning,
339 code: "unknown_enum_variant".to_string(),
340 citation_id: None,
341 ref_id: Some(ref_id.clone()),
342 message: format!("Reference '{ref_id}' uses unknown collection type '{s}'; rendering will use default collection formatting."),
343 });
344 }
345 }
346 ClassExtension::CollectionComponent(r) => {
347 if let MonographComponentType::Unknown(s) = &r.r#type {
348 warnings.push(Warning {
349 level: WarningLevel::Warning,
350 code: "unknown_enum_variant".to_string(),
351 citation_id: None,
352 ref_id: Some(ref_id.clone()),
353 message: format!("Reference '{ref_id}' uses unknown monograph component type '{s}'; rendering will use default chapter formatting."),
354 });
355 }
356 }
357 ClassExtension::SerialComponent(r) => {
358 if let SerialComponentType::Unknown(s) = &r.r#type {
359 warnings.push(Warning {
360 level: WarningLevel::Warning,
361 code: "unknown_enum_variant".to_string(),
362 citation_id: None,
363 ref_id: Some(ref_id.clone()),
364 message: format!("Reference '{ref_id}' uses unknown serial component type '{s}'; rendering will use default article formatting."),
365 });
366 }
367 }
368 _ => {}
369 }
370
371 for contributor in reference.all_contributor_entries() {
372 if let ReferenceRole::Unknown(s) = &contributor.role {
373 warnings.push(Warning {
374 level: WarningLevel::Warning,
375 code: "unknown_enum_variant".to_string(),
376 citation_id: None,
377 ref_id: Some(ref_id.clone()),
378 message: format!("Reference '{ref_id}' uses unknown contributor role '{s}'; this role may be ignored during rendering."),
379 });
380 }
381 }
382 }
383
384 if let Some(templates) = &processor.style.templates {
386 for (name, template) in templates {
387 scan_template_for_unknowns(template, &format!("template '{name}'"), &mut warnings);
388 }
389 }
390 if let Some(citation) = &processor.style.citation
391 && let Some(template) = &citation.template
392 {
393 scan_template_for_unknowns(template, "citation layout", &mut warnings);
394 }
395 if let Some(bib) = &processor.style.bibliography
396 && let Some(template) = &bib.template
397 {
398 scan_template_for_unknowns(template, "bibliography layout", &mut warnings);
399 }
400
401 warnings
402}
403
404fn scan_template_for_unknowns(
405 components: &[citum_schema::template::TemplateComponent],
406 location: &str,
407 warnings: &mut Vec<Warning>,
408) {
409 use citum_schema::template::TemplateComponent;
410 for component in components {
411 match component {
412 TemplateComponent::Term(t) => {
413 if let GeneralTerm::Unknown(s) = &t.term {
414 warnings.push(Warning {
415 level: WarningLevel::Warning,
416 code: "unknown_enum_variant".to_string(),
417 citation_id: None,
418 ref_id: None,
419 message: format!("Style {location} uses unknown locale term key '{s}'; this term may render as empty."),
420 });
421 }
422 if let Some(TermForm::Unknown(s)) = &t.form {
423 warnings.push(Warning {
424 level: WarningLevel::Warning,
425 code: "unknown_enum_variant".to_string(),
426 citation_id: None,
427 ref_id: None,
428 message: format!("Style {location} uses unknown term form '{s}'; falling back to long form."),
429 });
430 }
431 }
432 TemplateComponent::Contributor(c) => {
433 if let TemplateRole::Unknown(s) = &c.contributor {
434 warnings.push(Warning {
435 level: WarningLevel::Warning,
436 code: "unknown_enum_variant".to_string(),
437 citation_id: None,
438 ref_id: None,
439 message: format!("Style {location} uses unknown contributor role '{s}'; this role may be ignored."),
440 });
441 }
442 }
443 TemplateComponent::Date(d) => {
444 if let citum_schema::template::DateForm::Unknown(s) = &d.form {
445 warnings.push(Warning {
446 level: WarningLevel::Warning,
447 code: "unknown_enum_variant".to_string(),
448 citation_id: None,
449 ref_id: None,
450 message: format!("Style {location} uses unknown date form '{s}'; falling back to year only."),
451 });
452 }
453 }
454 TemplateComponent::Group(g) => {
455 scan_template_for_unknowns(&g.group, location, warnings);
456 }
457 _ => {}
458 }
459 }
460}
461
462fn format_by_kind<F>(
464 processor: &Processor,
465 citations: &[Citation],
466) -> Result<Vec<FormattedCitation>, FormatDocumentError>
467where
468 F: OutputFormat<Output = String>,
469{
470 let texts = processor.process_citations_with_format::<F>(citations)?;
471
472 let formatted = citations
473 .iter()
474 .zip(texts.iter())
475 .map(|(citation, text)| {
476 let ref_ids = citation.items.iter().map(|item| item.id.clone()).collect();
477 FormattedCitation {
478 id: citation.id.clone().unwrap_or_default(),
479 text: text.clone(),
480 ref_ids,
481 }
482 })
483 .collect();
484
485 Ok(formatted)
486}
487
488fn format_bibliography<F>(
490 processor: &Processor,
491 format_kind: OutputFormatKind,
492 doc_opts: Option<&DocumentOptions>,
493) -> Result<FormattedBibliography, FormatDocumentError>
494where
495 F: OutputFormat<Output = String>,
496{
497 let (annotations, annotation_style) = if let Some(opts) = doc_opts {
499 if let Some(anns) = &opts.annotations {
500 let style = opts.annotation_format.as_ref().map(|fmt| AnnotationStyle {
501 format: fmt.clone(),
502 });
503 (anns.clone(), style)
504 } else {
505 (HashMap::new(), None)
506 }
507 } else {
508 (HashMap::new(), None)
509 };
510
511 let content = if annotations.is_empty() {
513 processor
514 .render_bibliography_with_format_and_annotations::<F>(None, annotation_style.as_ref())
515 } else {
516 processor.render_bibliography_with_format_and_annotations::<F>(
517 Some(&annotations),
518 annotation_style.as_ref(),
519 )
520 };
521
522 let proc_entries = processor.process_references().bibliography;
524 let entries = proc_entries
525 .into_iter()
526 .map(|entry| {
527 let entry_anns = if annotations.is_empty() {
528 None
529 } else {
530 Some(&annotations)
531 };
532 let text = crate::render::bibliography::refs_to_string_with_format::<F>(
533 vec![entry.clone()],
534 entry_anns,
535 annotation_style.as_ref(),
536 );
537 let metadata = EntryMetadata {
538 author: entry.metadata.author.unwrap_or_default(),
539 year: entry.metadata.year.unwrap_or_default(),
540 title: entry.metadata.title.unwrap_or_default(),
541 };
542 BibliographyEntry {
543 id: entry.id,
544 text,
545 metadata,
546 }
547 })
548 .collect();
549
550 Ok(FormattedBibliography {
551 format: format_kind,
552 content,
553 entries,
554 })
555}
556
557#[cfg(test)]
558#[allow(
559 clippy::unwrap_used,
560 clippy::expect_used,
561 clippy::panic,
562 clippy::indexing_slicing,
563 reason = "test code uses assertions and panic"
564)]
565mod tests {
566 use super::*;
567 use crate::api::CitationOccurrenceItem;
568 use crate::{
569 Config, ContributorForm, ContributorRole, DateForm, Processing, Rendering,
570 TemplateComponent, TemplateContributor, TemplateDate, TemplateDateVariable,
571 WrapPunctuation,
572 };
573 use citum_schema::reference::{EdtfString, InputReference, Monograph, MonographType, Title};
574 use citum_schema::{CitationSpec, StyleInfo};
575
576 fn make_test_style() -> Style {
577 Style {
578 info: StyleInfo {
579 title: Some("Test Style".to_string()),
580 id: Some("test".into()),
581 ..Default::default()
582 },
583 options: Some(Config {
584 processing: Some(Processing::AuthorDate),
585 ..Default::default()
586 }),
587 citation: Some(CitationSpec {
588 template: Some(vec![
589 TemplateComponent::Contributor(TemplateContributor {
590 contributor: ContributorRole::Author,
591 form: ContributorForm::Short,
592 rendering: Rendering::default(),
593 ..Default::default()
594 }),
595 TemplateComponent::Date(TemplateDate {
596 date: TemplateDateVariable::Issued,
597 form: DateForm::Year,
598 rendering: Rendering::default(),
599 ..Default::default()
600 }),
601 ]),
602 wrap: Some(WrapPunctuation::Parentheses.into()),
603 ..Default::default()
604 }),
605 ..Default::default()
606 }
607 }
608
609 fn make_test_bibliography() -> RefsInput {
610 let mut refs = Bibliography::new();
611 refs.insert(
612 "smith2020".to_string(),
613 InputReference::Monograph(Box::new(Monograph {
614 id: Some("smith2020".into()),
615 r#type: MonographType::Book,
616 title: Some(Title::Single("Sample Work".to_string())),
617 issued: EdtfString("2020".to_string()),
618 ..Default::default()
619 })),
620 );
621 RefsInput::Json(serde_json::to_value(refs).unwrap())
622 }
623
624 #[test]
625 fn format_document_with_style_empty_citations() {
626 let style = make_test_style();
627 let refs = make_test_bibliography();
628 let request = FormatDocumentRequest {
629 style: StyleInput::Yaml("dummy".to_string()),
630 locale: None,
631 output_format: OutputFormatKind::Plain,
632 refs,
633 citations: vec![],
634 document_options: None,
635 };
636
637 let result = format_document_with_style(style, request);
638 assert!(result.is_ok());
639 let res = result.unwrap();
640 assert_eq!(res.formatted_citations.len(), 0);
641 }
642
643 #[test]
644 fn format_document_missing_ref_warning() {
645 let style = make_test_style();
646 let refs = make_test_bibliography();
647
648 let citation_occ = CitationOccurrence {
649 id: "cite1".to_string(),
650 items: vec![CitationOccurrenceItem {
651 id: "unknown_ref".to_string(),
652 locator: None,
653 prefix: None,
654 suffix: None,
655 integral_name_state: None,
656 org_abbreviation_state: None,
657 }],
658 mode: None,
659 note_number: None,
660 suppress_author: None,
661 grouped: None,
662 prefix: None,
663 suffix: None,
664 };
665
666 let request = FormatDocumentRequest {
667 style: StyleInput::Yaml("dummy".to_string()),
668 locale: None,
669 output_format: OutputFormatKind::Plain,
670 refs,
671 citations: vec![citation_occ],
672 document_options: None,
673 };
674
675 let result = format_document_with_style(style, request);
676 assert!(result.is_ok());
677 let res = result.unwrap();
678 assert!(res.warnings.iter().any(|w| w.code == "missing_ref"));
679 }
680
681 #[test]
682 fn format_document_unknown_reference_class_warning() {
683 let style = make_test_style();
684 let mut refs = Bibliography::new();
685 let unknown_ref: InputReference = serde_json::from_str(
686 r#"{
687 "class": "dance-performance",
688 "id": "pina2011",
689 "title": "Pina",
690 "issued": "2011",
691 "venue": "Berlin"
692 }"#,
693 )
694 .expect("unknown class should parse through the compatibility path");
695 refs.insert("pina2011".to_string(), unknown_ref);
696
697 let citation_occ = CitationOccurrence {
698 id: "cite1".to_string(),
699 items: vec![CitationOccurrenceItem {
700 id: "pina2011".to_string(),
701 locator: None,
702 prefix: None,
703 suffix: None,
704 integral_name_state: None,
705 org_abbreviation_state: None,
706 }],
707 mode: None,
708 note_number: None,
709 suppress_author: None,
710 grouped: None,
711 prefix: None,
712 suffix: None,
713 };
714
715 let request = FormatDocumentRequest {
716 style: StyleInput::Yaml("dummy".to_string()),
717 locale: None,
718 output_format: OutputFormatKind::Plain,
719 refs: RefsInput::Json(serde_json::to_value(refs).unwrap()),
720 citations: vec![citation_occ],
721 document_options: None,
722 };
723
724 let result = format_document_with_style(style, request).unwrap();
725 let warning = result
726 .warnings
727 .iter()
728 .find(|w| w.code == "unknown_reference_class")
729 .expect("unknown class warning should be emitted");
730 assert_eq!(warning.ref_id.as_deref(), Some("pina2011"));
731 assert!(warning.message.contains("dance-performance"));
732 }
733
734 #[test]
735 fn format_document_yaml_style_input() {
736 let style = make_test_style();
737 let yaml_style = serde_yaml::to_string(&style).expect("serialize test style");
738
739 let mut refs = Bibliography::new();
740 refs.insert(
741 "test2024".to_string(),
742 InputReference::Monograph(Box::new(Monograph {
743 id: Some("test2024".into()),
744 r#type: MonographType::Book,
745 title: Some(Title::Single("Test Work".to_string())),
746 issued: EdtfString("2024".to_string()),
747 ..Default::default()
748 })),
749 );
750
751 let citation_occ = CitationOccurrence {
752 id: "c1".to_string(),
753 items: vec![CitationOccurrenceItem {
754 id: "test2024".to_string(),
755 locator: None,
756 prefix: None,
757 suffix: None,
758 integral_name_state: None,
759 org_abbreviation_state: None,
760 }],
761 mode: None,
762 note_number: None,
763 suppress_author: None,
764 grouped: None,
765 prefix: None,
766 suffix: None,
767 };
768
769 let request = FormatDocumentRequest {
770 style: StyleInput::Yaml(yaml_style),
771 locale: None,
772 output_format: OutputFormatKind::Plain,
773 refs: RefsInput::Json(serde_json::to_value(refs).unwrap()),
774 citations: vec![citation_occ],
775 document_options: None,
776 };
777
778 let result = format_document(request);
779 assert!(result.is_ok());
780 let res = result.unwrap();
781 assert_eq!(res.formatted_citations.len(), 1);
782 assert!(!res.formatted_citations[0].text.is_empty());
783 }
784
785 #[test]
786 fn format_document_uri_input_unresolved() {
787 let request = FormatDocumentRequest {
788 style: StyleInput::Uri("https://example.com/style.yaml".to_string()),
789 locale: None,
790 output_format: OutputFormatKind::Plain,
791 refs: RefsInput::Json(serde_json::Value::Object(Default::default())),
792 citations: vec![],
793 document_options: None,
794 };
795
796 let result = format_document(request);
797 match result {
798 Err(FormatDocumentError::UnresolvedInput(_)) => {
799 }
801 _ => panic!("Expected UnresolvedInput error"),
802 }
803 }
804
805 struct MockResolver(Style);
807
808 impl citum_resolver_api::StyleResolver for MockResolver {
809 type Style = Style;
810 type Locale = citum_schema::locale::Locale;
811
812 fn resolve_style(&self, _uri: &str) -> Result<Style, citum_schema::ResolverError> {
813 Ok(self.0.clone())
814 }
815
816 fn resolve_locale(
817 &self,
818 id: &str,
819 ) -> Result<citum_schema::locale::Locale, citum_schema::ResolverError> {
820 Err(citum_schema::ResolverError::LocaleNotFound(
821 std::borrow::Cow::Owned(id.to_string()),
822 ))
823 }
824 }
825
826 #[test]
827 fn format_document_with_resolver_injects_style_for_id_input() {
828 let style = make_test_style();
829 let resolver = MockResolver(style);
830 let refs = make_test_bibliography();
831
832 let citation_occ = CitationOccurrence {
833 id: "c1".to_string(),
834 items: vec![CitationOccurrenceItem {
835 id: "smith2020".to_string(),
836 locator: None,
837 prefix: None,
838 suffix: None,
839 integral_name_state: None,
840 org_abbreviation_state: None,
841 }],
842 mode: None,
843 note_number: None,
844 suppress_author: None,
845 grouped: None,
846 prefix: None,
847 suffix: None,
848 };
849
850 let request = FormatDocumentRequest {
851 style: StyleInput::Id("any-id".to_string()),
852 locale: None,
853 output_format: OutputFormatKind::Plain,
854 refs,
855 citations: vec![citation_occ],
856 document_options: None,
857 };
858
859 match format_document(request.clone()) {
861 Err(FormatDocumentError::UnresolvedInput(_)) => {}
862 other => panic!("expected UnresolvedInput without resolver, got: {other:?}"),
863 }
864
865 let result = format_document_with_resolver(request, &resolver);
867 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
868 let res = result.unwrap();
869 assert_eq!(res.formatted_citations.len(), 1);
870 assert!(
871 !res.formatted_citations[0].text.is_empty(),
872 "formatted citation text should not be empty"
873 );
874 }
875}