1use std::collections::{BTreeMap, BTreeSet};
22use std::cell::RefCell;
23
24use serde::{Deserialize, Serialize};
25
26use crate::types::{Block, Format};
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
34pub struct Author {
35 pub family: String,
37 pub given: Option<String>,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
44#[serde(rename_all = "lowercase")]
45pub enum RefType {
46 #[default]
48 Article,
49 Book,
51 Chapter,
53 Web,
55 Report,
57 Conference,
59}
60
61impl RefType {
62 pub fn from_str_lossy(s: &str) -> RefType {
64 match s.trim().to_ascii_lowercase().as_str() {
65 "book" => RefType::Book,
66 "chapter" | "incollection" | "bookchapter" => RefType::Chapter,
67 "web" | "website" | "online" | "webpage" => RefType::Web,
68 "report" | "techreport" | "whitepaper" => RefType::Report,
69 "conference" | "proceedings" | "inproceedings" | "paper" => RefType::Conference,
70 _ => RefType::Article,
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
78pub struct Reference {
79 pub key: String,
81 pub ref_type: RefType,
83 pub authors: Vec<Author>,
85 pub editors: Vec<Author>,
87 pub title: Option<String>,
89 pub container: Option<String>,
91 pub publisher: Option<String>,
93 pub year: Option<String>,
95 pub volume: Option<String>,
97 pub issue: Option<String>,
99 pub pages: Option<String>,
101 pub url: Option<String>,
103 pub doi: Option<String>,
105 pub accessed: Option<String>,
107 pub edition: Option<String>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct CiteItem {
118 pub key: String,
120 pub locator: Option<String>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct CiteRef {
127 pub items: Vec<CiteItem>,
129}
130
131fn opt(s: &str) -> Option<String> {
136 let t = s.trim();
137 if t.is_empty() {
138 None
139 } else {
140 Some(t.to_string())
141 }
142}
143
144pub fn parse_author(s: &str) -> Author {
147 let s = s.trim();
148 if let Some((last, first)) = s.split_once(',') {
149 Author {
150 family: last.trim().to_string(),
151 given: opt(first),
152 }
153 } else if let Some(idx) = s.rfind(' ') {
154 let (given, family) = s.split_at(idx);
155 Author {
156 family: family.trim().to_string(),
157 given: opt(given),
158 }
159 } else {
160 Author {
161 family: s.to_string(),
162 given: None,
163 }
164 }
165}
166
167pub fn parse_authors(s: &str) -> Vec<Author> {
169 s.split(';')
170 .map(parse_author)
171 .filter(|a| !a.family.is_empty())
172 .collect()
173}
174
175fn initials(given: &Option<String>) -> String {
177 match given {
178 None => String::new(),
179 Some(g) => g
180 .split_whitespace()
181 .filter_map(|w| w.chars().next())
182 .map(|c| format!("{}.", c.to_uppercase()))
183 .collect::<Vec<_>>()
184 .join(" "),
185 }
186}
187
188fn inverted(a: &Author) -> String {
190 match &a.given {
191 Some(g) => format!("{}, {}", a.family, g),
192 None => a.family.clone(),
193 }
194}
195
196fn natural(a: &Author) -> String {
198 match &a.given {
199 Some(g) => format!("{} {}", g, a.family),
200 None => a.family.clone(),
201 }
202}
203
204fn strip_page_prefix(s: &str) -> String {
207 let t = s.trim();
208 for p in ["pp.", "pages", "page", "pg.", "p."] {
209 if let Some(rest) = t.strip_prefix(p) {
210 return rest.trim().to_string();
211 }
212 }
213 t.to_string()
214}
215
216fn fmt_date_mla(iso: &str) -> String {
219 const MONTHS: [&str; 12] = [
220 "January", "February", "March", "April", "May", "June", "July", "August",
221 "September", "October", "November", "December",
222 ];
223 let parts: Vec<&str> = iso.split('-').collect();
224 if parts.len() == 3 {
225 if let (Ok(y), Ok(m), Ok(d)) = (
226 parts[0].parse::<i32>(),
227 parts[1].parse::<usize>(),
228 parts[2].parse::<i32>(),
229 ) {
230 if (1..=12).contains(&m) {
231 return format!("{} {} {}", d, MONTHS[m - 1], y);
232 }
233 }
234 }
236 iso.to_string()
237}
238
239pub fn is_numbered(style: Format) -> bool {
246 matches!(style, Format::Ieee | Format::Acm)
247}
248
249pub fn bibliography_heading(style: Format) -> &'static str {
251 match style {
252 Format::Mla => "Works Cited",
253 Format::Chicago => "Bibliography",
254 _ => "References",
256 }
257}
258
259pub fn active_style(format: Option<Format>) -> Format {
261 format.unwrap_or(Format::Apa)
262}
263
264fn intext_names(r: &Reference, style: Format) -> String {
270 let fams: Vec<&str> = r.authors.iter().map(|a| a.family.as_str()).collect();
271 let amp = matches!(style, Format::Apa);
272 match fams.len() {
273 0 => r.title.clone().unwrap_or_else(|| r.key.clone()),
274 1 => fams[0].to_string(),
275 2 => {
276 if amp {
277 format!("{} & {}", fams[0], fams[1])
278 } else {
279 format!("{} and {}", fams[0], fams[1])
280 }
281 }
282 _ => format!("{} et al.", fams[0]),
283 }
284}
285
286pub fn format_in_text(
300 refs: &[Reference],
301 cite: &CiteRef,
302 style: Format,
303 numbers: &BTreeMap<String, usize>,
304) -> String {
305 if is_numbered(style) {
306 let parts: Vec<String> = cite
307 .items
308 .iter()
309 .map(|it| {
310 let n = numbers.get(&it.key).copied().unwrap_or(0);
311 match &it.locator {
312 Some(loc) => format!("[{}, {}]", n, loc),
313 None => format!("[{}]", n),
314 }
315 })
316 .collect();
317 return parts.join(", ");
318 }
319
320 let find = |key: &str| refs.iter().find(|r| r.key == key);
321 let parts: Vec<String> = cite
322 .items
323 .iter()
324 .map(|it| {
325 let r = find(&it.key);
326 let names = r
327 .map(|r| intext_names(r, style))
328 .unwrap_or_else(|| it.key.clone());
329 match style {
330 Format::Mla => {
331 match &it.locator {
333 Some(loc) => format!("{} {}", names, strip_page_prefix(loc)),
334 None => names,
335 }
336 }
337 Format::Chicago => {
338 let year = r
340 .and_then(|r| r.year.clone())
341 .unwrap_or_else(|| "n.d.".to_string());
342 match &it.locator {
343 Some(loc) => format!("{} {}, {}", names, year, strip_page_prefix(loc)),
344 None => format!("{} {}", names, year),
345 }
346 }
347 _ => {
349 let year = r
350 .and_then(|r| r.year.clone())
351 .unwrap_or_else(|| "n.d.".to_string());
352 match &it.locator {
353 Some(loc) => format!("{}, {}, {}", names, year, loc),
354 None => format!("{}, {}", names, year),
355 }
356 }
357 }
358 })
359 .collect();
360 format!("({})", parts.join("; "))
361}
362
363fn join_apa(names: &[String]) -> String {
371 match names.len() {
372 0 => String::new(),
373 1 => names[0].clone(),
374 2 => format!("{}, & {}", names[0], names[1]),
375 _ => {
376 let (last, rest) = names.split_last().unwrap();
377 format!("{}, & {}", rest.join(", "), last)
378 }
379 }
380}
381
382fn join_and(parts: &[String]) -> String {
383 match parts.len() {
384 0 => String::new(),
385 1 => parts[0].clone(),
386 2 => format!("{}, and {}", parts[0], parts[1]),
387 _ => {
388 let (last, rest) = parts.split_last().unwrap();
389 format!("{}, and {}", rest.join(", "), last)
390 }
391 }
392}
393
394fn apa_authors(authors: &[Author]) -> String {
395 let names: Vec<String> = authors
396 .iter()
397 .map(|a| {
398 let ini = initials(&a.given);
399 if ini.is_empty() {
400 a.family.clone()
401 } else {
402 format!("{}, {}", a.family, ini)
403 }
404 })
405 .collect();
406 join_apa(&names)
407}
408
409fn mla_authors(authors: &[Author]) -> String {
410 match authors.len() {
411 0 => String::new(),
412 1 => inverted(&authors[0]),
413 2 => format!("{}, and {}", inverted(&authors[0]), natural(&authors[1])),
414 _ => format!("{}, et al.", inverted(&authors[0])),
415 }
416}
417
418fn chicago_authors(authors: &[Author]) -> String {
419 match authors.len() {
420 0 => String::new(),
421 1 => inverted(&authors[0]),
422 _ => {
423 let mut parts = vec![inverted(&authors[0])];
424 for a in &authors[1..] {
425 parts.push(natural(a));
426 }
427 join_and(&parts)
428 }
429 }
430}
431
432fn ieee_authors(authors: &[Author]) -> String {
433 let names: Vec<String> = authors
434 .iter()
435 .map(|a| {
436 let ini = initials(&a.given);
437 if ini.is_empty() {
438 a.family.clone()
439 } else {
440 format!("{} {}", ini, a.family)
441 }
442 })
443 .collect();
444 match names.len() {
445 0 => String::new(),
446 1 => names[0].clone(),
447 2 => format!("{} and {}", names[0], names[1]),
448 _ => {
449 let (last, rest) = names.split_last().unwrap();
450 format!("{}, and {}", rest.join(", "), last)
451 }
452 }
453}
454
455fn apa_reference(r: &Reference) -> String {
456 let authors = apa_authors(&r.authors);
457 let year = r.year.clone().unwrap_or_else(|| "n.d.".to_string());
458 let head = if authors.is_empty() {
459 format!("({}).", year)
460 } else {
461 format!("{} ({}).", authors, year)
462 };
463 let title = r.title.clone().unwrap_or_default();
464 match r.ref_type {
465 RefType::Book => {
466 let mut s = format!("{} *{}*.", head, title);
467 if let Some(p) = &r.publisher {
468 s.push_str(&format!(" {}.", p));
469 }
470 s
471 }
472 _ => {
473 let mut s = head;
474 if !title.is_empty() {
475 s.push_str(&format!(" {}.", title));
476 }
477 if let Some(c) = &r.container {
478 s.push_str(&format!(" *{}*", c));
479 if let Some(v) = &r.volume {
480 s.push_str(&format!(", *{}*", v));
481 if let Some(i) = &r.issue {
482 s.push_str(&format!("({})", i));
483 }
484 }
485 if let Some(pg) = &r.pages {
486 s.push_str(&format!(", {}", pg));
487 }
488 s.push('.');
489 }
490 if let Some(d) = &r.doi {
491 s.push_str(&format!(" https://doi.org/{}", d));
492 } else if let Some(u) = &r.url {
493 s.push_str(&format!(" {}", u));
494 }
495 s
496 }
497 }
498}
499
500fn mla_reference(r: &Reference) -> String {
501 let authors = mla_authors(&r.authors);
502 let mut s = String::new();
503 if !authors.is_empty() {
504 if authors.ends_with('.') {
506 s.push_str(&format!("{} ", authors));
507 } else {
508 s.push_str(&format!("{}. ", authors));
509 }
510 }
511 let title = r.title.clone().unwrap_or_default();
512 match r.ref_type {
513 RefType::Book => {
514 s.push_str(&format!("*{}*.", title));
515 if let Some(p) = &r.publisher {
516 s.push_str(&format!(" {},", p));
517 }
518 if let Some(y) = &r.year {
519 s.push_str(&format!(" {}.", y));
520 }
521 }
522 _ => {
523 if !title.is_empty() {
524 s.push_str(&format!("\"{}.\" ", title));
525 }
526 if let Some(c) = &r.container {
527 s.push_str(&format!("*{}*", c));
528 let mut parts: Vec<String> = Vec::new();
529 if let Some(v) = &r.volume {
530 parts.push(format!("vol. {}", v));
531 }
532 if let Some(i) = &r.issue {
533 parts.push(format!("no. {}", i));
534 }
535 if let Some(y) = &r.year {
536 parts.push(y.clone());
537 }
538 if let Some(pg) = &r.pages {
539 parts.push(format!("pp. {}", pg));
540 }
541 if matches!(r.ref_type, RefType::Web) {
542 if let Some(u) = &r.url {
543 parts.push(u.clone());
544 }
545 }
546 for p in parts {
547 s.push_str(&format!(", {}", p));
548 }
549 s.push('.');
550 }
551 if let Some(acc) = &r.accessed {
552 s.push_str(&format!(" Accessed {}.", fmt_date_mla(acc)));
553 }
554 }
555 }
556 s
557}
558
559fn chicago_reference(r: &Reference) -> String {
560 let authors = chicago_authors(&r.authors);
561 let year = r.year.clone().unwrap_or_else(|| "n.d.".to_string());
562 let head = if authors.is_empty() {
563 format!("{}.", year)
564 } else {
565 format!("{}. {}.", authors, year)
566 };
567 let title = r.title.clone().unwrap_or_default();
568 match r.ref_type {
569 RefType::Book => {
570 let mut s = format!("{} *{}*.", head, title);
571 if let Some(p) = &r.publisher {
572 s.push_str(&format!(" {}.", p));
573 }
574 s
575 }
576 _ => {
577 let mut s = format!("{} \"{}.\"", head, title);
578 if let Some(c) = &r.container {
579 s.push_str(&format!(" *{}*", c));
580 let has_vol = r.volume.is_some();
581 if let Some(v) = &r.volume {
582 s.push_str(&format!(" {}", v));
583 }
584 if let Some(i) = &r.issue {
585 s.push_str(&format!(" ({})", i));
586 }
587 if let Some(pg) = &r.pages {
588 if has_vol {
589 s.push_str(&format!(": {}", pg));
590 } else {
591 s.push_str(&format!(", {}", pg));
592 }
593 }
594 s.push('.');
595 }
596 if let Some(u) = &r.url {
597 s.push_str(&format!(" {}.", u));
598 }
599 s
600 }
601 }
602}
603
604fn ieee_reference(r: &Reference, number: usize) -> String {
605 let prefix = format!("[{}] ", number);
606 let authors = ieee_authors(&r.authors);
607 let title = r.title.clone().unwrap_or_default();
608 match r.ref_type {
609 RefType::Book => {
610 let mut s = format!("{}{}, *{}*.", prefix, authors, title);
611 if let Some(p) = &r.publisher {
612 s.push_str(&format!(" {},", p));
613 }
614 if let Some(y) = &r.year {
615 s.push_str(&format!(" {}.", y));
616 }
617 s
618 }
619 RefType::Conference => {
620 let mut s = format!("{}{}, \"{},\"", prefix, authors, title);
621 if let Some(c) = &r.container {
622 s.push_str(&format!(" in *{}*", c));
623 }
624 if let Some(y) = &r.year {
625 s.push_str(&format!(", {}", y));
626 }
627 if let Some(pg) = &r.pages {
628 s.push_str(&format!(", pp. {}", pg));
629 }
630 s.push('.');
631 s
632 }
633 RefType::Web => {
634 let mut s = format!("{}{}, \"{},\"", prefix, authors, title);
635 if let Some(c) = &r.container {
636 s.push_str(&format!(" *{}*", c));
637 }
638 if let Some(y) = &r.year {
639 s.push_str(&format!(", {}", y));
640 }
641 s.push_str(". [Online]. Available: ");
642 if let Some(u) = &r.url {
643 s.push_str(u);
644 } else if let Some(d) = &r.doi {
645 s.push_str(&format!("https://doi.org/{}", d));
646 }
647 s
648 }
649 _ => {
650 let mut s = format!("{}{}, \"{},\"", prefix, authors, title);
651 if let Some(c) = &r.container {
652 s.push_str(&format!(" *{}*", c));
653 }
654 if let Some(v) = &r.volume {
655 s.push_str(&format!(", vol. {}", v));
656 }
657 if let Some(i) = &r.issue {
658 s.push_str(&format!(", no. {}", i));
659 }
660 if let Some(pg) = &r.pages {
661 s.push_str(&format!(", pp. {}", pg));
662 }
663 if let Some(y) = &r.year {
664 s.push_str(&format!(", {}", y));
665 }
666 s.push('.');
667 s
668 }
669 }
670}
671
672pub fn format_reference(r: &Reference, style: Format, number: Option<usize>) -> String {
675 match style {
676 Format::Mla => mla_reference(r),
677 Format::Chicago => chicago_reference(r),
678 Format::Ieee | Format::Acm => ieee_reference(r, number.unwrap_or(0)),
679 _ => apa_reference(r),
681 }
682}
683
684fn sort_key(r: &Reference) -> (String, String, String) {
686 let fam = r
687 .authors
688 .first()
689 .map(|a| a.family.to_lowercase())
690 .unwrap_or_else(|| r.title.clone().unwrap_or_default().to_lowercase());
691 (
692 fam,
693 r.year.clone().unwrap_or_default(),
694 r.title.clone().unwrap_or_default().to_lowercase(),
695 )
696}
697
698pub fn reference_list_keyed(refs: &[Reference], style: Format) -> Vec<(String, String)> {
703 if is_numbered(style) {
704 refs.iter()
705 .enumerate()
706 .map(|(i, r)| (r.key.clone(), format_reference(r, style, Some(i + 1))))
707 .collect()
708 } else {
709 let mut sorted: Vec<&Reference> = refs.iter().collect();
710 sorted.sort_by(|a, b| sort_key(a).cmp(&sort_key(b)));
711 sorted
712 .iter()
713 .map(|r| (r.key.clone(), format_reference(r, style, None)))
714 .collect()
715 }
716}
717
718pub fn reference_list(refs: &[Reference], style: Format) -> Vec<String> {
720 reference_list_keyed(refs, style)
721 .into_iter()
722 .map(|(_, s)| s)
723 .collect()
724}
725
726#[derive(Debug, Clone)]
733pub struct CiteContext {
734 pub style: Format,
736 pub references: Vec<Reference>,
738 pub numbers: BTreeMap<String, usize>,
741}
742
743fn children_of(b: &Block) -> Option<&[Block]> {
745 match b {
746 Block::Page { children, .. }
747 | Block::Section { children, .. }
748 | Block::Slide { children, .. }
749 | Block::App { children, .. }
750 | Block::AppShell { children, .. }
751 | Block::Sidebar { children, .. }
752 | Block::Panel { children, .. }
753 | Block::TabContent { children, .. }
754 | Block::Drawer { children, .. }
755 | Block::Modal { children, .. } => Some(children),
756 _ => None,
757 }
758}
759
760fn cite_text_of(b: &Block) -> Option<&str> {
762 match b {
763 Block::Markdown { content, .. }
764 | Block::Callout { content, .. }
765 | Block::Summary { content, .. }
766 | Block::Quote { content, .. }
767 | Block::Section { content, .. }
768 | Block::Details { content, .. } => Some(content),
769 _ => None,
770 }
771}
772
773fn collect_refs_rec(blocks: &[Block], out: &mut Vec<Reference>) {
774 for b in blocks {
775 if let Block::Cite { reference, .. } = b {
776 out.push(reference.clone());
777 }
778 if let Some(children) = children_of(b) {
779 collect_refs_rec(children, out);
780 }
781 }
782}
783
784pub fn collect_references(blocks: &[Block]) -> Vec<Reference> {
787 let mut out = Vec::new();
788 collect_refs_rec(blocks, &mut out);
789 out
790}
791
792fn collect_keys_rec(blocks: &[Block], order: &mut Vec<String>, seen: &mut BTreeSet<String>) {
793 for b in blocks {
794 if let Some(text) = cite_text_of(b) {
795 for (_, _, cr) in crate::inline::find_inline_cites(text) {
796 for it in cr.items {
797 if seen.insert(it.key.clone()) {
798 order.push(it.key);
799 }
800 }
801 }
802 }
803 if let Some(children) = children_of(b) {
804 collect_keys_rec(children, order, seen);
805 }
806 }
807}
808
809pub fn collect_cited_keys(blocks: &[Block]) -> Vec<String> {
811 let mut order = Vec::new();
812 let mut seen = BTreeSet::new();
813 collect_keys_rec(blocks, &mut order, &mut seen);
814 order
815}
816
817fn assign_numbers(refs: &[Reference], order: &[String]) -> BTreeMap<String, usize> {
820 let mut nums = BTreeMap::new();
821 let mut n = 1usize;
822 for k in order {
823 if refs.iter().any(|r| &r.key == k) && !nums.contains_key(k) {
824 nums.insert(k.clone(), n);
825 n += 1;
826 }
827 }
828 for r in refs {
829 if !nums.contains_key(&r.key) {
830 nums.insert(r.key.clone(), n);
831 n += 1;
832 }
833 }
834 nums
835}
836
837pub fn build_context(blocks: &[Block], format: Option<Format>) -> CiteContext {
839 let references = collect_references(blocks);
840 let order = collect_cited_keys(blocks);
841 let numbers = assign_numbers(&references, &order);
842 CiteContext {
843 style: active_style(format),
844 references,
845 numbers,
846 }
847}
848
849pub fn ordered_references(ctx: &CiteContext) -> Vec<Reference> {
852 if is_numbered(ctx.style) {
853 let mut v = ctx.references.clone();
854 v.sort_by_key(|r| ctx.numbers.get(&r.key).copied().unwrap_or(usize::MAX));
855 v
856 } else {
857 ctx.references.clone()
858 }
859}
860
861thread_local! {
866 static ACTIVE: RefCell<Option<CiteContext>> = const { RefCell::new(None) };
867}
868
869pub struct CiteScope {
871 _private: (),
872}
873
874impl Drop for CiteScope {
875 fn drop(&mut self) {
876 ACTIVE.with(|c| *c.borrow_mut() = None);
877 }
878}
879
880pub fn install_context(ctx: CiteContext) -> CiteScope {
885 ACTIVE.with(|c| *c.borrow_mut() = Some(ctx));
886 CiteScope { _private: () }
887}
888
889pub fn with_active<R>(f: impl FnOnce(Option<&CiteContext>) -> R) -> R {
891 ACTIVE.with(|c| f(c.borrow().as_ref()))
892}
893
894pub fn substitute_text_cites(text: &str) -> String {
898 with_active(|ctx| match ctx {
899 Some(ctx) if !ctx.references.is_empty() => {
900 let cites = crate::inline::find_inline_cites(text);
901 if cites.is_empty() {
902 return text.to_string();
903 }
904 let mut out = String::with_capacity(text.len());
905 let mut last = 0;
906 for (s, e, cr) in cites {
907 out.push_str(&text[last..s]);
908 out.push_str(&format_in_text(&ctx.references, &cr, ctx.style, &ctx.numbers));
909 last = e;
910 }
911 out.push_str(&text[last..]);
912 out
913 }
914 _ => text.to_string(),
915 })
916}
917
918#[cfg(test)]
919mod tests {
920 use super::*;
921
922 fn fixtures() -> Vec<Reference> {
923 vec![
924 Reference {
925 key: "smith2020".into(),
926 ref_type: RefType::Article,
927 authors: parse_authors("Smith, John"),
928 title: Some("Deep Learning for Climate Models".into()),
929 container: Some("Journal of Climate AI".into()),
930 year: Some("2020".into()),
931 volume: Some("12".into()),
932 issue: Some("3".into()),
933 pages: Some("45-67".into()),
934 doi: Some("10.1000/jcai.2020.45".into()),
935 ..Default::default()
936 },
937 Reference {
938 key: "jones2019".into(),
939 ref_type: RefType::Book,
940 authors: parse_authors("Jones, Alice; Brown, Bob"),
941 title: Some("Foundations of Data Science".into()),
942 publisher: Some("MIT Press".into()),
943 year: Some("2019".into()),
944 ..Default::default()
945 },
946 Reference {
947 key: "lee2021".into(),
948 ref_type: RefType::Web,
949 authors: parse_authors("Lee, Carol"),
950 title: Some("Understanding Transformers".into()),
951 container: Some("AI Weekly".into()),
952 year: Some("2021".into()),
953 url: Some("https://aiweekly.example/transformers".into()),
954 accessed: Some("2021-05-01".into()),
955 ..Default::default()
956 },
957 Reference {
958 key: "garcia2022".into(),
959 ref_type: RefType::Conference,
960 authors: parse_authors("Garcia, David; Patel, Esha; Wong, Fang"),
961 title: Some("Scalable Inference".into()),
962 container: Some("Proceedings of NeurIPS".into()),
963 year: Some("2022".into()),
964 pages: Some("100-110".into()),
965 ..Default::default()
966 },
967 ]
968 }
969
970 fn cite1(key: &str) -> CiteRef {
971 CiteRef {
972 items: vec![CiteItem {
973 key: key.into(),
974 locator: None,
975 }],
976 }
977 }
978
979 #[test]
982 fn parse_author_inverted_and_natural() {
983 assert_eq!(parse_author("Smith, John"), Author { family: "Smith".into(), given: Some("John".into()) });
984 assert_eq!(parse_author("John Smith"), Author { family: "Smith".into(), given: Some("John".into()) });
985 assert_eq!(parse_author("Plato"), Author { family: "Plato".into(), given: None });
986 }
987
988 #[test]
989 fn parse_authors_semicolon() {
990 let a = parse_authors("Jones, Alice; Brown, Bob");
991 assert_eq!(a.len(), 2);
992 assert_eq!(a[0].family, "Jones");
993 assert_eq!(a[1].family, "Brown");
994 }
995
996 #[test]
997 fn initials_multiword() {
998 assert_eq!(initials(&Some("John".into())), "J.");
999 assert_eq!(initials(&Some("Mary Jane".into())), "M. J.");
1000 assert_eq!(initials(&None), "");
1001 }
1002
1003 #[test]
1006 fn apa_in_text_and_list() {
1007 let refs = fixtures();
1008 let nums = BTreeMap::new();
1009 assert_eq!(format_in_text(&refs, &cite1("smith2020"), Format::Apa, &nums), "(Smith, 2020)");
1010 assert_eq!(
1011 format_in_text(&refs, &CiteRef { items: vec![CiteItem { key: "smith2020".into(), locator: Some("p. 12".into()) }] }, Format::Apa, &nums),
1012 "(Smith, 2020, p. 12)"
1013 );
1014 assert_eq!(format_in_text(&refs, &cite1("jones2019"), Format::Apa, &nums), "(Jones & Brown, 2019)");
1015 assert_eq!(format_in_text(&refs, &cite1("garcia2022"), Format::Apa, &nums), "(Garcia et al., 2022)");
1016 assert_eq!(
1017 format_in_text(&refs, &CiteRef { items: vec![CiteItem { key: "smith2020".into(), locator: None }, CiteItem { key: "jones2019".into(), locator: None }] }, Format::Apa, &nums),
1018 "(Smith, 2020; Jones & Brown, 2019)"
1019 );
1020
1021 let list = reference_list(&refs, Format::Apa);
1022 assert_eq!(list[0], "Garcia, D., Patel, E., & Wong, F. (2022). Scalable Inference. *Proceedings of NeurIPS*, 100-110.");
1024 assert_eq!(list[1], "Jones, A., & Brown, B. (2019). *Foundations of Data Science*. MIT Press.");
1025 assert_eq!(list[2], "Lee, C. (2021). Understanding Transformers. *AI Weekly*. https://aiweekly.example/transformers");
1026 assert_eq!(list[3], "Smith, J. (2020). Deep Learning for Climate Models. *Journal of Climate AI*, *12*(3), 45-67. https://doi.org/10.1000/jcai.2020.45");
1027 }
1028
1029 #[test]
1032 fn mla_in_text_and_list() {
1033 let refs = fixtures();
1034 let nums = BTreeMap::new();
1035 assert_eq!(format_in_text(&refs, &cite1("smith2020"), Format::Mla, &nums), "(Smith)");
1036 assert_eq!(
1037 format_in_text(&refs, &CiteRef { items: vec![CiteItem { key: "smith2020".into(), locator: Some("p. 12".into()) }] }, Format::Mla, &nums),
1038 "(Smith 12)"
1039 );
1040 assert_eq!(format_in_text(&refs, &cite1("jones2019"), Format::Mla, &nums), "(Jones and Brown)");
1041 assert_eq!(format_in_text(&refs, &cite1("garcia2022"), Format::Mla, &nums), "(Garcia et al.)");
1042
1043 let list = reference_list(&refs, Format::Mla);
1044 assert_eq!(list[0], "Garcia, David, et al. \"Scalable Inference.\" *Proceedings of NeurIPS*, 2022, pp. 100-110.");
1045 assert_eq!(list[1], "Jones, Alice, and Bob Brown. *Foundations of Data Science*. MIT Press, 2019.");
1046 assert_eq!(list[2], "Lee, Carol. \"Understanding Transformers.\" *AI Weekly*, 2021, https://aiweekly.example/transformers. Accessed 1 May 2021.");
1047 assert_eq!(list[3], "Smith, John. \"Deep Learning for Climate Models.\" *Journal of Climate AI*, vol. 12, no. 3, 2020, pp. 45-67.");
1048 }
1049
1050 #[test]
1053 fn chicago_in_text_and_list() {
1054 let refs = fixtures();
1055 let nums = BTreeMap::new();
1056 assert_eq!(format_in_text(&refs, &cite1("smith2020"), Format::Chicago, &nums), "(Smith 2020)");
1057 assert_eq!(
1058 format_in_text(&refs, &CiteRef { items: vec![CiteItem { key: "smith2020".into(), locator: Some("p. 12".into()) }] }, Format::Chicago, &nums),
1059 "(Smith 2020, 12)"
1060 );
1061 assert_eq!(format_in_text(&refs, &cite1("jones2019"), Format::Chicago, &nums), "(Jones and Brown 2019)");
1062
1063 let list = reference_list(&refs, Format::Chicago);
1064 assert_eq!(list[0], "Garcia, David, Esha Patel, and Fang Wong. 2022. \"Scalable Inference.\" *Proceedings of NeurIPS*, 100-110.");
1065 assert_eq!(list[1], "Jones, Alice, and Bob Brown. 2019. *Foundations of Data Science*. MIT Press.");
1066 assert_eq!(list[2], "Lee, Carol. 2021. \"Understanding Transformers.\" *AI Weekly*. https://aiweekly.example/transformers.");
1067 assert_eq!(list[3], "Smith, John. 2020. \"Deep Learning for Climate Models.\" *Journal of Climate AI* 12 (3): 45-67.");
1068 }
1069
1070 #[test]
1073 fn ieee_in_text_and_list() {
1074 let refs = fixtures();
1075 let mut nums = BTreeMap::new();
1076 nums.insert("smith2020".to_string(), 1);
1077 nums.insert("jones2019".to_string(), 2);
1078 nums.insert("garcia2022".to_string(), 3);
1079 nums.insert("lee2021".to_string(), 4);
1080
1081 assert_eq!(format_in_text(&refs, &cite1("smith2020"), Format::Ieee, &nums), "[1]");
1082 assert_eq!(
1083 format_in_text(&refs, &CiteRef { items: vec![CiteItem { key: "smith2020".into(), locator: None }, CiteItem { key: "jones2019".into(), locator: None }] }, Format::Ieee, &nums),
1084 "[1], [2]"
1085 );
1086 assert_eq!(
1087 format_in_text(&refs, &CiteRef { items: vec![CiteItem { key: "smith2020".into(), locator: Some("p. 12".into()) }] }, Format::Ieee, &nums),
1088 "[1, p. 12]"
1089 );
1090
1091 let ordered = vec![refs[0].clone(), refs[1].clone(), refs[3].clone(), refs[2].clone()];
1093 let list = reference_list(&ordered, Format::Ieee);
1094 assert_eq!(list[0], "[1] J. Smith, \"Deep Learning for Climate Models,\" *Journal of Climate AI*, vol. 12, no. 3, pp. 45-67, 2020.");
1095 assert_eq!(list[1], "[2] A. Jones and B. Brown, *Foundations of Data Science*. MIT Press, 2019.");
1096 assert_eq!(list[2], "[3] D. Garcia, E. Patel, and F. Wong, \"Scalable Inference,\" in *Proceedings of NeurIPS*, 2022, pp. 100-110.");
1097 assert_eq!(list[3], "[4] C. Lee, \"Understanding Transformers,\" *AI Weekly*, 2021. [Online]. Available: https://aiweekly.example/transformers");
1098 }
1099
1100 #[test]
1103 fn reference_list_is_deterministic() {
1104 let refs = fixtures();
1105 for style in [Format::Apa, Format::Mla, Format::Chicago, Format::Ieee] {
1106 assert_eq!(reference_list(&refs, style), reference_list(&refs, style));
1107 }
1108 }
1109
1110 #[test]
1111 fn bibliography_headings() {
1112 assert_eq!(bibliography_heading(Format::Mla), "Works Cited");
1113 assert_eq!(bibliography_heading(Format::Apa), "References");
1114 assert_eq!(bibliography_heading(Format::Chicago), "Bibliography");
1115 assert_eq!(bibliography_heading(Format::Ieee), "References");
1116 }
1117
1118 #[test]
1119 fn active_style_defaults_to_apa() {
1120 assert_eq!(active_style(None), Format::Apa);
1121 assert_eq!(active_style(Some(Format::Ieee)), Format::Ieee);
1122 }
1123}