1use crate::processor::Processor;
16use crate::reference::Bibliography;
17use citum_schema::locale::{GeneralTerm, TermForm};
18use citum_schema::options::TermLocale;
19use citum_schema::reference::{
20 ClassExtension, CollectionType, ContributorRole as ReferenceRole, MonographComponentType,
21 MonographType, ReferenceClass, SerialComponentType,
22};
23use citum_schema::template::ContributorRole as TemplateRole;
24
25use super::{Warning, WarningLevel};
26
27pub fn term_locale_fallback_warnings(processor: &Processor) -> Vec<Warning> {
39 let term_locale_is_item = |config: &citum_schema::options::Config| {
40 config
41 .multilingual
42 .as_ref()
43 .is_some_and(|ml| ml.term_locale == TermLocale::Item)
44 };
45 let active = term_locale_is_item(&processor.get_citation_config())
46 || term_locale_is_item(&processor.get_bibliography_config());
47 if !active {
48 return Vec::new();
49 }
50
51 processor
52 .bibliography
53 .iter()
54 .filter_map(|(ref_id, reference)| {
55 let language = crate::values::effective_item_language(reference)?;
56 if crate::processor::rendering::lookup_embedded_locale(&language).is_some() {
57 return None;
58 }
59 Some(Warning {
60 level: WarningLevel::Warning,
61 code: "term_locale_unavailable".to_string(),
62 citation_id: None,
63 ref_id: Some(ref_id.clone()),
64 message: format!(
65 "Reference '{ref_id}' has language '{language}' but no loaded locale matches it; \
66 term-locale: item falls back to the style locale for this reference's terms."
67 ),
68 })
69 })
70 .collect()
71}
72
73pub fn unknown_reference_class_warnings(bibliography: &Bibliography) -> Vec<Warning> {
75 bibliography
76 .iter()
77 .filter_map(|(ref_id, reference)| {
78 let ReferenceClass::Unknown(class) = reference.class() else {
79 return None;
80 };
81 Some(Warning {
82 level: WarningLevel::Warning,
83 code: "unknown_reference_class".to_string(),
84 citation_id: None,
85 ref_id: Some(ref_id.clone()),
86 message: format!(
87 "Reference '{ref_id}' uses unknown class '{class}'; rendering will use only fields this engine understands."
88 ),
89 })
90 })
91 .collect()
92}
93
94pub fn unknown_reference_field_warnings(bibliography: &Bibliography) -> Vec<Warning> {
100 bibliography
101 .iter()
102 .filter_map(|(ref_id, reference)| {
103 let unknown = reference.unknown_fields()?;
104 if unknown.is_empty() {
105 return None;
106 }
107 let keys: Vec<&str> = unknown.keys().map(String::as_str).collect();
108 Some(Warning {
109 level: WarningLevel::Warning,
110 code: "unknown_reference_field".to_string(),
111 citation_id: None,
112 ref_id: Some(ref_id.clone()),
113 message: format!(
114 "Reference '{ref_id}' has unknown field(s): {}; these fields are ignored during rendering.",
115 keys.join(", ")
116 ),
117 })
118 })
119 .collect()
120}
121
122pub fn unknown_enum_warnings(processor: &Processor) -> Vec<Warning> {
127 let mut warnings = Vec::new();
128
129 for (ref_id, reference) in &processor.bibliography {
131 match reference.extension() {
132 ClassExtension::Monograph(r) => {
133 if let MonographType::Unknown(s) = &r.r#type {
134 warnings.push(Warning {
135 level: WarningLevel::Warning,
136 code: "unknown_enum_variant".to_string(),
137 citation_id: None,
138 ref_id: Some(ref_id.clone()),
139 message: format!("Reference '{ref_id}' uses unknown monograph type '{s}'; rendering will use default monograph formatting."),
140 });
141 }
142 }
143 ClassExtension::Collection(r) => {
144 if let CollectionType::Unknown(s) = &r.r#type {
145 warnings.push(Warning {
146 level: WarningLevel::Warning,
147 code: "unknown_enum_variant".to_string(),
148 citation_id: None,
149 ref_id: Some(ref_id.clone()),
150 message: format!("Reference '{ref_id}' uses unknown collection type '{s}'; rendering will use default collection formatting."),
151 });
152 }
153 }
154 ClassExtension::CollectionComponent(r) => {
155 if let MonographComponentType::Unknown(s) = &r.r#type {
156 warnings.push(Warning {
157 level: WarningLevel::Warning,
158 code: "unknown_enum_variant".to_string(),
159 citation_id: None,
160 ref_id: Some(ref_id.clone()),
161 message: format!("Reference '{ref_id}' uses unknown monograph component type '{s}'; rendering will use default chapter formatting."),
162 });
163 }
164 }
165 ClassExtension::SerialComponent(r) => {
166 if let SerialComponentType::Unknown(s) = &r.r#type {
167 warnings.push(Warning {
168 level: WarningLevel::Warning,
169 code: "unknown_enum_variant".to_string(),
170 citation_id: None,
171 ref_id: Some(ref_id.clone()),
172 message: format!("Reference '{ref_id}' uses unknown serial component type '{s}'; rendering will use default article formatting."),
173 });
174 }
175 }
176 _ => {}
177 }
178
179 for contributor in reference.all_contributor_entries() {
180 for role in contributor.roles.as_slice() {
181 if let ReferenceRole::Unknown(s) = role {
182 warnings.push(Warning {
183 level: WarningLevel::Warning,
184 code: "unknown_enum_variant".to_string(),
185 citation_id: None,
186 ref_id: Some(ref_id.clone()),
187 message: format!("Reference '{ref_id}' uses unknown contributor role '{s}'; this role may be ignored during rendering."),
188 });
189 }
190 }
191 }
192 }
193
194 if let Some(templates) = &processor.style.templates {
196 for (name, template) in templates {
197 scan_template_for_unknowns(template, &format!("template '{name}'"), &mut warnings);
198 }
199 }
200 if let Some(citation) = &processor.style.citation {
201 scan_citation_spec_for_unknowns(citation, "citation layout", &mut warnings);
202 }
203 if let Some(bib) = &processor.style.bibliography {
204 if let Some(template) = &bib.template {
205 scan_template_for_unknowns(template, "bibliography layout", &mut warnings);
206 }
207 if let Some(type_variants) = &bib.type_variants {
208 for variant in type_variants.values() {
209 if let Some(template) = variant.as_template() {
210 scan_template_for_unknowns(template, "bibliography layout", &mut warnings);
211 }
212 }
213 }
214 if let Some(locales) = &bib.locales {
215 for locale_spec in locales {
216 scan_template_for_unknowns(
217 &locale_spec.template,
218 "bibliography layout",
219 &mut warnings,
220 );
221 }
222 }
223 }
224 scan_bibliography_config_sort_for_citation_number(processor, &mut warnings);
225
226 warnings
227}
228
229fn scan_bibliography_config_sort_for_citation_number(
236 processor: &Processor,
237 warnings: &mut Vec<Warning>,
238) {
239 let Some(citum_schema::options::SortEntry::Explicit(sort)) = processor
240 .get_bibliography_config()
241 .processing
242 .as_ref()
243 .map(citum_schema::options::Processing::config)
244 .and_then(|config| config.sort)
245 else {
246 return;
247 };
248
249 let uses_citation_number = sort
250 .template
251 .iter()
252 .any(|spec| matches!(spec.key, citum_schema::options::SortKey::CitationNumber));
253
254 if uses_citation_number {
255 warnings.push(Warning {
256 level: WarningLevel::Warning,
257 code: "citation_number_sort_not_supported".to_string(),
258 citation_id: None,
259 ref_id: None,
260 message: "Style bibliography configuration lists 'citation-number' as an explicit \
261 sort key; it is not supported and is ignored for bibliography ordering."
262 .to_string(),
263 });
264 }
265}
266
267fn scan_citation_spec_for_unknowns(
275 spec: &citum_schema::CitationSpec,
276 location: &str,
277 warnings: &mut Vec<Warning>,
278) {
279 if let Some(template) = &spec.template {
280 scan_template_for_unknowns(template, location, warnings);
281 }
282 if let Some(type_variants) = &spec.type_variants {
283 for variant in type_variants.values() {
284 if let Some(template) = variant.as_template() {
285 scan_template_for_unknowns(template, location, warnings);
286 }
287 }
288 }
289 if let Some(locales) = &spec.locales {
290 for locale_spec in locales {
291 scan_template_for_unknowns(&locale_spec.template, location, warnings);
292 }
293 }
294
295 if let Some(child) = &spec.integral {
296 scan_citation_spec_for_unknowns(child, &format!("{location} (integral)"), warnings);
297 }
298 if let Some(child) = &spec.non_integral {
299 scan_citation_spec_for_unknowns(child, &format!("{location} (non-integral)"), warnings);
300 }
301 if let Some(child) = &spec.subsequent {
302 scan_citation_spec_for_unknowns(child, &format!("{location} (subsequent)"), warnings);
303 }
304 if let Some(child) = &spec.ibid {
305 scan_citation_spec_for_unknowns(child, &format!("{location} (ibid)"), warnings);
306 }
307}
308
309fn scan_template_for_unknowns(
310 components: &[citum_schema::template::TemplateComponent],
311 location: &str,
312 warnings: &mut Vec<Warning>,
313) {
314 use citum_schema::template::TemplateComponent;
315 for component in components {
316 match component {
317 TemplateComponent::Term(t) => {
318 if let GeneralTerm::Unknown(s) = &t.term {
319 warnings.push(Warning {
320 level: WarningLevel::Warning,
321 code: "unknown_enum_variant".to_string(),
322 citation_id: None,
323 ref_id: None,
324 message: format!("Style {location} uses unknown locale term key '{s}'; this term may render as empty."),
325 });
326 }
327 if let Some(TermForm::Unknown(s)) = &t.form {
328 warnings.push(Warning {
329 level: WarningLevel::Warning,
330 code: "unknown_enum_variant".to_string(),
331 citation_id: None,
332 ref_id: None,
333 message: format!("Style {location} uses unknown term form '{s}'; falling back to long form."),
334 });
335 }
336 }
337 TemplateComponent::Contributor(c) => {
338 for role in c.contributor.as_slice() {
339 if let TemplateRole::Unknown(s) = role {
340 warnings.push(Warning {
341 level: WarningLevel::Warning,
342 code: "unknown_enum_variant".to_string(),
343 citation_id: None,
344 ref_id: None,
345 message: format!("Style {location} uses unknown contributor role '{s}'; this role may be ignored."),
346 });
347 }
348 }
349 if let Some(label) = &c.label {
350 let term = label.term.as_str();
351 if !crate::values::contributor::labels::RECOGNIZED_LABEL_TERMS.contains(&term) {
352 warnings.push(Warning {
353 level: WarningLevel::Warning,
354 code: "unknown_role_label_term".to_string(),
355 citation_id: None,
356 ref_id: None,
357 message: format!("Style {location} uses unrecognized role-label term '{term}'; falling back to the contributor's own role term instead of the requested one."),
358 });
359 }
360 }
361 }
362 TemplateComponent::Date(d) => {
363 if let citum_schema::template::DateForm::Unknown(s) = &d.form {
364 warnings.push(Warning {
365 level: WarningLevel::Warning,
366 code: "unknown_enum_variant".to_string(),
367 citation_id: None,
368 ref_id: None,
369 message: format!("Style {location} uses unknown date form '{s}'; falling back to year only."),
370 });
371 }
372 }
373 TemplateComponent::Group(g) => {
374 scan_template_for_unknowns(&g.group, location, warnings);
375 }
376 _ => {}
377 }
378 }
379}
380
381#[cfg(test)]
382#[allow(clippy::unwrap_used, reason = "tests")]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn unknown_enum_warnings_reports_unknown_term_in_integral_sub_spec() {
388 let yaml = "info:\n title: Test\ncitation:\n integral:\n template:\n - term: not-a-real-term\n";
389 let style = citum_schema::Style::from_yaml_str(yaml).unwrap();
390 let processor = Processor::new(style, Bibliography::new());
391
392 let warnings = unknown_enum_warnings(&processor);
393 assert!(
394 warnings
395 .iter()
396 .any(|w| w.message.contains("not-a-real-term") && w.message.contains("(integral)")),
397 "expected a warning for the unknown term in citation.integral.template, got: {warnings:?}"
398 );
399 }
400
401 #[test]
402 fn unknown_enum_warnings_reports_unknown_role_label_term() {
403 let yaml = "info:\n title: Test\nbibliography:\n template:\n - contributor: editor\n form: long\n label: {term: not-a-real-role}\n";
404 let style = citum_schema::Style::from_yaml_str(yaml).unwrap();
405 let processor = Processor::new(style, Bibliography::new());
406
407 let warnings = unknown_enum_warnings(&processor);
408 assert!(
409 warnings
410 .iter()
411 .any(|w| w.code == "unknown_role_label_term"
412 && w.message.contains("not-a-real-role")),
413 "expected a warning for the unrecognized role-label term, got: {warnings:?}"
414 );
415 }
416
417 #[test]
418 fn unknown_enum_warnings_does_not_flag_recognized_role_label_terms() {
419 let yaml = "info:\n title: Test\nbibliography:\n template:\n - contributor: editor\n form: long\n label: {term: editor}\n";
420 let style = citum_schema::Style::from_yaml_str(yaml).unwrap();
421 let processor = Processor::new(style, Bibliography::new());
422
423 let warnings = unknown_enum_warnings(&processor);
424 assert!(
425 !warnings.iter().any(|w| w.code == "unknown_role_label_term"),
426 "did not expect a warning for a recognized role-label term, got: {warnings:?}"
427 );
428 }
429
430 #[test]
431 fn unknown_enum_warnings_reports_unknown_term_in_type_variants() {
432 let yaml = "info:\n title: Test\ncitation:\n type-variants:\n book:\n - term: not-a-real-term-2\n";
433 let style = citum_schema::Style::from_yaml_str(yaml).unwrap();
434 let processor = Processor::new(style, Bibliography::new());
435
436 let warnings = unknown_enum_warnings(&processor);
437 assert!(
438 warnings
439 .iter()
440 .any(|w| w.message.contains("not-a-real-term-2")),
441 "expected a warning for the unknown term in citation.type-variants, got: {warnings:?}"
442 );
443 }
444}