#![allow(missing_docs, reason = "test")]
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "Panicking is acceptable and often desired in test code."
)]
mod common;
use common::announce_behavior;
use citum_engine::Processor;
use citum_io::load_bibliography;
use citum_schema::citation::{Citation, CitationItem};
use citum_schema::options::{BibliographyOptions, DateConfig};
use citum_schema::reference::{
Contributor, ContributorEntry, ContributorRole, DateValue, InputReference, Monograph,
MonographType, StructuredName, Title,
};
use citum_schema::template::{
DateForm, DateVariable, TemplateComponent, TemplateDate, WrapConfig, WrapPunctuation,
};
use citum_schema::{BibliographySpec, Style, StyleInfo};
use indexmap::IndexMap;
use rstest::rstest;
use std::path::PathBuf;
fn zh_cn_locale() -> citum_schema::Locale {
let bytes = citum_schema::embedded::get_locale_bytes("zh-CN").expect("zh-CN must be embedded");
citum_schema::Locale::from_yaml_str(std::str::from_utf8(bytes).expect("valid UTF-8"))
.expect("zh-CN locale should parse")
}
fn project_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}
fn render_pinned_gbt_entry(style_name: &str, ref_id: &str) -> String {
let corpus_path = project_root().join("tests/fixtures/test-items-library/gb-t-7714-2025.json");
let corpus = load_bibliography(&corpus_path).expect("pinned GB/T corpus should load");
let reference = corpus
.get(ref_id)
.unwrap_or_else(|| panic!("pinned GB/T corpus should contain {ref_id}"))
.clone();
let bibliography = IndexMap::from([(ref_id.to_string(), reference)]);
let style = citum_schema::embedded::get_embedded_style(style_name)
.unwrap_or_else(|| panic!("{style_name} should be embedded"))
.unwrap_or_else(|_| panic!("{style_name} should parse"))
.into_resolved();
Processor::with_locale(style, bibliography, zh_cn_locale()).render_bibliography()
}
fn annotated_book(id: &str, title: &str, year: &str, note: &str) -> InputReference {
InputReference::Monograph(Box::new(Monograph {
id: Some(id.into()),
r#type: MonographType::Book,
title: Some(Title::Single(title.to_string())),
issued: DateValue {
value: year.to_string(),
note: Some(note.to_string()),
},
..Default::default()
}))
}
fn annotated_book_by(
id: &str,
title: &str,
author_family: &str,
year: &str,
note: &str,
) -> InputReference {
InputReference::Monograph(Box::new(Monograph {
id: Some(id.into()),
r#type: MonographType::Book,
title: Some(Title::Single(title.to_string())),
contributors: vec![ContributorEntry {
roles: ContributorRole::Author.into(),
contributor: Contributor::StructuredName(StructuredName {
family: author_family.into(),
..Default::default()
}),
gender: None,
}],
issued: DateValue {
value: year.to_string(),
note: Some(note.to_string()),
},
..Default::default()
}))
}
fn style_with_duplicated_issued_date() -> Style {
Style {
info: StyleInfo {
title: Some("Duplicated Issued Date".to_string()),
..Default::default()
},
bibliography: Some(BibliographySpec {
options: Some(BibliographyOptions {
dates: Some(DateConfig {
note_wrap: Some(WrapConfig {
punctuation: WrapPunctuation::Parentheses,
inner_prefix: None,
inner_suffix: None,
}),
..Default::default()
}),
..Default::default()
}),
template: Some(vec![
TemplateComponent::Date(TemplateDate {
date: DateVariable::Issued,
form: DateForm::Year,
..Default::default()
}),
TemplateComponent::Date(TemplateDate {
date: DateVariable::Issued,
form: DateForm::Year,
suppress_note: Some(true),
..Default::default()
}),
]),
..Default::default()
}),
..Default::default()
}
.into_resolved()
}
#[test]
fn given_component_with_suppress_note_when_date_renders_twice_then_note_appears_once() {
announce_behavior(
"A template rendering the same date variable twice per item wraps the \
calendar note on the occurrence without `suppress-note: true` only — \
the second, redundant occurrence renders bare, even though the \
section's `note-wrap` is configured and the date carries a note on \
both renders (it's the same underlying date value) — csl26-gl0n.",
);
let style = style_with_duplicated_issued_date();
let bibliography = IndexMap::from([(
"minguo-1947".to_string(),
annotated_book("minguo-1947", "戰後臺灣史", "1947", "民国三十六年"),
)]);
let processor = Processor::new(style, bibliography);
let rendered = processor.render_bibliography();
assert_eq!(rendered, "1947(民国三十六年). 1947");
}
fn single_item_citation(id: &str) -> Citation {
Citation {
items: vec![CitationItem {
id: id.to_string(),
..Default::default()
}],
..Default::default()
}
}
#[test]
fn given_gb_t_author_date_style_when_rendering_bibliography_then_calendar_note_is_wrapped() {
announce_behavior(
"GB/T 7714 author-date wraps an issued date's opaque note (e.g. a Minguo \
calendar annotation) in the bibliography, per the style's `note-wrap: \
parentheses` bibliography-scoped `dates` option — csl26-epu6.",
);
let style = citum_schema::embedded::get_embedded_style("gb-t-7714-2025-author-date")
.expect("gb-t-7714-2025-author-date should be embedded")
.expect("gb-t-7714-2025-author-date should parse")
.into_resolved();
let bibliography = IndexMap::from([(
"minguo-1947".to_string(),
annotated_book("minguo-1947", "戰後臺灣史", "1947", "民国三十六年"),
)]);
let processor = Processor::with_locale(style, bibliography, zh_cn_locale());
let rendered = processor.render_bibliography();
assert_eq!(rendered, "佚名,1947(民国三十六年). 戰後臺灣史[M].");
}
#[test]
fn given_gb_t_author_date_style_when_rendering_citation_then_calendar_note_is_absent() {
announce_behavior(
"GB/T 7714 author-date citations carry no `note-wrap` option (it is set only \
under bibliography.options.dates), so an annotated date's note never appears \
in citation output — bibliography-only scoping, csl26-epu6.",
);
let style = citum_schema::embedded::get_embedded_style("gb-t-7714-2025-author-date")
.expect("gb-t-7714-2025-author-date should be embedded")
.expect("gb-t-7714-2025-author-date should parse")
.into_resolved();
let bibliography = IndexMap::from([(
"minguo-1947".to_string(),
annotated_book("minguo-1947", "戰後臺灣史", "1947", "民国三十六年"),
)]);
let processor = Processor::new(style, bibliography);
let citation = processor
.process_citation(&single_item_citation("minguo-1947"))
.expect("annotated-date citation should render");
assert_eq!(citation, "(“戰後臺灣史”,1947)");
}
#[test]
fn given_two_refs_same_author_year_different_note_when_disambiguating_then_still_collide() {
announce_behavior(
"A date's opaque note never enters author-date collision grouping — two \
references by the same author with the same issued year but different \
calendar notes still collide and receive year-suffix disambiguators, exactly \
as if neither carried a note. `year()`/`effective_issued_date` read only \
`value`; `note` is invisible to `build_group_key` — csl26-epu6.",
);
let style = citum_schema::embedded::get_embedded_style("gb-t-7714-2025-author-date")
.expect("gb-t-7714-2025-author-date should be embedded")
.expect("gb-t-7714-2025-author-date should parse")
.into_resolved();
let bibliography = IndexMap::from([
(
"kang-a".to_string(),
annotated_book_by("kang-a", "First Work", "Kang", "1947", "民国三十六年"),
),
(
"kang-b".to_string(),
annotated_book_by("kang-b", "Second Work", "Kang", "1947", "不同的注释"),
),
]);
let processor = Processor::new(style, bibliography);
let rendered = processor.render_bibliography();
assert_eq!(
rendered,
"Kang,1947a(民国三十六年). First Work[M].\n\n\
Kang,1947b(不同的注释). Second Work[M]."
);
}
#[rstest]
#[case::gb_t_7714_2025_author_date_minguo(
"gb-t-7714-2025-author-date",
"gbt7714.7.5.4.1:1",
"佚名,1947(民国三十六年). [M]."
)]
#[case::gb_t_7714_2025_author_date_kangxi(
"gb-t-7714-2025-author-date",
"gbt7714.7.5.4.1:2",
"佚名,1705(康熙四十四年). [M]."
)]
#[case::gb_t_7714_2025_numeric_minguo(
"gb-t-7714-2025-numeric",
"gbt7714.7.5.4.1:1",
"[1][M]. 1947(民国三十六年)."
)]
#[case::gb_t_7714_2025_numeric_kangxi(
"gb-t-7714-2025-numeric",
"gbt7714.7.5.4.1:2",
"[1][M]. 1705(康熙四十四年)."
)]
#[case::gb_t_7714_2025_note_minguo(
"gb-t-7714-2025-note",
"gbt7714.7.5.4.1:1",
"[1][M]. 1947(民国三十六年)."
)]
#[case::gb_t_7714_2025_note_kangxi(
"gb-t-7714-2025-note",
"gbt7714.7.5.4.1:2",
"[1][M]. 1705(康熙四十四年)."
)]
#[case::gb_t_7714_2025_author_date_qing_dynasty(
"gb-t-7714-2025-author-date",
"gbt7714.8.2.2:2",
"王夫之,1865(清同治四年). 宋论[M]. 刻本. 金陵:湘乡曾国荃."
)]
#[case::gb_t_7714_2025_author_date_guangxu_era(
"gb-t-7714-2025-author-date",
"gbt7714.8.12.3:1",
"李鸿章,1887. 奏请上海道库洋务外销要款无款可筹仍拨药厘接济事:04-01-35-0399-039[A]. 北京:中国第一历史档案馆,1887(光绪十三年三月十三日)."
)]
#[case::gb_t_7714_2025_author_date_republic_era(
"gb-t-7714-2025-author-date",
"gbt7714.8.12.3:3",
"佚名,1949. 中国人民解放军武汉市军事管制委员会接管国立武汉大学的文告[A/OL]. 武汉:武汉大学档案馆,1949(中华民国三十八年八月). https://archive.whu.edu.cn/index/forwardView/20/51."
)]
fn given_pinned_gbt_record_when_rendering_bibliography_then_note_is_wrapped(
#[case] style_name: &str,
#[case] ref_id: &str,
#[case] expected: &str,
) {
let rendered = render_pinned_gbt_entry(style_name, ref_id);
assert_eq!(rendered, expected);
}