1use libxml::tree::Node;
18use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
19
20use crate::{
21 document::{NodeData, PostDocument, PostDocumentOptions},
22 object_db::ObjectDB,
23 processor::{ProcessResult, Processor, find_documentclass_and_packages},
24 radix::radix_alpha,
25};
26
27#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum RawBibSource {
38 Path(String),
40 Literal(String),
42}
43
44#[derive(Debug, Clone)]
49pub struct BibConversionRequest {
50 pub sources: Vec<RawBibSource>,
52 pub search_paths: Vec<String>,
54 pub preloads: Vec<String>,
59 pub wanted_keys: Option<Vec<String>>,
66}
67
68pub type BibConverterFn = fn(&BibConversionRequest) -> Option<PostDocument>;
75
76thread_local! {
77 static BIB_CONVERTER: std::cell::Cell<Option<BibConverterFn>> =
78 const { std::cell::Cell::new(None) };
79}
80
81pub fn set_bib_converter(convert: BibConverterFn) {
83 BIB_CONVERTER.with(|slot| slot.set(Some(convert)));
84}
85
86fn bib_converter() -> Option<BibConverterFn> { BIB_CONVERTER.with(|slot| slot.get()) }
87
88#[derive(Debug, Clone, PartialEq)]
90pub enum CitationStyle {
91 Numbers,
93 AuthorYear,
95 Alpha,
97}
98
99#[derive(Debug)]
103struct BibEntryData {
104 bib_key: String,
105 cited_key: Option<String>,
106 sort_key: String,
107 initial: String,
108 author_year: String,
109 suffix: Option<String>,
110 authors_short: String,
112 authors_full: String,
114 sort_names: String,
116 year: String,
117 title: String,
118 entry_type: String,
120 number: u32,
122 referrers: HashSet<String>,
124 bibreferrers: HashSet<String>,
126 citations: Vec<String>,
128 bibentry: Option<Node>,
130}
131
132impl BibEntryData {
133 fn bib_type(&self) -> &str {
135 if self.entry_type.is_empty() {
136 "misc"
137 } else {
138 &self.entry_type
139 }
140 }
141
142 fn format_type(&self) -> &str {
146 match self.entry_type.as_str() {
147 "article" => "article",
148 "book" | "periodical" | "collection" | "proceedings" | "manual" | "misc" | "unpublished"
149 | "booklet" => "book",
150 "incollection"
151 | "collection.article"
152 | "proceedings.article"
153 | "inproceedings"
154 | "inbook" => "incollection",
155 "report" | "techreport" => "report",
156 "thesis" | "mastersthesis" | "phdthesis" => "thesis",
157 "website" | "online" => "website",
158 "software" => "software",
159 _ => "book", }
161 }
162}
163
164pub struct MakeBibliography {
168 name: String,
169 pub db: ObjectDB,
170 split: bool,
171 bibliographies: Vec<String>,
172}
173
174impl MakeBibliography {
175 pub fn new(db: ObjectDB, split: bool) -> Self {
176 MakeBibliography {
177 name: "MakeBibliography".to_string(),
178 db,
179 split,
180 bibliographies: Vec::new(),
181 }
182 }
183
184 pub fn set_bibliographies(&mut self, bibs: Vec<String>) { self.bibliographies = bibs; }
185
186 fn get_bibliographies(
196 &self,
197 doc: &PostDocument,
198 wanted_keys: Option<&Vec<String>>,
199 ) -> Vec<PostDocument> {
200 let mut bibnames: Vec<String> = Vec::new();
201 let mut from_bibliography = false;
202
203 if !self.bibliographies.is_empty() {
205 bibnames = self.bibliographies.clone();
206 } else {
207 if let Some(bibnode) = doc.findnode("//ltx:bibliography") {
209 let files = bibnode
210 .get_attribute("files")
211 .or_else(|| bibnode.get_parent().and_then(|p| p.get_attribute("files")));
212 if let Some(f) = files {
213 from_bibliography = true;
214 bibnames = f.split(',').map(|s| s.trim().to_string()).collect();
215 }
216 }
217 }
218
219 let search_paths = doc.get_search_paths();
220 let mut bibs: Vec<PostDocument> = Vec::new();
221 let mut rawbibs: Vec<RawBibSource> = Vec::new();
226
227 for bib in &bibnames {
228 let mut loaded = false;
229 let is_bib_style = bib.ends_with(".bib") || bib.ends_with(".bib.xml") || from_bibliography;
235
236 if let Some(data) = bib.strip_prefix("literal:") {
238 rawbibs.push(RawBibSource::Literal(data.to_string()));
239 continue;
240 }
241
242 if bib.ends_with(".xml") {
244 if let Some(path) = find_file(bib, search_paths) {
245 match PostDocument::new_from_file(&path, PostDocumentOptions {
246 source_directory: Some(".".to_string()),
247 ..PostDocumentOptions::default()
248 }) {
249 Ok(bibdoc) => {
250 bibs.push(bibdoc);
251 loaded = true;
252 },
253 Err(e) => Warn!("I/O", bib, "Failed to load bibliography '{}': {}", bib, e),
254 }
255 }
256 }
257 else if is_bib_style {
259 let xmlbib = if from_bibliography && !bib.ends_with(".bib") {
260 format!("{}.bib", bib)
261 } else {
262 bib.clone()
263 };
264 let xml_candidate = if xmlbib.ends_with(".xml") {
266 xmlbib.clone()
267 } else {
268 format!("{}.xml", xmlbib)
269 };
270 if let Some(path) = find_file(&xml_candidate, search_paths) {
271 match PostDocument::new_from_file(&path, PostDocumentOptions {
272 source_directory: Some(".".to_string()),
273 ..PostDocumentOptions::default()
274 }) {
275 Ok(bibdoc) => {
276 bibs.push(bibdoc);
277 loaded = true;
278 },
279 Err(e) => Warn!("I/O", path, "Failed to load bibliography '{}': {}", path, e),
280 }
281 }
282 }
283
284 if !loaded {
290 let bib_file = if from_bibliography && !bib.ends_with(".bib") {
291 format!("{}.bib", bib)
292 } else {
293 bib.clone()
294 };
295 if let Some(bib_path) = find_file(&bib_file, search_paths)
296 .or_else(|| latexml_core::util::pathname::kpsewhich(&[bib_file.as_str()]))
297 {
298 rawbibs.push(RawBibSource::Path(bib_path));
299 loaded = true;
300 } else if is_bib_style {
301 Error!(
310 "missing_file",
311 bib,
312 "Couldn't find Bibliography '{}'\nSearchpaths were {}",
313 bib,
314 search_paths.join(",")
315 );
316 }
317 }
318
319 if !loaded {
320 Info!(
321 "bibliography",
322 "missing",
323 "Couldn't find usable bibliography for '{}'",
324 bib
325 );
326 }
327 }
328
329 if !rawbibs.is_empty() {
332 match bib_converter() {
333 Some(convert) => {
334 let (class, packages) = find_documentclass_and_packages(doc);
335 let mut preloads = Vec::with_capacity(1 + packages.len());
337 preloads.push(if class.options.is_empty() {
338 format!("{}.cls", class.name)
339 } else {
340 format!("[{}]{}.cls", class.options, class.name)
341 });
342 for pkg in &packages {
343 preloads.push(if pkg.options.is_empty() {
344 format!("{}.sty", pkg.name)
345 } else {
346 format!("[{}]{}.sty", pkg.options, pkg.name)
347 });
348 }
349 let request = BibConversionRequest {
350 sources: rawbibs,
351 search_paths: search_paths.to_vec(),
352 preloads,
353 wanted_keys: wanted_keys.cloned(),
354 };
355 match convert(&request) {
356 Some(bibdoc) => bibs.push(bibdoc),
357 None => Error!(
358 "bibliography",
359 "convert",
360 "Recursive BibTeX conversion produced no bibliography"
361 ),
362 }
363 },
364 None => Error!(
368 "bibliography",
369 "converter",
370 "No BibTeX converter installed; cannot convert {} raw bibliography source(s) \
371 (call make_bibliography::set_bib_converter before post-processing)",
372 rawbibs.len()
373 ),
374 }
375 }
376
377 Info!(
378 "bibliography",
379 "using",
380 "MakeBibliography: using {} bibliographies",
381 bibs.len()
382 );
383 bibs
384 }
385
386 fn scan_bibentries(entries: &mut HashMap<String, BibEntryData>, srcdoc: &PostDocument) {
396 for bibentry in srcdoc.findnodes("//ltx:bibentry") {
397 let bibkey = match bibentry.get_attribute("key") {
398 Some(k) => k,
399 None => continue,
400 };
401 let lc_key = bibkey.to_lowercase();
402 let citations: Vec<String> = srcdoc
404 .findnodes_at(".//@bibrefs", Some(&bibentry))
405 .iter()
406 .filter_map(|n| {
407 let val = n.get_content();
408 if val.is_empty() { None } else { Some(val) }
409 })
410 .flat_map(|s| s.split(',').map(String::from).collect::<Vec<_>>())
411 .filter(|s| !s.is_empty())
412 .collect();
413
414 entries.insert(lc_key, BibEntryData {
415 bib_key: bibkey,
416 cited_key: None,
417 sort_key: String::new(),
418 initial: String::new(),
419 author_year: String::new(),
420 suffix: None,
421 authors_short: String::new(),
422 authors_full: String::new(),
423 sort_names: String::new(),
424 year: String::new(),
425 title: String::new(),
426 entry_type: String::new(),
427 number: 0,
428 referrers: HashSet::default(),
429 bibreferrers: HashSet::default(),
430 citations,
431 bibentry: Some(bibentry.clone()),
432 });
433 }
434 }
435
436 fn cited_keys(&self, lists: &[&str]) -> Option<Vec<String>> {
447 let mut keys: Vec<String> = Vec::new();
448 for db_key in self.db.get_keys() {
449 let Some(rest) = db_key.strip_prefix("BIBLABEL:") else {
450 continue;
451 };
452 let Some((list, bibkey)) = rest.split_once(':') else {
453 continue;
454 };
455 if !lists.contains(&list) {
456 continue;
457 }
458 if bibkey == "*" {
459 return None; }
461 keys.push(bibkey.to_string());
462 }
463 (!keys.is_empty()).then_some(keys)
464 }
465
466 fn get_bib_entries(
467 &self,
468 doc: &PostDocument,
469 bib_node: &Node,
470 ) -> (HashMap<String, BibEntryData>, Vec<PostDocument>) {
471 let lists_str = bib_node
472 .get_attribute("lists")
473 .unwrap_or_else(|| "bibliography".to_string());
474 let lists: Vec<&str> = lists_str.split_whitespace().collect();
475
476 let mut entries: HashMap<String, BibEntryData> = HashMap::default();
481 let bib_docs = self.get_bibliographies(doc, self.cited_keys(&lists).as_ref());
482 for bibdoc in &bib_docs {
483 Self::scan_bibentries(&mut entries, bibdoc);
484 }
485 Self::scan_bibentries(&mut entries, doc);
500
501 let cite_star = lists
504 .iter()
505 .any(|list| self.db.lookup(&format!("BIBLABEL:{}:*", list)).is_some());
506
507 let mut queue: Vec<String> = Vec::new();
508 for db_key in self.db.get_keys() {
509 if !db_key.starts_with("BIBLABEL:") {
510 continue;
511 }
512 let parts: Vec<&str> = db_key.splitn(3, ':').collect();
513 if parts.len() < 3 {
514 continue;
515 }
516 let (list, bibkey) = (parts[1], parts[2]);
517 if !lists.contains(&list) {
518 continue;
519 }
520
521 let lc_key = bibkey.to_lowercase();
522 if let Some(bentry) = self.db.lookup(db_key) {
523 let has_refs = bentry
524 .get_value("referrers")
525 .map(|v| v.is_truthy())
526 .unwrap_or(false);
527 if has_refs {
528 if let Some(crate::object_db::Value::Hash(refs)) = bentry.get_value("referrers") {
530 for ref_id in refs.keys() {
531 let mut rid = ref_id.clone();
532 let mut is_from_bib = false;
533 while let Some(entry) = self.db.lookup(&format!("ID:{}", rid)) {
534 let entry_type = entry.get_string("type").unwrap_or("");
535 if entry_type == "ltx:bibitem" {
536 is_from_bib = true;
537 break;
538 }
539 match entry.get_string("parent").map(String::from) {
540 Some(parent) => rid = parent,
541 None => break,
542 }
543 }
544 if !is_from_bib {
545 if let Some(existing) = entries.get(&lc_key) {
547 if let Some(ref prev_key) = existing.cited_key {
548 if prev_key != bibkey {
549 Warn!(
550 "bibliography",
551 "case_mismatch",
552 "Case mismatch in bib key '{}' vs '{}'",
553 prev_key,
554 bibkey
555 );
556 }
557 }
558 }
559 let entry = entries
560 .entry(lc_key.clone())
561 .or_insert_with(|| BibEntryData {
562 bib_key: bibkey.to_string(),
563 cited_key: None,
564 sort_key: String::new(),
565 initial: String::new(),
566 author_year: String::new(),
567 suffix: None,
568 authors_short: String::new(),
569 authors_full: String::new(),
570 sort_names: String::new(),
571 year: String::new(),
572 title: String::new(),
573 entry_type: String::new(),
574 number: 0,
575 referrers: HashSet::default(),
576 bibreferrers: HashSet::default(),
577 citations: Vec::new(),
578 bibentry: None,
579 });
580 entry.cited_key = Some(bibkey.to_string());
581 entry.referrers.insert(ref_id.clone());
582 }
583 }
584 }
585 if entries
586 .get(&lc_key)
587 .map(|e| !e.referrers.is_empty())
588 .unwrap_or(false)
589 {
590 queue.push(bibkey.to_string());
591 }
592 }
593 }
594 }
595
596 if cite_star {
609 let mut all: Vec<String> = entries.values().map(|e| e.bib_key.clone()).collect();
610 all.sort();
611 queue.extend(all);
612 }
613
614 let mut seen: HashSet<String> = HashSet::default();
617 let mut included: HashMap<String, BibEntryData> = HashMap::default();
618 let mut missing_keys: Vec<String> = Vec::new();
619
620 while let Some(bibkey) = queue.pop() {
621 if seen.contains(&bibkey) || bibkey == "*" {
622 continue;
623 }
624 seen.insert(bibkey.clone());
625 let lc_key = bibkey.to_lowercase();
626
627 match entries.remove(&lc_key) {
628 Some(mut entry) => {
629 if let Some(ref bibentry) = entry.bibentry {
631 let (sort_names, short_names, _full_names) = extract_names(doc, bibentry);
639 entry.sort_names = sort_names.clone();
640 entry.authors_short = short_names.clone();
641
642 let date_content =
644 PostDocument::findnodes_foreign("ltx:bib-date[@role='publication']", bibentry)
645 .into_iter()
646 .next()
647 .map(|n| n.get_content())
648 .unwrap_or_default();
649 let year = extract_four_digit_year(&date_content);
650 entry.year = year.clone();
651
652 let title = PostDocument::findnodes_foreign("ltx:bib-title", bibentry)
665 .into_iter()
666 .next()
667 .map(|n| n.get_content())
668 .unwrap_or_default();
669 entry.title = title.clone();
670
671 let entry_type = bibentry
673 .get_attribute("type")
674 .unwrap_or_else(|| "misc".to_string());
675 entry.entry_type = entry_type;
676
677 entry.author_year = format!("{}.{}", short_names, year);
684 entry.initial = PostDocument::initial(&short_names, true);
685
686 let sort_key = format!("{}.{}.{}.{}", sort_names, year, title, bibkey).to_lowercase();
688 entry.sort_key = sort_key.clone();
689
690 let citations = entry.citations.clone();
692 for c in &citations {
693 queue.push(c.clone());
694 }
695 included.insert(sort_key, entry);
696 } else {
697 let id = self.find_bib_id(&bibkey, &lists);
699 if let Some(id) = id {
700 let id_key = format!("ID:{}", id);
701 let authors = self
702 .db
703 .lookup(&id_key)
704 .and_then(|e| e.get_value("authors").map(|v| v.to_string()))
705 .unwrap_or_default();
706 let full_authors = self
707 .db
708 .lookup(&id_key)
709 .and_then(|e| e.get_value("fullauthors").map(|v| v.to_string()))
710 .unwrap_or_else(|| authors.clone());
711 let year = self
712 .db
713 .lookup(&id_key)
714 .and_then(|e| e.get_value("year").map(|v| v.to_string()))
715 .unwrap_or_default();
716 let title = self
717 .db
718 .lookup(&id_key)
719 .and_then(|e| e.get_value("title").map(|v| v.to_string()))
720 .unwrap_or_default();
721 let entry_type = self
722 .db
723 .lookup(&id_key)
724 .and_then(|e| e.get_value("type").map(|v| v.to_string()))
725 .unwrap_or_else(|| "misc".to_string());
726
727 let year_short = extract_four_digit_year(&year);
728 let names = if authors.is_empty() {
729 bibkey.clone()
730 } else {
731 authors.clone()
732 };
733 let author_year = format!("{}.{}", names, year_short);
734 let initial = PostDocument::initial(&names, true);
735 let sort_key =
736 format!("{}.{}.{}.{}", names, year_short, title, bibkey).to_lowercase();
737
738 entry.authors_short = authors;
739 entry.authors_full = full_authors;
740 entry.sort_names = names;
741 entry.year = year_short;
742 entry.title = title;
743 entry.entry_type = entry_type;
744 entry.author_year = author_year;
745 entry.initial = initial;
746 entry.sort_key = sort_key.clone();
747
748 included.insert(sort_key, entry);
749 } else {
750 missing_keys.push(bibkey);
751 }
752 }
753 },
754 _ => {
755 missing_keys.push(bibkey);
757 },
758 }
759 }
760
761 if !missing_keys.is_empty() {
762 Warn!(
763 "bibliography",
764 "missing_keys",
765 "Missing bibkeys: {}",
766 missing_keys.join(", ")
767 );
768 }
769
770 let citations_map: Vec<(String, Vec<String>)> = included
773 .values()
774 .map(|e| (e.bib_key.clone(), e.citations.clone()))
775 .collect();
776 for (bibkey, citations) in &citations_map {
777 for cited in citations {
778 let lc = cited.to_lowercase();
779 for entry in included.values_mut() {
781 if entry.bib_key.to_lowercase() == lc {
782 entry.bibreferrers.insert(bibkey.clone());
783 }
784 }
785 }
786 }
787
788 Info!(
789 "bibliography",
790 "count",
791 "MakeBibliography: {} bibentries, {} cited",
792 entries.len() + included.len(),
793 included.len()
794 );
795
796 let mut sorted_keys: Vec<String> = included.keys().cloned().collect();
799 unisort(&mut sorted_keys);
800
801 let mut ay_last: HashMap<String, String> = HashMap::default(); for key in &sorted_keys {
804 if let Some(entry) = included.get(key) {
805 let ay = entry.author_year.clone();
806 if let Some(prev_key) = ay_last.get(&ay) {
807 let prev_key = prev_key.clone();
808 if let Some(prev) = included.get_mut(&prev_key) {
810 if prev.suffix.is_none() {
811 prev.suffix = Some(radix_alpha(1));
812 }
813 }
814 let prev_counter = included
815 .get(&prev_key)
816 .and_then(|p| p.suffix.as_ref())
817 .map(|s| suffix_to_counter(s))
818 .unwrap_or(1);
819 if let Some(e) = included.get_mut(key) {
820 e.suffix = Some(radix_alpha(prev_counter + 1));
821 }
822 }
823 ay_last.insert(ay, key.clone());
824 }
825 }
826
827 for entry in included.values() {
829 if let Some(ref bibentry) = entry.bibentry {
830 let sort_errors = PostDocument::findnodes_foreign(".//ltx:ERROR[@class='sort']", bibentry);
831 for mut sortnode in sort_errors {
832 sortnode.unlink();
833 }
834 }
835 }
836
837 (included, bib_docs)
843 }
844
845 fn find_bib_id(&self, bibkey: &str, lists: &[&str]) -> Option<String> {
847 for list in lists {
848 let bkey = format!("BIBLABEL:{}:{}", list, bibkey);
849 if let Some(bentry) = self.db.lookup(&bkey) {
850 if let Some(id) = bentry.get_string("id") {
851 return Some(id.to_string());
852 }
853 }
854 }
855 None
856 }
857
858 fn make_bibliography_list(
862 &self,
863 doc: &PostDocument,
864 bib_id: &str,
865 initial: Option<&str>,
866 entries: &HashMap<String, BibEntryData>,
867 style: &CitationStyle,
868 ) -> NodeData {
869 let id = if let Some(init) = initial {
870 format!("{}.L1.{}", bib_id, init)
871 } else {
872 format!("{}.L1", bib_id)
873 };
874
875 let mut ordered: Vec<&BibEntryData> = entries.values().collect();
882 ordered.sort_by_key(|e| e.number);
883 let items: Vec<NodeData> = ordered
884 .iter()
885 .map(|entry| self.format_bib_entry(doc, bib_id, entry, style))
886 .collect();
887
888 NodeData::Element {
889 tag: "ltx:biblist".to_string(),
890 attributes: Some(HashMap::from_iter([("xml:id".to_string(), id)])),
891 children: items,
892 }
893 }
894
895 fn format_bib_entry(
899 &self,
900 doc: &PostDocument,
901 bib_id: &str,
902 entry: &BibEntryData,
903 style: &CitationStyle,
904 ) -> NodeData {
905 let id = if let Some(ref bibentry) = entry.bibentry {
909 let orig_id = crate::document::get_xml_id(bibentry).unwrap_or_default();
910 if orig_id.is_empty() {
911 format!("{}.bib{}", bib_id, entry.number)
913 } else {
914 let stripped = orig_id.strip_prefix("bib").unwrap_or(&orig_id);
915 format!("{}{}", bib_id, stripped)
916 }
917 } else {
918 format!("{}.bib{}", bib_id, entry.number)
919 };
920
921 let cited_key = entry.cited_key.as_deref().unwrap_or(&entry.bib_key);
922 let mut children = Vec::new();
923
924 let mut tags = Vec::new();
926
927 tags.push(NodeData::Element {
929 tag: "ltx:tag".to_string(),
930 attributes: Some(HashMap::from_iter([
931 ("role".to_string(), "number".to_string()),
932 ("class".to_string(), "ltx_bib_number".to_string()),
933 ])),
934 children: vec![NodeData::Text(entry.number.to_string())],
935 });
936
937 let (author_tag_nodes, has_names, has_key, has_year, has_typetag) =
939 self.build_author_year_tags(doc, entry);
940 tags.extend(author_tag_nodes);
941
942 let mut effective_style = style.clone();
944 if !((has_names || has_key) && (has_year || has_typetag)) {
946 effective_style = CitationStyle::Numbers;
947 }
948
949 let mut skip_first_block = false;
950 let mut drop_first_block_year = false;
952 match effective_style {
953 CitationStyle::Numbers => {
954 tags.push(NodeData::Element {
955 tag: "ltx:tag".to_string(),
956 attributes: Some(HashMap::from_iter([
957 ("role".to_string(), "refnum".to_string()),
958 ("class".to_string(), "ltx_bib_key".to_string()),
959 ("open".to_string(), "[".to_string()),
960 ("close".to_string(), "]".to_string()),
961 ])),
962 children: vec![NodeData::Text(entry.number.to_string())],
963 });
964 },
965 CitationStyle::Alpha => {
966 let aa = self.make_alpha_label(doc, entry);
968 let yy = if entry.year.len() >= 4 {
969 entry.year[2..4].to_string()
970 } else {
971 entry.year.clone()
972 };
973 let suffix = entry.suffix.as_deref().unwrap_or("");
974 tags.push(NodeData::Element {
975 tag: "ltx:tag".to_string(),
976 attributes: Some(HashMap::from_iter([
977 ("role".to_string(), "refnum".to_string()),
978 ("class".to_string(), "ltx_bib_abbrv".to_string()),
979 ("open".to_string(), "[".to_string()),
980 ("close".to_string(), "]".to_string()),
981 ])),
982 children: vec![NodeData::Text(format!("{}{}{}", aa, yy, suffix))],
983 });
984 },
985 CitationStyle::AuthorYear => {
986 skip_first_block = false;
1011 drop_first_block_year = true;
1012 let suffix = entry.suffix.as_deref().unwrap_or("");
1013 let mut refnum_children: Vec<NodeData> = if let Some(ref bibentry) = entry.bibentry {
1014 let authors = PostDocument::findnodes_foreign("ltx:bib-name[@role='author']", bibentry);
1015 if !authors.is_empty() {
1016 do_names_short(authors)
1017 } else {
1018 let editors = PostDocument::findnodes_foreign("ltx:bib-name[@role='editor']", bibentry);
1019 if !editors.is_empty() {
1020 do_editors_a(editors)
1021 } else {
1022 let key = PostDocument::findnodes_foreign("ltx:bib-key", bibentry)
1024 .into_iter()
1025 .next()
1026 .map(|k| k.get_content())
1027 .unwrap_or_else(|| entry.bib_key.clone());
1028 vec![NodeData::Text(key)]
1029 }
1030 }
1031 } else {
1032 let s = if !entry.authors_full.is_empty() {
1035 entry.authors_full.clone()
1036 } else if !entry.authors_short.is_empty() {
1037 entry.authors_short.clone()
1038 } else {
1039 entry.bib_key.clone()
1040 };
1041 vec![NodeData::Text(s)]
1042 };
1043 let year_text = if !entry.year.is_empty() {
1046 format!("{}{}", entry.year, suffix)
1047 } else if let Some(ref bibentry) = entry.bibentry {
1048 PostDocument::findnodes_foreign("ltx:bib-type", bibentry)
1049 .into_iter()
1050 .next()
1051 .map(|t| t.get_content())
1052 .unwrap_or_default()
1053 } else {
1054 String::new()
1055 };
1056 refnum_children.push(NodeData::Text(format!(" ({})", year_text)));
1057 tags.push(NodeData::Element {
1058 tag: "ltx:tag".to_string(),
1059 attributes: Some(HashMap::from_iter([
1060 ("role".to_string(), "refnum".to_string()),
1061 ("class".to_string(), "ltx_bib_author-year".to_string()),
1062 ])),
1063 children: refnum_children,
1064 });
1065 },
1066 }
1067
1068 if !tags.is_empty() {
1069 children.push(NodeData::Element {
1070 tag: "ltx:tags".to_string(),
1071 attributes: None,
1072 children: tags,
1073 });
1074 }
1075
1076 let blocks = self.format_blocks(doc, entry, skip_first_block, drop_first_block_year);
1078 children.extend(blocks);
1079
1080 let mut citedby: Vec<NodeData> = Vec::new();
1082 let mut sorted_referrers: Vec<&String> = entry.referrers.iter().collect();
1083 sorted_referrers.sort();
1084 for ref_id in &sorted_referrers {
1085 citedby.push(NodeData::Element {
1086 tag: "ltx:ref".to_string(),
1087 attributes: Some(HashMap::from_iter([
1088 ("idref".to_string(), (*ref_id).clone()),
1089 ("show".to_string(), "typerefnum".to_string()),
1090 ])),
1091 children: vec![],
1092 });
1093 }
1094 if !entry.bibreferrers.is_empty() {
1095 let mut sorted_bibrefs: Vec<&String> = entry.bibreferrers.iter().collect();
1096 sorted_bibrefs.sort();
1097 citedby.push(NodeData::Element {
1098 tag: "ltx:bibref".to_string(),
1099 attributes: Some(HashMap::from_iter([
1100 (
1101 "bibrefs".to_string(),
1102 sorted_bibrefs
1103 .iter()
1104 .map(|s| s.as_str())
1105 .collect::<Vec<_>>()
1106 .join(","),
1107 ),
1108 ("show".to_string(), "refnum".to_string()),
1109 ])),
1110 children: vec![],
1111 });
1112 }
1113 if !citedby.is_empty() {
1114 let conjoined = PostDocument::conjoin(
1115 crate::document::Conjunction::Simple(",\n".to_string()),
1116 citedby,
1117 );
1118 let mut block_children = vec![NodeData::Text("Cited by: ".to_string())];
1119 block_children.extend(conjoined);
1120 block_children.push(NodeData::Text(".".to_string()));
1121 children.push(NodeData::Element {
1122 tag: "ltx:bibblock".to_string(),
1123 attributes: Some(HashMap::from_iter([(
1124 "class".to_string(),
1125 "ltx_bib_cited".to_string(),
1126 )])),
1127 children: block_children,
1128 });
1129 }
1130
1131 NodeData::Element {
1132 tag: "ltx:bibitem".to_string(),
1133 attributes: Some(HashMap::from_iter([
1134 ("xml:id".to_string(), id),
1135 ("key".to_string(), cited_key.to_string()),
1136 ("type".to_string(), entry.entry_type.clone()),
1137 ("class".to_string(), format!("ltx_bib_{}", entry.bib_type())),
1138 ])),
1139 children,
1140 }
1141 }
1142
1143 fn build_author_year_tags(
1147 &self,
1148 doc: &PostDocument,
1149 entry: &BibEntryData,
1150 ) -> (Vec<NodeData>, bool, bool, bool, bool) {
1151 let mut tags = Vec::new();
1152 let mut has_names = false;
1153 let mut has_key = false;
1154 let mut has_year = false;
1155 let mut has_typetag = false;
1156
1157 if let Some(ref bibentry) = entry.bibentry {
1158 let mut surnames: Vec<Node> =
1160 doc.findnodes_at("ltx:bib-name[@role='author']/ltx:surname", Some(bibentry));
1161 if surnames.is_empty() {
1162 surnames = doc.findnodes_at("ltx:bib-name[@role='editor']/ltx:surname", Some(bibentry));
1163 }
1164
1165 if surnames.len() > 2 {
1166 has_names = true;
1167 let first_text = surnames[0].get_content();
1169 tags.push(NodeData::Element {
1170 tag: "ltx:tag".to_string(),
1171 attributes: Some(HashMap::from_iter([
1172 ("role".to_string(), "authors".to_string()),
1173 ("class".to_string(), "ltx_bib_author".to_string()),
1174 ])),
1175 children: vec![NodeData::Text(first_text), NodeData::Element {
1176 tag: "ltx:text".to_string(),
1177 attributes: Some(HashMap::from_iter([(
1178 "class".to_string(),
1179 "ltx_bib_etal".to_string(),
1180 )])),
1181 children: vec![NodeData::Text(" et al.".to_string())],
1182 }],
1183 });
1184 let mut full_children: Vec<NodeData> = Vec::new();
1186 for (i, surname) in surnames.iter().enumerate() {
1187 if i > 0 && i < surnames.len() - 1 {
1188 full_children.push(NodeData::Text(", ".to_string()));
1189 } else if i == surnames.len() - 1 {
1190 full_children.push(NodeData::Text(" and ".to_string()));
1191 }
1192 full_children.push(NodeData::Text(surname.get_content()));
1193 }
1194 tags.push(NodeData::Element {
1195 tag: "ltx:tag".to_string(),
1196 attributes: Some(HashMap::from_iter([
1197 ("role".to_string(), "fullauthors".to_string()),
1198 ("class".to_string(), "ltx_bib_author".to_string()),
1199 ])),
1200 children: full_children,
1201 });
1202 } else if surnames.len() == 2 {
1203 has_names = true;
1204 tags.push(NodeData::Element {
1205 tag: "ltx:tag".to_string(),
1206 attributes: Some(HashMap::from_iter([
1207 ("role".to_string(), "authors".to_string()),
1208 ("class".to_string(), "ltx_bib_author".to_string()),
1209 ])),
1210 children: vec![
1211 NodeData::Text(surnames[0].get_content()),
1212 NodeData::Text(" and ".to_string()),
1213 NodeData::Text(surnames[1].get_content()),
1214 ],
1215 });
1216 } else if !surnames.is_empty() {
1217 has_names = true;
1218 tags.push(NodeData::Element {
1219 tag: "ltx:tag".to_string(),
1220 attributes: Some(HashMap::from_iter([
1221 ("role".to_string(), "authors".to_string()),
1222 ("class".to_string(), "ltx_bib_author".to_string()),
1223 ])),
1224 children: vec![NodeData::Text(surnames[0].get_content())],
1225 });
1226 }
1227
1228 if let Some(key_node) = PostDocument::findnodes_foreign("ltx:bib-key", bibentry)
1230 .into_iter()
1231 .next()
1232 {
1233 has_key = true;
1234 tags.push(NodeData::Element {
1235 tag: "ltx:tag".to_string(),
1236 attributes: Some(HashMap::from_iter([
1237 ("role".to_string(), "key".to_string()),
1238 ("class".to_string(), "ltx_bib_key".to_string()),
1239 ])),
1240 children: vec![NodeData::Text(key_node.get_content())],
1241 });
1242 }
1243
1244 if let Some(date_node) =
1246 PostDocument::findnodes_foreign("ltx:bib-date[@role='publication']", bibentry)
1247 .into_iter()
1248 .next()
1249 {
1250 has_year = true;
1251 let year_text = extract_four_digit_year(&date_node.get_content());
1252 let suffix = entry.suffix.as_deref().unwrap_or("");
1253 tags.push(NodeData::Element {
1254 tag: "ltx:tag".to_string(),
1255 attributes: Some(HashMap::from_iter([
1256 ("role".to_string(), "year".to_string()),
1257 ("class".to_string(), "ltx_bib_year".to_string()),
1258 ])),
1259 children: vec![NodeData::Text(format!("{}{}", year_text, suffix))],
1260 });
1261 }
1262
1263 if let Some(type_node) = PostDocument::findnodes_foreign("ltx:bib-type", bibentry)
1265 .into_iter()
1266 .next()
1267 {
1268 has_typetag = true;
1269 tags.push(NodeData::Element {
1270 tag: "ltx:tag".to_string(),
1271 attributes: Some(HashMap::from_iter([
1272 ("role".to_string(), "bibtype".to_string()),
1273 ("class".to_string(), "ltx_bib_type".to_string()),
1274 ])),
1275 children: vec![NodeData::Text(type_node.get_content())],
1276 });
1277 }
1278
1279 if let Some(title_node) = PostDocument::findnodes_foreign("ltx:bib-title", bibentry)
1281 .into_iter()
1282 .next()
1283 {
1284 tags.push(NodeData::Element {
1285 tag: "ltx:tag".to_string(),
1286 attributes: Some(HashMap::from_iter([
1287 ("role".to_string(), "title".to_string()),
1288 ("class".to_string(), "ltx_bib_title".to_string()),
1289 ])),
1290 children: vec![NodeData::Text(title_node.get_content())],
1291 });
1292 }
1293 } else {
1294 if !entry.authors_short.is_empty() {
1296 has_names = true;
1297 tags.push(NodeData::Element {
1298 tag: "ltx:tag".to_string(),
1299 attributes: Some(HashMap::from_iter([
1300 ("role".to_string(), "authors".to_string()),
1301 ("class".to_string(), "ltx_bib_author".to_string()),
1302 ])),
1303 children: vec![NodeData::Text(entry.authors_short.clone())],
1304 });
1305 if entry.authors_full != entry.authors_short {
1306 tags.push(NodeData::Element {
1307 tag: "ltx:tag".to_string(),
1308 attributes: Some(HashMap::from_iter([
1309 ("role".to_string(), "fullauthors".to_string()),
1310 ("class".to_string(), "ltx_bib_author".to_string()),
1311 ])),
1312 children: vec![NodeData::Text(entry.authors_full.clone())],
1313 });
1314 }
1315 }
1316 if !entry.year.is_empty() {
1317 has_year = true;
1318 let suffix = entry.suffix.as_deref().unwrap_or("");
1319 tags.push(NodeData::Element {
1320 tag: "ltx:tag".to_string(),
1321 attributes: Some(HashMap::from_iter([
1322 ("role".to_string(), "year".to_string()),
1323 ("class".to_string(), "ltx_bib_year".to_string()),
1324 ])),
1325 children: vec![NodeData::Text(format!("{}{}", entry.year, suffix))],
1326 });
1327 }
1328 if !entry.title.is_empty() {
1329 tags.push(NodeData::Element {
1330 tag: "ltx:tag".to_string(),
1331 attributes: Some(HashMap::from_iter([
1332 ("role".to_string(), "title".to_string()),
1333 ("class".to_string(), "ltx_bib_title".to_string()),
1334 ])),
1335 children: vec![NodeData::Text(entry.title.clone())],
1336 });
1337 }
1338 }
1339
1340 (tags, has_names, has_key, has_year, has_typetag)
1341 }
1342
1343 fn make_alpha_label(&self, doc: &PostDocument, entry: &BibEntryData) -> String {
1347 if let Some(ref bibentry) = entry.bibentry {
1348 let mut surnames: Vec<Node> =
1349 doc.findnodes_at("ltx:bib-name[@role='author']/ltx:surname", Some(bibentry));
1350 if surnames.is_empty() {
1351 surnames = doc.findnodes_at("ltx:bib-name[@role='editor']/ltx:surname", Some(bibentry));
1352 }
1353 if surnames.len() > 1 {
1354 let initials: Vec<char> = surnames
1362 .iter()
1363 .map(|n| n.get_content().chars().next().unwrap_or('?'))
1364 .collect();
1365 if initials.len() > 3 {
1366 format!("{}+", initials[..3].iter().collect::<String>())
1367 } else {
1368 initials.iter().collect::<String>()
1369 }
1370 } else if !surnames.is_empty() {
1371 let text = surnames[0].get_content();
1372 text.chars().take(3).collect::<String>().to_uppercase()
1373 } else {
1374 entry
1375 .bib_key
1376 .chars()
1377 .take(3)
1378 .collect::<String>()
1379 .to_uppercase()
1380 }
1381 } else {
1382 if !entry.authors_short.is_empty() {
1384 entry
1385 .authors_short
1386 .split_whitespace()
1387 .filter_map(|w| w.chars().next())
1388 .map(|c| c.to_uppercase().to_string())
1389 .collect::<Vec<_>>()
1390 .join("")
1391 } else {
1392 entry
1393 .bib_key
1394 .chars()
1395 .take(3)
1396 .collect::<String>()
1397 .to_uppercase()
1398 }
1399 }
1400 }
1401
1402 fn format_blocks(
1406 &self,
1407 doc: &PostDocument,
1408 entry: &BibEntryData,
1409 skip_first: bool,
1410 drop_first_year: bool,
1411 ) -> Vec<NodeData> {
1412 let format_type = entry.format_type();
1413 let block_specs = get_fmt_spec(format_type);
1414 let mut blocks = Vec::new();
1415
1416 for (i, block_spec) in block_specs.iter().enumerate() {
1417 if skip_first && i == 0 {
1418 continue;
1419 }
1420
1421 let mut items: Vec<NodeData> = Vec::new();
1422 for field_spec in block_spec {
1423 if drop_first_year && i == 0 && field_spec.class == "year" {
1431 continue;
1432 }
1433 let (nodes_found, negated) = if let Some(ref bibentry) = entry.bibentry {
1434 let xpath = field_spec.xpath.trim_start_matches('!').trim();
1435 let negated = field_spec.xpath.starts_with('!');
1436 if xpath == "true" {
1437 (true, false)
1438 } else {
1439 let found = !PostDocument::findnodes_foreign(xpath, bibentry).is_empty();
1440 (found, negated)
1441 }
1442 } else {
1443 let found = match_metadata_field(field_spec.xpath, entry);
1445 (found, field_spec.xpath.starts_with('!'))
1446 };
1447
1448 if field_spec.xpath != "true" {
1450 if negated {
1451 if nodes_found {
1452 continue;
1453 }
1454 } else {
1455 if !nodes_found {
1456 continue;
1457 }
1458 }
1459 }
1460
1461 if !field_spec.punct.is_empty() && !items.is_empty() {
1463 items.push(NodeData::Text(field_spec.punct.to_string()));
1464 }
1465 if !field_spec.pre.is_empty() {
1467 items.push(NodeData::Text(field_spec.pre.to_string()));
1468 }
1469 if !field_spec.class.is_empty() {
1471 let content = if let Some(ref bibentry) = entry.bibentry {
1472 let xpath = field_spec.xpath.trim_start_matches('!').trim();
1473 if xpath == "true" {
1474 Vec::new()
1475 } else {
1476 let nodes = PostDocument::findnodes_foreign(xpath, bibentry);
1477 apply_formatter(doc, field_spec.formatter, &nodes)
1478 }
1479 } else {
1480 get_metadata_content(field_spec.xpath, entry)
1481 };
1482 if !content.is_empty() {
1483 items.push(NodeData::Element {
1484 tag: "ltx:text".to_string(),
1485 attributes: Some(HashMap::from_iter([(
1486 "class".to_string(),
1487 format!("ltx_bib_{}", field_spec.class),
1488 )])),
1489 children: content,
1490 });
1491 }
1492 }
1493 if !field_spec.post.is_empty() {
1495 items.push(NodeData::Text(field_spec.post.to_string()));
1496 }
1497 }
1498
1499 if !items.is_empty() {
1500 blocks.push(make_bibblock("", &items));
1501 }
1502 }
1503
1504 blocks
1510 }
1511}
1512
1513impl Processor for MakeBibliography {
1514 fn get_name(&self) -> &str { &self.name }
1515
1516 fn to_process(&self, doc: &PostDocument) -> Vec<Node> { doc.findnodes("//ltx:bibliography") }
1517
1518 fn process(&mut self, mut doc: PostDocument, nodes: Vec<Node>) -> ProcessResult {
1519 for bib in &nodes {
1520 if !doc.findnodes_at(".//ltx:bibitem", Some(bib)).is_empty() {
1522 continue;
1523 }
1524
1525 let citestyle_str = bib
1535 .get_attribute("citestyle")
1536 .filter(|s| !s.is_empty())
1537 .unwrap_or_else(|| "numbers".to_string());
1538 let style = match citestyle_str.as_str() {
1548 "numbers" => CitationStyle::Numbers,
1549 "AY" => CitationStyle::Alpha,
1550 _ => CitationStyle::AuthorYear,
1551 };
1552
1553 let (mut entries, _bib_docs) = self.get_bib_entries(&doc, bib);
1555 if entries.is_empty() {
1556 Info!(
1557 "bibliography",
1558 "empty",
1559 "MakeBibliography: no entries to process"
1560 );
1561 continue;
1562 }
1563
1564 let bib_id = crate::document::get_xml_id(bib)
1568 .or_else(|| {
1569 doc
1570 .get_document_element()
1571 .as_ref()
1572 .and_then(crate::document::get_xml_id)
1573 })
1574 .unwrap_or_else(|| "bib".to_string());
1575
1576 let mut number = 0u32;
1592
1593 let is_numeric = bib
1610 .get_attribute("citestyle")
1611 .as_deref()
1612 .map(|s| s.is_empty() || s == "numbers")
1613 .unwrap_or(true);
1614 let unsorted_style = bib.get_attribute("sort").as_deref() == Some("false")
1615 || bib
1616 .get_attribute("bibstyle")
1617 .as_deref()
1618 .is_some_and(is_citation_order_style);
1619 let cite_order = (is_numeric && unsorted_style).then(|| citation_order(&doc));
1620
1621 if self.split {
1622 let mut by_initial: HashMap<String, Vec<String>> = HashMap::default();
1624 for (key, entry) in &entries {
1625 by_initial
1626 .entry(entry.initial.clone())
1627 .or_default()
1628 .push(key.clone());
1629 }
1630 let mut initials: Vec<String> = by_initial.keys().cloned().collect();
1631 initials.sort();
1632 for group in by_initial.values_mut() {
1633 unisort(group);
1634 }
1635 for initial in &initials {
1636 for key in &by_initial[initial] {
1644 debug_assert!(entries.contains_key(key), "grouped key {key} left entries");
1645 if let Some(entry) = entries.get_mut(key) {
1646 number += 1;
1647 entry.number = number;
1648 }
1649 }
1650 let subset: HashMap<String, BibEntryData> = by_initial[initial]
1652 .iter()
1653 .filter_map(|k| entries.get(k).map(|e| (k.clone(), clone_entry(e))))
1654 .collect();
1655 let biblist = self.make_bibliography_list(&doc, &bib_id, Some(initial), &subset, &style);
1656 let mut bib_mut = bib.clone();
1657 doc.add_nodes(&mut bib_mut, &[biblist]);
1658 }
1659 } else {
1660 let sorted_keys = order_entry_keys(&entries, cite_order.as_ref());
1661 for key in &sorted_keys {
1662 debug_assert!(entries.contains_key(key), "sorted key {key} left entries");
1663 if let Some(entry) = entries.get_mut(key) {
1664 number += 1;
1665 entry.number = number;
1666 }
1667 }
1668 let biblist = self.make_bibliography_list(&doc, &bib_id, None, &entries, &style);
1669 let mut bib_mut = bib.clone();
1670 doc.add_nodes(&mut bib_mut, &[biblist]);
1671 }
1672
1673 Info!(
1674 "bibliography",
1675 "formatted",
1676 "MakeBibliography: formatted {} entries",
1677 entries.len()
1678 );
1679
1680 let lists_str = bib
1684 .get_attribute("lists")
1685 .unwrap_or_else(|| "bibliography".to_string());
1686 for entry in entries.values() {
1687 let cited_key = entry.cited_key.as_deref().unwrap_or(&entry.bib_key);
1688 let bibitem_id = if let Some(ref bibentry) = entry.bibentry {
1690 let orig_id = crate::document::get_xml_id(bibentry).unwrap_or_default();
1691 if orig_id.is_empty() {
1692 format!("{}.bib{}", bib_id, entry.number)
1693 } else {
1694 let stripped = orig_id.strip_prefix("bib").unwrap_or(&orig_id);
1695 format!("{}{}", bib_id, stripped)
1696 }
1697 } else {
1698 format!("{}.bib{}", bib_id, entry.number)
1699 };
1700
1701 for list in lists_str.split_whitespace() {
1703 let label_key = format!("BIBLABEL:{}:{}", list, cited_key);
1704 self.db.register(&label_key, vec![(
1705 "id",
1706 crate::object_db::Value::from(bibitem_id.as_str()),
1707 )]);
1708 }
1709
1710 let location = doc.site_relative_destination().unwrap_or_default();
1715 self.db.register(&format!("ID:{}", bibitem_id), vec![
1716 ("type", crate::object_db::Value::from("ltx:bibitem")),
1717 ("location", crate::object_db::Value::from(location.as_str())),
1718 ("fragid", crate::object_db::Value::from(bibitem_id.as_str())),
1719 (
1720 "number",
1721 crate::object_db::Value::from(entry.number.to_string().as_str()),
1722 ),
1723 ]);
1724 }
1725 }
1726
1727 let location = doc.site_relative_destination().unwrap_or_default();
1752 for node in doc.findnodes("//ltx:bibliography//*[@xml:id]") {
1753 let Some(id) = crate::document::get_xml_id(&node) else {
1754 continue;
1755 };
1756 let key = format!("ID:{}", id);
1757 let qname = doc
1758 .get_qname(&node)
1759 .unwrap_or_else(|| "ltx:text".to_string());
1760 if qname == "ltx:bibitem" {
1761 let props = crate::scan::bibitem_tag_props(&doc, &node);
1771 if !props.is_empty() {
1772 let entry = self.db.register(&key, vec![]);
1773 for (k, v) in props {
1774 entry.set_value(&k, v);
1775 }
1776 }
1777 } else if self.db.lookup(&key).is_none() {
1778 self.db.register(&key, vec![
1782 ("type", crate::object_db::Value::from(qname.as_str())),
1783 ("location", crate::object_db::Value::from(location.as_str())),
1784 ("fragid", crate::object_db::Value::from(id.as_str())),
1785 ]);
1786 }
1787 }
1788
1789 let bibentries = doc.findnodes("//ltx:bibentry");
1791 if !bibentries.is_empty() {
1792 doc.remove_nodes(&bibentries);
1793 }
1794
1795 let biblists = doc.findnodes("//ltx:biblist");
1797 let empty_lists: Vec<Node> = biblists
1798 .into_iter()
1799 .filter(|n| {
1800 n.get_first_child()
1801 .map(|c| {
1802 let mut has_element = false;
1803 let mut current = Some(c);
1804 while let Some(ref node) = current {
1805 if node.get_type() == Some(libxml::tree::NodeType::ElementNode) {
1806 has_element = true;
1807 break;
1808 }
1809 current = node.get_next_sibling();
1810 }
1811 !has_element
1812 })
1813 .unwrap_or(true)
1814 })
1815 .collect();
1816 if !empty_lists.is_empty() {
1817 doc.remove_nodes(&empty_lists);
1818 }
1819
1820 Ok(vec![doc])
1821 }
1822}
1823
1824#[derive(Clone)]
1833struct FieldSpec {
1834 xpath: &'static str,
1836 punct: &'static str,
1838 pre: &'static str,
1840 class: &'static str,
1842 formatter: Formatter,
1844 post: &'static str,
1846}
1847
1848#[derive(Clone, Copy)]
1849enum Formatter {
1850 Any,
1851 Authors,
1852 EditorsA,
1853 EditorsB,
1854 Year,
1855 Type,
1856 Title,
1857 ThesisType,
1858 Edition,
1859 Pages,
1860 CrossRef,
1861 Links,
1862 None,
1863}
1864
1865fn get_fmt_spec(format_type: &str) -> Vec<Vec<FieldSpec>> {
1867 let meta_block: Vec<Vec<FieldSpec>> = vec![
1868 vec![FieldSpec {
1869 xpath: "ltx:bib-note",
1870 punct: "",
1871 pre: "Note: ",
1872 class: "note",
1873 formatter: Formatter::Any,
1874 post: "",
1875 }],
1876 vec![FieldSpec {
1877 xpath: "ltx:bib-links | ltx:bib-review | ltx:bib-identifier | ltx:bib-url",
1878 punct: "",
1879 pre: "External Links: ",
1880 class: "links",
1881 formatter: Formatter::Links,
1882 post: "",
1883 }],
1884 ];
1885
1886 let mut blocks = match format_type {
1887 "article" => vec![
1888 vec![
1890 FieldSpec {
1891 xpath: "ltx:bib-name[@role='author']",
1892 punct: "",
1893 pre: "",
1894 class: "author",
1895 formatter: Formatter::Authors,
1896 post: "",
1897 },
1898 FieldSpec {
1899 xpath: "ltx:bib-date[@role='publication']",
1900 punct: "",
1901 pre: "",
1902 class: "year",
1903 formatter: Formatter::Year,
1904 post: "",
1905 },
1906 ],
1907 vec![FieldSpec {
1909 xpath: "ltx:bib-title",
1910 punct: "",
1911 pre: "",
1912 class: "title",
1913 formatter: Formatter::Title,
1914 post: ".",
1915 }],
1916 vec![
1918 FieldSpec {
1919 xpath: "ltx:bib-part[@role='part']",
1920 punct: "",
1921 pre: "",
1922 class: "part",
1923 formatter: Formatter::Any,
1924 post: "",
1925 },
1926 FieldSpec {
1927 xpath: "ltx:bib-related/ltx:bib-title",
1928 punct: ", ",
1929 pre: "",
1930 class: "journal",
1931 formatter: Formatter::Any,
1932 post: "",
1933 },
1934 FieldSpec {
1935 xpath: "ltx:bib-part[@role='volume']",
1936 punct: " ",
1937 pre: "",
1938 class: "volume",
1939 formatter: Formatter::Any,
1940 post: "",
1941 },
1942 FieldSpec {
1943 xpath: "ltx:bib-part[@role='number']",
1944 punct: " ",
1945 pre: "(",
1946 class: "number",
1947 formatter: Formatter::Any,
1948 post: ")",
1949 },
1950 FieldSpec {
1951 xpath: "ltx:bib-status",
1952 punct: ", ",
1953 pre: "(",
1954 class: "status",
1955 formatter: Formatter::Any,
1956 post: ")",
1957 },
1958 FieldSpec {
1959 xpath: "ltx:bib-part[@role='pages']",
1960 punct: ", ",
1961 pre: "",
1962 class: "pages",
1963 formatter: Formatter::Pages,
1964 post: "",
1965 },
1966 FieldSpec {
1967 xpath: "ltx:bib-language",
1968 punct: " ",
1969 pre: "(",
1970 class: "language",
1971 formatter: Formatter::Any,
1972 post: ")",
1973 },
1974 FieldSpec {
1975 xpath: "true",
1976 punct: ".",
1977 pre: "",
1978 class: "",
1979 formatter: Formatter::None,
1980 post: "",
1981 },
1982 ],
1983 ],
1984 "book" => vec![
1985 vec![
1986 FieldSpec {
1987 xpath: "ltx:bib-name[@role='author']",
1988 punct: "",
1989 pre: "",
1990 class: "author",
1991 formatter: Formatter::Authors,
1992 post: "",
1993 },
1994 FieldSpec {
1995 xpath: "ltx:bib-name[@role='editor']",
1996 punct: "",
1997 pre: "",
1998 class: "editor",
1999 formatter: Formatter::EditorsA,
2000 post: "",
2001 },
2002 FieldSpec {
2003 xpath: "ltx:bib-date[@role='publication']",
2004 punct: "",
2005 pre: "",
2006 class: "year",
2007 formatter: Formatter::Year,
2008 post: "",
2009 },
2010 ],
2011 vec![FieldSpec {
2012 xpath: "ltx:bib-title",
2013 punct: "",
2014 pre: "",
2015 class: "title",
2016 formatter: Formatter::Title,
2017 post: ".",
2018 }],
2019 vec![
2020 FieldSpec {
2021 xpath: "ltx:bib-type",
2022 punct: "",
2023 pre: "",
2024 class: "type",
2025 formatter: Formatter::Any,
2026 post: "",
2027 },
2028 FieldSpec {
2029 xpath: "ltx:bib-edition",
2030 punct: ", ",
2031 pre: "",
2032 class: "edition",
2033 formatter: Formatter::Edition,
2034 post: "",
2035 },
2036 FieldSpec {
2037 xpath: "ltx:bib-part[@role='series']",
2038 punct: ", ",
2039 pre: "",
2040 class: "series",
2041 formatter: Formatter::Any,
2042 post: "",
2043 },
2044 FieldSpec {
2045 xpath: "ltx:bib-part[@role='volume']",
2046 punct: ", ",
2047 pre: "Vol. ",
2048 class: "volume",
2049 formatter: Formatter::Any,
2050 post: "",
2051 },
2052 FieldSpec {
2053 xpath: "ltx:bib-part[@role='part']",
2054 punct: ", ",
2055 pre: "Part ",
2056 class: "part",
2057 formatter: Formatter::Any,
2058 post: "",
2059 },
2060 FieldSpec {
2061 xpath: "ltx:bib-publisher",
2062 punct: ", ",
2063 pre: " ",
2064 class: "publisher",
2065 formatter: Formatter::Any,
2066 post: "",
2067 },
2068 FieldSpec {
2069 xpath: "ltx:bib-organization",
2070 punct: ", ",
2071 pre: " ",
2072 class: "publisher",
2073 formatter: Formatter::Any,
2074 post: "",
2075 },
2076 FieldSpec {
2077 xpath: "ltx:bib-place",
2078 punct: ", ",
2079 pre: "",
2080 class: "place",
2081 formatter: Formatter::Any,
2082 post: "",
2083 },
2084 FieldSpec {
2085 xpath: "ltx:bib-status",
2086 punct: " ",
2087 pre: "(",
2088 class: "status",
2089 formatter: Formatter::Any,
2090 post: ")",
2091 },
2092 FieldSpec {
2093 xpath: "ltx:bib-language",
2094 punct: " ",
2095 pre: "(",
2096 class: "language",
2097 formatter: Formatter::Any,
2098 post: ")",
2099 },
2100 FieldSpec {
2101 xpath: "true",
2102 punct: ".",
2103 pre: "",
2104 class: "",
2105 formatter: Formatter::None,
2106 post: "",
2107 },
2108 ],
2109 ],
2110 "incollection" => vec![
2111 vec![
2112 FieldSpec {
2113 xpath: "ltx:bib-name[@role='author']",
2114 punct: "",
2115 pre: "",
2116 class: "author",
2117 formatter: Formatter::Authors,
2118 post: "",
2119 },
2120 FieldSpec {
2121 xpath: "ltx:bib-date[@role='publication']",
2122 punct: "",
2123 pre: "",
2124 class: "year",
2125 formatter: Formatter::Year,
2126 post: "",
2127 },
2128 ],
2129 vec![FieldSpec {
2130 xpath: "ltx:bib-title",
2131 punct: "",
2132 pre: "",
2133 class: "title",
2134 formatter: Formatter::Title,
2135 post: ".",
2136 }],
2137 vec![
2138 FieldSpec {
2139 xpath: "ltx:bib-type",
2140 punct: "",
2141 pre: "",
2142 class: "type",
2143 formatter: Formatter::Any,
2144 post: "",
2145 },
2146 FieldSpec {
2147 xpath: "ltx:bib-related[@bibrefs]",
2148 punct: " ",
2149 pre: "See ",
2150 class: "crossref",
2151 formatter: Formatter::CrossRef,
2152 post: ",",
2153 },
2154 FieldSpec {
2155 xpath: "ltx:bib-related[@type][not(../ltx:bib-related[@bibrefs])]/ltx:bib-title",
2156 punct: " ",
2157 pre: "In ",
2158 class: "inbook",
2159 formatter: Formatter::Title,
2160 post: ",",
2161 },
2162 FieldSpec {
2163 xpath: "ltx:bib-related[@type][not(../ltx:bib-related[@bibrefs])]/ltx:bib-name[@role='editor']",
2164 punct: " ",
2165 pre: " ",
2166 class: "editor",
2167 formatter: Formatter::EditorsA,
2168 post: ",",
2169 },
2170 ],
2171 vec![
2172 FieldSpec {
2173 xpath: "ltx:bib-edition",
2174 punct: "",
2175 pre: "",
2176 class: "edition",
2177 formatter: Formatter::Edition,
2178 post: "",
2179 },
2180 FieldSpec {
2181 xpath: "ltx:bib-name[@role='editor']",
2182 punct: ", ",
2183 pre: "",
2184 class: "editor",
2185 formatter: Formatter::EditorsB,
2186 post: "",
2187 },
2188 FieldSpec {
2189 xpath: "ltx:bib-related/ltx:bib-part[@role='series']",
2190 punct: ", ",
2191 pre: "",
2192 class: "series",
2193 formatter: Formatter::Any,
2194 post: "",
2195 },
2196 FieldSpec {
2197 xpath: "ltx:bib-related/ltx:bib-part[@role='volume']",
2198 punct: ", ",
2199 pre: "Vol. ",
2200 class: "volume",
2201 formatter: Formatter::Any,
2202 post: "",
2203 },
2204 FieldSpec {
2205 xpath: "ltx:bib-related/ltx:bib-part[@role='part']",
2206 punct: ", ",
2207 pre: "Part ",
2208 class: "part",
2209 formatter: Formatter::Any,
2210 post: "",
2211 },
2212 FieldSpec {
2213 xpath: "ltx:bib-publisher",
2214 punct: ", ",
2215 pre: " ",
2216 class: "publisher",
2217 formatter: Formatter::Any,
2218 post: "",
2219 },
2220 FieldSpec {
2221 xpath: "ltx:bib-organization",
2222 punct: ", ",
2223 pre: "",
2224 class: "publisher",
2225 formatter: Formatter::Any,
2226 post: "",
2227 },
2228 FieldSpec {
2229 xpath: "ltx:bib-place",
2230 punct: ", ",
2231 pre: "",
2232 class: "place",
2233 formatter: Formatter::Any,
2234 post: "",
2235 },
2236 FieldSpec {
2237 xpath: "ltx:bib-part[@role='pages']",
2238 punct: ", ",
2239 pre: "",
2240 class: "pages",
2241 formatter: Formatter::Pages,
2242 post: "",
2243 },
2244 FieldSpec {
2245 xpath: "ltx:bib-status",
2246 punct: " ",
2247 pre: "(",
2248 class: "status",
2249 formatter: Formatter::Any,
2250 post: ")",
2251 },
2252 FieldSpec {
2253 xpath: "ltx:bib-language",
2254 punct: " ",
2255 pre: "(",
2256 class: "language",
2257 formatter: Formatter::Any,
2258 post: ")",
2259 },
2260 FieldSpec {
2261 xpath: "true",
2262 punct: ".",
2263 pre: "",
2264 class: "",
2265 formatter: Formatter::None,
2266 post: "",
2267 },
2268 ],
2269 ],
2270 "report" => vec![
2271 vec![
2272 FieldSpec {
2273 xpath: "ltx:bib-name[@role='author']",
2274 punct: "",
2275 pre: "",
2276 class: "author",
2277 formatter: Formatter::Authors,
2278 post: "",
2279 },
2280 FieldSpec {
2281 xpath: "ltx:bib-name[@role='editor']",
2282 punct: "",
2283 pre: "",
2284 class: "editor",
2285 formatter: Formatter::EditorsA,
2286 post: "",
2287 },
2288 FieldSpec {
2289 xpath: "ltx:bib-date[@role='publication']",
2290 punct: "",
2291 pre: "",
2292 class: "year",
2293 formatter: Formatter::Year,
2294 post: "",
2295 },
2296 ],
2297 vec![FieldSpec {
2298 xpath: "ltx:bib-title",
2299 punct: "",
2300 pre: "",
2301 class: "title",
2302 formatter: Formatter::Title,
2303 post: ".",
2304 }],
2305 vec![FieldSpec {
2306 xpath: "ltx:bib-type",
2307 punct: "",
2308 pre: "",
2309 class: "type",
2310 formatter: Formatter::Any,
2311 post: "",
2312 }],
2313 vec![
2314 FieldSpec {
2315 xpath: "ltx:bib-part[@role='number']",
2316 punct: "",
2317 pre: "Technical Report ",
2318 class: "number",
2319 formatter: Formatter::Any,
2320 post: "",
2321 },
2322 FieldSpec {
2323 xpath: "ltx:bib-part[@role='series']",
2324 punct: ", ",
2325 pre: "",
2326 class: "series",
2327 formatter: Formatter::Any,
2328 post: "",
2329 },
2330 FieldSpec {
2331 xpath: "ltx:bib-part[@role='volume']",
2332 punct: ", ",
2333 pre: "Vol. ",
2334 class: "volume",
2335 formatter: Formatter::Any,
2336 post: "",
2337 },
2338 FieldSpec {
2339 xpath: "ltx:bib-part[@role='part']",
2340 punct: ", ",
2341 pre: "Part ",
2342 class: "part",
2343 formatter: Formatter::Any,
2344 post: "",
2345 },
2346 FieldSpec {
2347 xpath: "ltx:bib-publisher",
2348 punct: ", ",
2349 pre: " ",
2350 class: "publisher",
2351 formatter: Formatter::Any,
2352 post: "",
2353 },
2354 FieldSpec {
2355 xpath: "ltx:bib-organization",
2356 punct: ", ",
2357 pre: " ",
2358 class: "publisher",
2359 formatter: Formatter::Any,
2360 post: "",
2361 },
2362 FieldSpec {
2363 xpath: "ltx:bib-place",
2364 punct: ", ",
2365 pre: " ",
2366 class: "place",
2367 formatter: Formatter::Any,
2368 post: "",
2369 },
2370 FieldSpec {
2371 xpath: "ltx:bib-status",
2372 punct: ", ",
2373 pre: "(",
2374 class: "status",
2375 formatter: Formatter::Any,
2376 post: ")",
2377 },
2378 FieldSpec {
2379 xpath: "ltx:bib-language",
2380 punct: " ",
2381 pre: "(",
2382 class: "language",
2383 formatter: Formatter::Any,
2384 post: ")",
2385 },
2386 FieldSpec {
2387 xpath: "true",
2388 punct: ".",
2389 pre: "",
2390 class: "",
2391 formatter: Formatter::None,
2392 post: "",
2393 },
2394 ],
2395 ],
2396 "thesis" => vec![
2397 vec![
2398 FieldSpec {
2399 xpath: "ltx:bib-name[@role='author']",
2400 punct: "",
2401 pre: "",
2402 class: "author",
2403 formatter: Formatter::Authors,
2404 post: "",
2405 },
2406 FieldSpec {
2407 xpath: "ltx:bib-name[@role='editor']",
2408 punct: "",
2409 pre: "",
2410 class: "editor",
2411 formatter: Formatter::EditorsA,
2412 post: "",
2413 },
2414 FieldSpec {
2415 xpath: "ltx:bib-date[@role='publication']",
2416 punct: "",
2417 pre: "",
2418 class: "year",
2419 formatter: Formatter::Year,
2420 post: "",
2421 },
2422 ],
2423 vec![FieldSpec {
2424 xpath: "ltx:bib-title",
2425 punct: "",
2426 pre: "",
2427 class: "title",
2428 formatter: Formatter::Title,
2429 post: ".",
2430 }],
2431 vec![
2432 FieldSpec {
2433 xpath: "ltx:bib-type",
2434 punct: " ",
2435 pre: "",
2436 class: "type",
2437 formatter: Formatter::ThesisType,
2438 post: "",
2439 },
2440 FieldSpec {
2441 xpath: "ltx:bib-part[@role='part']",
2442 punct: ", ",
2443 pre: "Part ",
2444 class: "part",
2445 formatter: Formatter::Any,
2446 post: "",
2447 },
2448 FieldSpec {
2449 xpath: "ltx:bib-publisher",
2450 punct: ", ",
2451 pre: "",
2452 class: "publisher",
2453 formatter: Formatter::Any,
2454 post: "",
2455 },
2456 FieldSpec {
2457 xpath: "ltx:bib-organization",
2458 punct: ", ",
2459 pre: "",
2460 class: "publisher",
2461 formatter: Formatter::Any,
2462 post: "",
2463 },
2464 FieldSpec {
2465 xpath: "ltx:bib-place",
2466 punct: ", ",
2467 pre: "",
2468 class: "place",
2469 formatter: Formatter::Any,
2470 post: "",
2471 },
2472 FieldSpec {
2473 xpath: "ltx:bib-status",
2474 punct: ", ",
2475 pre: "(",
2476 class: "status",
2477 formatter: Formatter::Any,
2478 post: ")",
2479 },
2480 FieldSpec {
2481 xpath: "ltx:bib-language",
2482 punct: ", ",
2483 pre: "(",
2484 class: "language",
2485 formatter: Formatter::Any,
2486 post: ")",
2487 },
2488 FieldSpec {
2489 xpath: "true",
2490 punct: ".",
2491 pre: "",
2492 class: "",
2493 formatter: Formatter::None,
2494 post: "",
2495 },
2496 ],
2497 ],
2498 "website" => vec![
2499 vec![
2500 FieldSpec {
2501 xpath: "ltx:bib-name[@role='author']",
2502 punct: "",
2503 pre: "",
2504 class: "author",
2505 formatter: Formatter::Authors,
2506 post: "",
2507 },
2508 FieldSpec {
2509 xpath: "ltx:bib-name[@role='editor']",
2510 punct: "",
2511 pre: "",
2512 class: "editor",
2513 formatter: Formatter::EditorsA,
2514 post: "",
2515 },
2516 FieldSpec {
2517 xpath: "ltx:bib-date[@role='publication']",
2518 punct: "",
2519 pre: "",
2520 class: "year",
2521 formatter: Formatter::Year,
2522 post: "",
2523 },
2524 FieldSpec {
2525 xpath: "ltx:bib-title",
2526 punct: "",
2527 pre: "",
2528 class: "title",
2529 formatter: Formatter::Any,
2530 post: "",
2531 },
2532 FieldSpec {
2533 xpath: "ltx:bib-type",
2534 punct: "",
2535 pre: "",
2536 class: "type",
2537 formatter: Formatter::Any,
2538 post: "",
2539 },
2540 FieldSpec {
2541 xpath: "! ltx:bib-type",
2542 punct: "",
2543 pre: "",
2544 class: "type",
2545 formatter: Formatter::None,
2546 post: "(Website)",
2547 },
2548 ],
2549 vec![
2550 FieldSpec {
2551 xpath: "ltx:bib-organization",
2552 punct: ", ",
2553 pre: " ",
2554 class: "publisher",
2555 formatter: Formatter::Any,
2556 post: "",
2557 },
2558 FieldSpec {
2559 xpath: "ltx:bib-place",
2560 punct: ", ",
2561 pre: "",
2562 class: "place",
2563 formatter: Formatter::Any,
2564 post: "",
2565 },
2566 FieldSpec {
2567 xpath: "true",
2568 punct: ".",
2569 pre: "",
2570 class: "",
2571 formatter: Formatter::None,
2572 post: "",
2573 },
2574 ],
2575 ],
2576 "software" => vec![
2577 vec![
2578 FieldSpec {
2579 xpath: "ltx:bib-key",
2580 punct: "",
2581 pre: "",
2582 class: "key",
2583 formatter: Formatter::Any,
2584 post: "",
2585 },
2586 FieldSpec {
2587 xpath: "ltx:bib-type",
2588 punct: "",
2589 pre: "",
2590 class: "type",
2591 formatter: Formatter::Type,
2592 post: "",
2593 },
2594 ],
2595 vec![FieldSpec {
2596 xpath: "ltx:bib-title",
2597 punct: "",
2598 pre: "",
2599 class: "title",
2600 formatter: Formatter::Any,
2601 post: "",
2602 }],
2603 vec![
2604 FieldSpec {
2605 xpath: "ltx:bib-organization",
2606 punct: ", ",
2607 pre: " ",
2608 class: "publisher",
2609 formatter: Formatter::Any,
2610 post: "",
2611 },
2612 FieldSpec {
2613 xpath: "ltx:bib-place",
2614 punct: ", ",
2615 pre: "",
2616 class: "place",
2617 formatter: Formatter::Any,
2618 post: "",
2619 },
2620 FieldSpec {
2621 xpath: "true",
2622 punct: ".",
2623 pre: "",
2624 class: "",
2625 formatter: Formatter::None,
2626 post: "",
2627 },
2628 ],
2629 ],
2630 _ => vec![
2631 vec![
2633 FieldSpec {
2634 xpath: "ltx:bib-name[@role='author']",
2635 punct: "",
2636 pre: "",
2637 class: "author",
2638 formatter: Formatter::Authors,
2639 post: "",
2640 },
2641 FieldSpec {
2642 xpath: "ltx:bib-date[@role='publication']",
2643 punct: "",
2644 pre: "",
2645 class: "year",
2646 formatter: Formatter::Year,
2647 post: "",
2648 },
2649 ],
2650 vec![FieldSpec {
2651 xpath: "ltx:bib-title",
2652 punct: "",
2653 pre: "",
2654 class: "title",
2655 formatter: Formatter::Title,
2656 post: ".",
2657 }],
2658 ],
2659 };
2660 blocks.extend(meta_block);
2661 blocks
2662}
2663
2664fn field_content(node: &Node) -> Vec<NodeData> {
2683 let mut children = Vec::new();
2684 let mut child = node.get_first_child();
2685 let mut has_element = false;
2686 while let Some(n) = child {
2687 has_element |= n.get_type() == Some(libxml::tree::NodeType::ElementNode);
2688 child = n.get_next_sibling();
2689 children.push(n);
2690 }
2691 if has_element {
2692 children.into_iter().map(NodeData::XmlNode).collect()
2693 } else {
2694 vec![NodeData::Text(node.get_content())]
2695 }
2696}
2697
2698fn apply_formatter(doc: &PostDocument, formatter: Formatter, nodes: &[Node]) -> Vec<NodeData> {
2702 match formatter {
2703 Formatter::Any => nodes.iter().flat_map(field_content).collect(),
2704 Formatter::Authors => format_author_nodes(doc, nodes),
2705 Formatter::EditorsA => {
2706 let mut result = format_author_nodes(doc, nodes);
2707 let suffix = if nodes.len() > 1 { " (Eds.)" } else { " (Ed.)" };
2708 result.push(NodeData::Text(suffix.to_string()));
2709 result
2710 },
2711 Formatter::EditorsB => {
2712 let mut result = vec![NodeData::Text("(".to_string())];
2713 result.extend(format_author_nodes(doc, nodes));
2714 let suffix = if nodes.len() > 1 { " Eds.)" } else { " Ed.)" };
2715 result.push(NodeData::Text(suffix.to_string()));
2716 result
2717 },
2718 Formatter::Year => {
2719 let suffix = "";
2729 let content: Vec<NodeData> = nodes
2730 .iter()
2731 .map(|n| {
2732 let text = n.get_content();
2733 let year = extract_four_digit_year(&text);
2734 NodeData::Text(year)
2735 })
2736 .collect();
2737 let mut result = vec![NodeData::Text(" (".to_string())];
2738 result.extend(content);
2739 result.push(NodeData::Text(format!("{})", suffix)));
2740 result
2741 },
2742 Formatter::Type => {
2743 let mut result = vec![NodeData::Text("(".to_string())];
2744 result.extend(nodes.iter().flat_map(field_content));
2745 result.push(NodeData::Text(")".to_string()));
2746 result
2747 },
2748 Formatter::Title => nodes.iter().flat_map(field_content).collect(),
2749 Formatter::ThesisType => nodes.iter().flat_map(field_content).collect(),
2750 Formatter::Edition => {
2751 let mut result: Vec<NodeData> = nodes.iter().flat_map(field_content).collect();
2752 result.push(NodeData::Text(" edition".to_string()));
2753 result
2754 },
2755 Formatter::Pages => {
2756 let mut result = vec![NodeData::Text("pp.\u{00A0}".to_string())]; result.extend(nodes.iter().flat_map(field_content));
2758 result
2759 },
2760 Formatter::CrossRef => {
2761 if let Some(node) = nodes.first() {
2763 if let Some(bibrefs) = node.get_attribute("bibrefs") {
2764 return vec![NodeData::Element {
2765 tag: "ltx:cite".to_string(),
2766 attributes: None,
2767 children: vec![NodeData::Element {
2768 tag: "ltx:bibref".to_string(),
2769 attributes: Some(HashMap::from_iter([
2770 ("bibrefs".to_string(), bibrefs),
2771 ("show".to_string(), "title, author".to_string()),
2772 ])),
2773 children: vec![],
2774 }],
2775 }];
2776 }
2777 }
2778 Vec::new()
2779 },
2780 Formatter::Links => format_links(doc, nodes),
2781 Formatter::None => Vec::new(),
2782 }
2783}
2784
2785fn format_author_nodes(_doc: &PostDocument, name_nodes: &[Node]) -> Vec<NodeData> {
2789 let mut result: Vec<NodeData> = Vec::new();
2790 let mut names: Vec<Node> = name_nodes.to_vec();
2791
2792 let etal = names
2794 .last()
2795 .map(|n| n.get_content().trim() == "others")
2796 .unwrap_or(false);
2797 if etal {
2798 names.pop();
2799 }
2800
2801 let sep = if names.len() > 2 { ", " } else { " " };
2802
2803 for (i, name) in names.iter().enumerate() {
2804 if i > 0 {
2805 result.push(NodeData::Text(sep.to_string()));
2806 if !etal && i == names.len() - 1 {
2807 result.push(NodeData::Text("and ".to_string()));
2808 }
2809 }
2810 if let Some(givenname) = PostDocument::findnodes_foreign("ltx:givenname", name)
2812 .into_iter()
2813 .next()
2814 {
2815 let given_text = givenname.get_content();
2816 let initials: String = given_text
2817 .split_whitespace()
2818 .map(|word| {
2819 if word.ends_with('.') {
2820 format!("{} ", word)
2821 } else if let Some(first) = word.chars().next() {
2822 format!("{}. ", first)
2823 } else {
2824 String::new()
2825 }
2826 })
2827 .collect();
2828 result.push(NodeData::Text(initials));
2829 }
2830 if let Some(surname) = PostDocument::findnodes_foreign("ltx:surname", name)
2831 .into_iter()
2832 .next()
2833 {
2834 result.push(NodeData::Text(surname.get_content()));
2835 }
2836 }
2837
2838 if etal {
2839 result.push(NodeData::Text(sep.to_string()));
2840 result.push(NodeData::Element {
2841 tag: "ltx:text".to_string(),
2842 attributes: Some(HashMap::from_iter([(
2843 "class".to_string(),
2844 "ltx_bib_etal".to_string(),
2845 )])),
2846 children: vec![NodeData::Text("et al.".to_string())],
2847 });
2848 }
2849
2850 result
2851}
2852
2853fn format_links(doc: &PostDocument, nodes: &[Node]) -> Vec<NodeData> {
2857 let mut links: Vec<NodeData> = Vec::new();
2858
2859 for node in nodes {
2860 let tag = doc.get_qname(node).unwrap_or_default();
2861 let scheme = node.get_attribute("scheme").unwrap_or_default();
2862 let href = node.get_attribute("href");
2863 let content_text = node.get_content();
2864
2865 let href = match (&href, scheme.as_str()) {
2871 (None, "doi") if !content_text.trim().is_empty() && content_text.contains('/') => {
2872 Some(doi_href(&content_text))
2873 },
2874 (Some(h), "doi") if !h.contains("://") => Some(doi_href(h.trim_start_matches('/'))),
2875 (Some(h), _) => Some(force_absolute_url(h)),
2876 (None, _) => None,
2877 };
2878 let children: Vec<NodeData> = node
2888 .get_child_nodes()
2889 .into_iter()
2890 .map(NodeData::XmlNode)
2891 .collect();
2892 match tag.as_str() {
2893 "ltx:bib-identifier" | "ltx:bib-review" => {
2894 if let Some(href) = href {
2895 links.push(NodeData::Element {
2896 tag: "ltx:ref".to_string(),
2897 attributes: Some(HashMap::from_iter([
2898 ("href".to_string(), href),
2899 ("class".to_string(), format!("{} ltx_bib_external", scheme)),
2900 ])),
2901 children,
2902 });
2903 } else {
2904 links.push(NodeData::Element {
2905 tag: "ltx:text".to_string(),
2906 attributes: Some(HashMap::from_iter([(
2907 "class".to_string(),
2908 format!("{} ltx_bib_external", scheme),
2909 )])),
2910 children,
2911 });
2912 }
2913 },
2914 "ltx:bib-links" => {
2915 links.push(NodeData::Element {
2916 tag: "ltx:text".to_string(),
2917 attributes: Some(HashMap::from_iter([(
2918 "class".to_string(),
2919 "ltx_bib_external".to_string(),
2920 )])),
2921 children,
2922 });
2923 },
2924 "ltx:bib-url" => {
2925 if let Some(href) = href {
2926 links.push(NodeData::Element {
2927 tag: "ltx:ref".to_string(),
2928 attributes: Some(HashMap::from_iter([
2929 ("href".to_string(), href),
2930 ("class".to_string(), "ltx_bib_external".to_string()),
2931 ])),
2932 children,
2933 });
2934 }
2935 },
2936 _ => {},
2937 }
2938 }
2939
2940 if links.len() > 1 {
2942 let mut result = Vec::new();
2943 for (i, link) in links.into_iter().enumerate() {
2944 if i > 0 {
2945 result.push(NodeData::Text(",\n".to_string()));
2946 }
2947 result.push(link);
2948 }
2949 result
2950 } else {
2951 links
2952 }
2953}
2954
2955fn unisort(keys: &mut [String]) {
2982 keys.sort_by_cached_key(|k| (collation_primary_key(k), k.clone()));
2983}
2984
2985fn is_citation_order_style(bibstyle: &str) -> bool {
2992 matches!(bibstyle, "unsrt" | "unsrtnat" | "ieeetr" | "IEEEtran")
2993}
2994
2995fn citation_order(doc: &PostDocument) -> HashMap<String, usize> {
3010 let mut order: HashMap<String, usize> = HashMap::default();
3011 let mut next = 0usize;
3012 for node in doc.findnodes("//ltx:bibref[not(ancestor::ltx:bibliography)]") {
3013 let Some(refs) = node.get_attribute("bibrefs") else {
3014 continue;
3015 };
3016 for key in refs.split(',') {
3017 let k = key.trim().to_lowercase();
3018 if k.is_empty() {
3019 continue;
3020 }
3021 order.entry(k).or_insert_with(|| {
3022 let i = next;
3023 next += 1;
3024 i
3025 });
3026 }
3027 }
3028 order
3029}
3030
3031fn order_entry_keys(
3037 entries: &HashMap<String, BibEntryData>,
3038 cite_order: Option<&HashMap<String, usize>>,
3039) -> Vec<String> {
3040 match cite_order {
3041 Some(order) => {
3042 let mut cited: Vec<(usize, String)> = Vec::new();
3043 let mut uncited: Vec<String> = Vec::new();
3044 for (sort_key, entry) in entries {
3045 match order.get(&entry.bib_key.to_lowercase()) {
3046 Some(&idx) => cited.push((idx, sort_key.clone())),
3047 None => uncited.push(sort_key.clone()),
3048 }
3049 }
3050 cited.sort_by_key(|(idx, _)| *idx);
3051 unisort(&mut uncited);
3052 cited.into_iter().map(|(_, k)| k).chain(uncited).collect()
3053 },
3054 None => {
3055 let mut keys: Vec<String> = entries.keys().cloned().collect();
3056 unisort(&mut keys);
3057 keys
3058 },
3059 }
3060}
3061
3062fn collation_primary_key(s: &str) -> String {
3065 use unicode_normalization::{UnicodeNormalization, char::is_combining_mark};
3066 s.nfd()
3067 .filter(|c| !is_combining_mark(*c))
3068 .flat_map(char::to_lowercase)
3069 .collect()
3070}
3071
3072fn extract_names(doc: &PostDocument, bibentry: &Node) -> (String, String, String) {
3077 let mut name_nodes: Vec<Node> =
3078 PostDocument::findnodes_foreign("ltx:bib-name[@role='author']", bibentry);
3079 if name_nodes.is_empty() {
3080 name_nodes = PostDocument::findnodes_foreign("ltx:bib-name[@role='editor']", bibentry);
3081 }
3082
3083 if name_nodes.is_empty() {
3084 if let Some(key_node) = PostDocument::findnodes_foreign("ltx:bib-key", bibentry)
3086 .into_iter()
3087 .next()
3088 {
3089 let text = key_node.get_content();
3090 return (text.clone(), text.clone(), text);
3091 }
3092 if let Some(title_node) = PostDocument::findnodes_foreign("ltx:bib-title", bibentry)
3094 .into_iter()
3095 .next()
3096 {
3097 let text = title_node.get_content();
3098 return (text.clone(), text.clone(), text);
3099 }
3100 return (String::new(), String::new(), String::new());
3101 }
3102
3103 let sort_names: String = name_nodes
3105 .iter()
3106 .map(|n| get_name_text(doc, n))
3107 .collect::<Vec<_>>()
3108 .join(" ");
3109
3110 let surnames: Vec<String> = name_nodes
3112 .iter()
3113 .filter_map(|n| {
3114 PostDocument::findnodes_foreign("ltx:surname", n)
3115 .into_iter()
3116 .next()
3117 .map(|s| s.get_content())
3118 })
3119 .collect();
3120
3121 let short_names = if surnames.len() > 2 {
3122 format!("{} et al", surnames[0])
3123 } else if surnames.len() == 2 {
3124 format!("{} and {}", surnames[0], surnames[1])
3125 } else if !surnames.is_empty() {
3126 surnames[0].clone()
3127 } else {
3128 String::new()
3129 };
3130
3131 let full_names = surnames.join(", ");
3132 (sort_names, short_names, full_names)
3133}
3134
3135fn do_name_text(namenode: &Node) -> String {
3143 let mut out = String::new();
3144 if let Some(given) = PostDocument::findnodes_foreign("ltx:givenname", namenode)
3145 .into_iter()
3146 .next()
3147 {
3148 for word in given.get_content().split_whitespace() {
3149 if word.ends_with('.') {
3150 out.push_str(word);
3151 out.push(' ');
3152 } else if let Some(c) = word.chars().next() {
3153 out.push(c);
3154 out.push_str(". ");
3155 }
3156 }
3157 }
3158 if let Some(surname) = PostDocument::findnodes_foreign("ltx:surname", namenode)
3159 .into_iter()
3160 .next()
3161 {
3162 out.push_str(&surname.get_content());
3163 }
3164 out
3165}
3166
3167fn do_names_short(mut names: Vec<Node>) -> Vec<NodeData> {
3181 let surname_text = |n: &Node| -> String {
3182 PostDocument::findnodes_foreign("ltx:surname", n)
3183 .into_iter()
3184 .next()
3185 .map(|s| s.get_content())
3186 .unwrap_or_else(|| n.get_content())
3187 .trim()
3188 .to_string()
3189 };
3190 let mut etal = names
3191 .last()
3192 .map(|n| n.get_content().trim() == "others")
3193 .unwrap_or(false);
3194 if etal {
3195 names.pop();
3196 }
3197 if names.len() > 2 {
3198 etal = true;
3199 }
3200 let etal_span = || NodeData::Element {
3201 tag: "ltx:text".to_string(),
3202 attributes: Some(HashMap::from_iter([(
3203 "class".to_string(),
3204 "ltx_bib_etal".to_string(),
3205 )])),
3206 children: vec![NodeData::Text("et al.".to_string())],
3207 };
3208 match (names.len(), etal) {
3209 (0, _) => Vec::new(),
3210 (_, true) => vec![
3211 NodeData::Text(surname_text(&names[0])),
3212 NodeData::Text(" ".to_string()),
3213 etal_span(),
3214 ],
3215 (1, false) => vec![NodeData::Text(surname_text(&names[0]))],
3216 (_, false) => vec![
3217 NodeData::Text(surname_text(&names[0])),
3218 NodeData::Text(" and ".to_string()),
3219 NodeData::Text(surname_text(&names[1])),
3220 ],
3221 }
3222}
3223
3224fn do_names(mut names: Vec<Node>) -> Vec<NodeData> {
3230 let sep = if names.len() > 2 { ", " } else { " " };
3231 let mut etal = false;
3232 if names
3233 .last()
3234 .map(|n| n.get_content().trim() == "others")
3235 .unwrap_or(false)
3236 {
3237 names.pop();
3238 etal = true;
3239 }
3240 let last = names.len().saturating_sub(1);
3241 let mut out: Vec<NodeData> = Vec::new();
3242 for (i, name) in names.iter().enumerate() {
3243 if !out.is_empty() {
3244 out.push(NodeData::Text(sep.to_string()));
3245 if !etal && i == last {
3246 out.push(NodeData::Text("and ".to_string()));
3247 }
3248 }
3249 out.push(NodeData::Text(do_name_text(name)));
3250 }
3251 if etal {
3252 out.push(NodeData::Text(sep.to_string()));
3253 out.push(NodeData::Element {
3254 tag: "ltx:text".to_string(),
3255 attributes: Some(HashMap::from_iter([(
3256 "class".to_string(),
3257 "ltx_bib_etal".to_string(),
3258 )])),
3259 children: vec![NodeData::Text("et al.".to_string())],
3260 });
3261 }
3262 out
3263}
3264
3265fn do_editors_a(names: Vec<Node>) -> Vec<NodeData> {
3268 let n = names.len();
3269 let mut out = do_names(names);
3270 if n > 1 {
3271 out.push(NodeData::Text(" (Eds.)".to_string()));
3272 } else if n == 1 {
3273 out.push(NodeData::Text(" (Ed.)".to_string()));
3274 }
3275 out
3276}
3277
3278fn get_name_text(_doc: &PostDocument, namenode: &Node) -> String {
3282 let surname = PostDocument::findnodes_foreign("ltx:surname", namenode)
3283 .into_iter()
3284 .next()
3285 .map(|n| n.get_content());
3286 let givenname = PostDocument::findnodes_foreign("ltx:givenname", namenode)
3287 .into_iter()
3288 .next()
3289 .map(|n| n.get_content());
3290 match (surname, givenname) {
3291 (Some(s), Some(g)) => format!("{} {}", s, g),
3292 (Some(s), None) => s,
3293 (None, Some(g)) => g,
3294 (None, None) => String::new(),
3295 }
3296}
3297
3298fn extract_four_digit_year(text: &str) -> String {
3300 if let Some(start) = text.find(|c: char| c.is_ascii_digit()) {
3301 let digits: String = text[start..]
3302 .chars()
3303 .take_while(|c| c.is_ascii_digit())
3304 .collect();
3305 if digits.len() >= 4 {
3306 return digits[..4].to_string();
3307 }
3308 }
3309 text.to_string()
3310}
3311
3312fn suffix_to_counter(suffix: &str) -> u32 {
3314 let mut n = 0u32;
3315 for c in suffix.chars() {
3316 n = n * 26 + (c as u32 - 'a' as u32 + 1);
3317 }
3318 n
3319}
3320
3321fn match_metadata_field(xpath: &str, entry: &BibEntryData) -> bool {
3323 let xpath = xpath.trim_start_matches('!').trim();
3324 match xpath {
3325 "true" => true,
3326 s if s.contains("bib-name[@role='author']") => !entry.authors_short.is_empty(),
3327 s if s.contains("bib-name[@role='editor']") => false, s if s.contains("bib-date[@role='publication']") => !entry.year.is_empty(),
3329 s if s.contains("bib-title") => !entry.title.is_empty(),
3330 _ => false,
3331 }
3332}
3333
3334fn get_metadata_content(xpath: &str, entry: &BibEntryData) -> Vec<NodeData> {
3336 let xpath = xpath.trim_start_matches('!').trim();
3337 match xpath {
3338 s if s.contains("bib-name[@role='author']") && !entry.authors_full.is_empty() => {
3339 vec![NodeData::Text(format_authors_text(&entry.authors_full))]
3340 },
3341 s if s.contains("bib-date[@role='publication']") && !entry.year.is_empty() => {
3342 vec![NodeData::Text(entry.year.clone())]
3343 },
3344 s if s.contains("bib-title") && !entry.title.is_empty() => {
3345 vec![NodeData::Text(entry.title.clone())]
3346 },
3347 _ => Vec::new(),
3348 }
3349}
3350
3351fn format_authors_text(authors: &str) -> String {
3353 let names: Vec<&str> = authors.split(" and ").collect();
3354 let n = names.len();
3355 if n == 0 {
3356 return authors.to_string();
3357 }
3358
3359 let has_etal = names.last().map(|n| n.trim() == "others").unwrap_or(false);
3360 let real_names: Vec<&str> = if has_etal {
3361 names[..n - 1].to_vec()
3362 } else {
3363 names
3364 };
3365
3366 let formatted: Vec<String> = real_names
3367 .iter()
3368 .map(|name| format_single_name(name.trim()))
3369 .collect();
3370
3371 let mut result = String::new();
3372 let sep = if formatted.len() > 2 { ", " } else { " " };
3373 for (i, name) in formatted.iter().enumerate() {
3374 if i > 0 {
3375 result.push_str(sep);
3376 if !has_etal && i == formatted.len() - 1 {
3377 result.push_str("and ");
3378 }
3379 }
3380 result.push_str(name);
3381 }
3382 if has_etal {
3383 result.push_str(sep);
3384 result.push_str("et al.");
3385 }
3386 result
3387}
3388
3389fn format_single_name(name: &str) -> String {
3393 if let Some((surname, given)) = name.split_once(',') {
3394 let surname = surname.trim();
3395 let initials: String = given
3396 .split_whitespace()
3397 .map(|word| {
3398 if word.ends_with('.') {
3399 format!("{} ", word)
3400 } else if let Some(first) = word.chars().next() {
3401 format!("{}. ", first)
3402 } else {
3403 String::new()
3404 }
3405 })
3406 .collect();
3407 format!("{}{}", initials, surname)
3408 } else {
3409 name.to_string()
3410 }
3411}
3412
3413fn clone_entry(e: &BibEntryData) -> BibEntryData {
3415 BibEntryData {
3416 bib_key: e.bib_key.clone(),
3417 cited_key: e.cited_key.clone(),
3418 sort_key: e.sort_key.clone(),
3419 initial: e.initial.clone(),
3420 author_year: e.author_year.clone(),
3421 suffix: e.suffix.clone(),
3422 authors_short: e.authors_short.clone(),
3423 authors_full: e.authors_full.clone(),
3424 sort_names: e.sort_names.clone(),
3425 year: e.year.clone(),
3426 title: e.title.clone(),
3427 entry_type: e.entry_type.clone(),
3428 number: e.number,
3429 referrers: e.referrers.clone(),
3430 bibreferrers: e.bibreferrers.clone(),
3431 citations: e.citations.clone(),
3432 bibentry: e.bibentry.clone(),
3433 }
3434}
3435
3436fn make_bibblock(class: &str, content: &[NodeData]) -> NodeData {
3438 let mut attrs = HashMap::default();
3439 attrs.insert("xml:space".to_string(), "preserve".to_string());
3440 if !class.is_empty() {
3441 attrs.insert("class".to_string(), class.to_string());
3442 }
3443 NodeData::Element {
3444 tag: "ltx:bibblock".to_string(),
3445 attributes: Some(attrs),
3446 children: content.to_vec(),
3447 }
3448}
3449
3450fn find_file(name: &str, search_paths: &[String]) -> Option<String> {
3464 latexml_core::util::pathname::find(name, latexml_core::util::pathname::PathnameFindOptions {
3465 paths: Some(search_paths.to_vec()),
3466 ..Default::default()
3467 })
3468}
3469
3470fn doi_href(doi: &str) -> String {
3474 let mut href = String::from("https://doi.org/");
3475 for c in doi.trim().chars() {
3476 if c.is_ascii_alphanumeric() || matches!(c, '.' | '/' | '-' | '+') {
3477 href.push(c);
3478 } else {
3479 let mut buf = [0u8; 4];
3480 for &b in c.encode_utf8(&mut buf).as_bytes() {
3481 href.push_str(&format!("%{:02X}", b));
3482 }
3483 }
3484 }
3485 href
3486}
3487
3488fn force_absolute_url(url: &str) -> String {
3491 let u = url.trim();
3492 if u.is_empty() || u.contains("://") || u.starts_with("mailto:") {
3493 u.to_string()
3494 } else {
3495 format!("https://{}", u)
3496 }
3497}
3498
3499#[cfg(test)]
3500mod tests {
3501 use super::*;
3502
3503 #[test]
3504 fn test_extract_four_digit_year() {
3505 assert_eq!(extract_four_digit_year("2024"), "2024");
3506 assert_eq!(
3507 extract_four_digit_year("Published in 2024, January"),
3508 "2024"
3509 );
3510 assert_eq!(extract_four_digit_year("99"), "99");
3511 assert_eq!(extract_four_digit_year(""), "");
3512 }
3513
3514 #[test]
3515 fn test_find_file_case_insensitive_bib() {
3516 let dir = std::env::temp_dir().join("lxo_bib_case_test");
3522 let _ = std::fs::create_dir_all(&dir);
3523 let on_disk = dir.join("Evoflock.bib");
3524 std::fs::write(&on_disk, "@article{k, title={T}}\n").unwrap();
3525 let dirs = vec![dir.to_str().unwrap().to_string()];
3526 assert!(find_file("Evoflock.bib", &dirs).is_some());
3528 let hit = find_file("EvoFlock.bib", &dirs);
3530 assert!(
3531 hit.is_some(),
3532 "case-mismatched bib filename should resolve via the pathname fallback"
3533 );
3534 assert!(hit.unwrap().to_lowercase().ends_with("evoflock.bib"));
3535 assert!(find_file("NoSuchBib.bib", &dirs).is_none());
3537 let _ = std::fs::remove_dir_all(&dir);
3538 }
3539
3540 #[test]
3541 fn test_suffix_to_counter() {
3542 assert_eq!(suffix_to_counter("a"), 1);
3543 assert_eq!(suffix_to_counter("b"), 2);
3544 assert_eq!(suffix_to_counter("z"), 26);
3545 assert_eq!(suffix_to_counter("aa"), 27);
3546 }
3547
3548 #[test]
3549 fn test_format_single_name() {
3550 assert_eq!(format_single_name("Smith, John"), "J. Smith");
3551 assert_eq!(format_single_name("Smith, J."), "J. Smith");
3552 assert_eq!(format_single_name("Smith, John Robert"), "J. R. Smith");
3553 assert_eq!(format_single_name("Smith"), "Smith");
3554 }
3555
3556 #[test]
3557 fn test_format_authors_text() {
3558 assert_eq!(format_authors_text("Smith"), "Smith");
3559 assert_eq!(
3560 format_authors_text("Smith, John and Doe, Jane"),
3561 "J. Smith and J. Doe"
3562 );
3563 assert_eq!(
3564 format_authors_text("Smith, J. and Doe, J. and Roe, R."),
3565 "J. Smith, J. Doe, and R. Roe"
3566 );
3567 }
3568
3569 #[test]
3570 fn test_fmt_spec_coverage() {
3571 for fmt in &[
3573 "article",
3574 "book",
3575 "incollection",
3576 "report",
3577 "thesis",
3578 "website",
3579 "software",
3580 ] {
3581 let specs = get_fmt_spec(fmt);
3582 assert!(
3583 !specs.is_empty(),
3584 "FMT_SPEC for '{}' should not be empty",
3585 fmt
3586 );
3587 }
3588 }
3589}