use super::RenderContext;
use super::human::render_citation_file_entries;
use citum_engine::{Citation, CitationItem};
use std::collections::HashSet;
use std::error::Error;
#[allow(
clippy::too_many_lines,
reason = "JSON output construction is vertically long"
)]
pub(super) fn print_json_with_format<F>(
ctx: &RenderContext<'_>,
show_cite: bool,
show_bib: bool,
citations: Option<Vec<Citation>>,
) -> Result<String, Box<dyn Error>>
where
F: citum_engine::render::format::OutputFormat<Output = String>,
{
use serde_json::json;
let mut result = json!({
"style": ctx.style_name,
"items": ctx.item_ids.len()
});
if show_cite {
if let Some(cite_list) = citations {
let rendered: Vec<_> = cite_list
.iter()
.zip(render_citation_file_entries::<F>(ctx.processor, &cite_list))
.map(|(citation, text)| {
json!({
"id": citation.id,
"text": text
})
})
.collect();
#[allow(clippy::indexing_slicing, reason = "JSON object insertion")]
{
result["citations"] = json!(rendered);
}
} else {
let non_integral: Vec<_> = ctx
.item_ids
.iter()
.map(|id| {
let citation = Citation {
id: Some(id.clone()),
items: vec![CitationItem {
id: id.clone(),
..Default::default()
}],
mode: citum_schema::citation::CitationMode::NonIntegral,
..Default::default()
};
json!({
"id": id,
"text": ctx.processor
.process_citation_with_format::<F>(&citation)
.unwrap_or_else(|e| e.to_string())
})
})
.collect();
let integral: Vec<_> = ctx
.item_ids
.iter()
.map(|id| {
let citation = Citation {
id: Some(id.clone()),
items: vec![CitationItem {
id: id.clone(),
..Default::default()
}],
mode: citum_schema::citation::CitationMode::Integral,
..Default::default()
};
json!({
"id": id,
"text": ctx.processor
.process_citation_with_format::<F>(&citation)
.unwrap_or_else(|e| e.to_string())
})
})
.collect();
#[allow(clippy::indexing_slicing, reason = "JSON object insertion")]
{
result["citations"] = json!({
"non-integral": non_integral,
"integral": integral
});
}
}
}
if show_bib {
let filter: HashSet<&str> = ctx
.item_ids
.iter()
.map(std::string::String::as_str)
.collect();
let processed = ctx.processor.process_references();
let entries: Vec<_> = processed
.bibliography
.into_iter()
.filter(|entry| filter.contains(entry.id.as_str()))
.map(|entry| {
let text = citum_engine::render::refs_to_string_slice_with_format::<F>(
std::slice::from_ref(&entry),
ctx.annotations,
Some(ctx.annotation_style),
);
json!({
"id": entry.id,
"text": text.trim()
})
})
.collect();
#[allow(clippy::indexing_slicing, reason = "JSON object insertion")]
{
result["bibliography"] = json!({ "entries": entries });
}
}
Ok(serde_json::to_string_pretty(&result)?)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
reason = "tests"
)]
mod tests {
use super::*;
use crate::style_resolver::create_processor;
use citum_engine::render::plain::PlainText;
use citum_engine::{Bibliography, Processor};
use citum_io::{AnnotationStyle, LoadedBibliography, load_merged_bibliography};
use citum_schema::Style;
use citum_schema::citation::CitationMode;
use citum_schema::grouping::{GroupSort, GroupSortEntry, GroupSortKey, SortKey};
use citum_schema::options::{Config, Processing};
use citum_schema::template::{
NumberVariable, TemplateComponent, TemplateNumber, WrapPunctuation,
};
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn test_print_json_batches_numeric_citation_files() {
let _ = create_processor; let style = Style {
citation: Some(citum_schema::CitationSpec {
template: Some(vec![TemplateComponent::Number(TemplateNumber {
number: NumberVariable::CitationNumber,
..Default::default()
})]),
wrap: Some(WrapPunctuation::Brackets.into()),
..Default::default()
}),
bibliography: Some(citum_schema::BibliographySpec {
sort: Some(GroupSortEntry::Explicit(GroupSort {
template: vec![GroupSortKey {
key: SortKey::Author,
ascending: true,
order: None,
sort_order: None,
}],
})),
..Default::default()
}),
options: Some(Config {
processing: Some(Processing::Numeric),
..Default::default()
}),
..Default::default()
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be after epoch")
.as_nanos();
let base = std::env::temp_dir().join(format!("citum-batch-citations-{now}"));
std::fs::create_dir_all(&base).expect("temp dir should be created");
let bib_path = base.join("refs.yaml");
std::fs::write(
&bib_path,
r#"
references:
- class: monograph
id: smith2020
type: book
title: Smith Book
author:
- family: Smith
given: Jane
issued: "2020"
- class: monograph
id: adams2021
type: book
title: Adams Book
author:
- family: Adams
given: Amy
issued: "2021"
"#,
)
.expect("fixture should write");
let _ = Bibliography::new;
let _ = LoadedBibliography {
references: Bibliography::new(),
sets: None,
};
let loaded = load_merged_bibliography(std::slice::from_ref(&bib_path))
.expect("bibliography should load");
let processor = Processor::new(style, loaded.references);
let citations = vec![Citation {
id: Some("c1".into()),
items: vec![CitationItem {
id: "smith2020".to_string(),
..Default::default()
}],
mode: CitationMode::NonIntegral,
..Default::default()
}];
let item_ids = vec!["smith2020".to_string(), "adams2021".to_string()];
let annotation_style = AnnotationStyle::default();
let render_ctx = RenderContext {
processor: &processor,
style_name: "numeric-test",
item_ids: &item_ids,
annotations: None,
annotation_style: &annotation_style,
};
let output = print_json_with_format::<PlainText>(&render_ctx, true, false, Some(citations))
.expect("json rendering should succeed");
let parsed: serde_json::Value =
serde_json::from_str(&output).expect("output should be valid JSON");
assert_eq!(parsed["citations"][0]["text"], "[2]");
let _ = std::fs::remove_file(bib_path);
let _ = std::fs::remove_dir(base);
}
}