Coverage for pybtexnbib/parsers.py: 100.00%
100 statements
« prev ^ index » next coverage.py v7.6.1, created at 2026-07-31 06:25 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2026-07-31 06:25 +0000
1import re
2from dataclasses import dataclass
3from collections import defaultdict
4from pathlib import Path
5from pybtex.database.input import BaseParser
6from pybtex.database import Entry, Person
7import csv
8from warnings import warn
9from pybtexris.parsers import clean_entry_key, get_entry_key
12data_dir = Path(__file__).parent/"data"
14@dataclass
15class NBIBField:
16 """ A field in a NBIB file. """
17 code: str
18 value: str
21class NBIBParser(BaseParser):
22 """
23 Parser for NBIB/Medline/PubMed citation files.
25 For information, see:
26 https://www.nlm.nih.gov/bsd/policy/cit_format.html
27 https://www.nlm.nih.gov/bsd/mms/medlineelements.html
28 """
29 default_suffix = '.nbib'
30 unicode_io = True
32 def __init__(self, *args, **kwargs):
33 super().__init__(*args, **kwargs)
34 self.nbib_type_to_bibtex = {}
36 with open(data_dir/"types.csv") as f:
37 reader = csv.reader(f, delimiter=',')
39 #skip the header
40 next(reader, None)
42 for row in reader:
43 nbib_type = row[0]
44 bibtex_type = row[1]
45 self.nbib_type_to_bibtex[nbib_type] = bibtex_type
47 def parse_stream(self, stream):
48 text = stream.read()
49 return self.parse_string(text)
51 def process_entry(self, entry_text):
52 # Read file into list and merge multi-line entries
53 nbib_fields = []
54 for line in entry_text.split('\n'):
55 m = re.match(r"^([A-Z]{2,4})\s*-\s*(.*)$", line.strip())
56 if m:
57 code = m.group(1)
58 value = m.group(2).strip()
59 nbib_fields.append( NBIBField(code=code, value=value) )
60 elif len(nbib_fields) > 0:
61 # If the line doesn't match the pattern then append the text to the previous field
62 nbib_fields[-1].value += line.strip()
63 else:
64 warn(f"First line of NBIB file '{line}' is invalid.")
65 continue
67 # Parse nbib fields
68 nbib_dict = defaultdict(list)
69 for field in nbib_fields:
70 nbib_dict[field.code].append(field.value)
72 # Get publication type
73 nbib_publication_types = nbib_dict.pop("PT", [])
74 bibtex_types = {
75 self.nbib_type_to_bibtex[nbib_type]
76 for nbib_type in nbib_publication_types
77 if nbib_type in self.nbib_type_to_bibtex
78 }
79 bibtex_type = bibtex_types.pop() if len(bibtex_types) == 1 else "misc"
80 nbib_publication_description = "; ".join(nbib_publication_types)
82 # Create Entry object
83 entry = Entry(bibtex_type)
84 if nbib_publication_description:
85 entry.fields["type"] = nbib_publication_description
87 # Read People
88 def add_person(code, role):
89 names = nbib_dict.pop(code, [])
90 for name in names:
91 person = Person(name)
92 entry.add_person(person, role)
94 add_person("FAU", "author")
95 add_person("FED", "editor")
96 # what should be done for AU and ED?
98 # Read Other Fields
99 def add_field(code, bibtex_field, delimiter="; "):
100 values = nbib_dict.pop(code, [])
101 for value in values:
102 if bibtex_field in entry.fields:
103 entry.fields[bibtex_field] += f"{delimiter}{value}"
104 else:
105 entry.fields[bibtex_field] = value
107 add_field("TI", "title")
108 add_field("JT", "journal")
109 add_field("JTI", "shortjournal")
110 add_field("DP", "date")
112 add_field("BTI", "booktitle")
113 add_field("PB", "publisher")
114 add_field("CY", "address")
115 add_field("VI", "volume")
116 add_field("PG", "pages")
117 add_field("OT", "keywords", delimiter=" | ")
118 add_field("GN", "note", delimiter=" | ")
119 add_field("ISBN", "isbn")
120 add_field("IS", "issn")
121 add_field("AB", "abstract")
123 add_field("AB", "abstract")
125 # Read year from date field if possible
126 if "date" in entry.fields:
127 m = re.match(r"^(\d{4})($|\D)", entry.fields['date'])
128 if m:
129 entry.fields['year'] = m.group(1)
131 # Get DOI
132 values = nbib_dict.pop("AID", [])
133 for value in values:
134 if "[doi]" in value:
135 entry.fields["doi"] = value.replace("[doi]", "").strip()
136 else:
137 nbib_dict["AID"].append(value)
139 # Add the remaining fields with the code as the field name
140 for code, values in nbib_dict.items():
141 entry.fields[code] = "; ".join(values)
143 entry_key = get_entry_key(entry)
145 return entry_key,entry
147 def parse_string(self, text):
148 self.unnamed_entry_counter = 1
149 self.command_start = 0
151 entry_texts = re.split(r"ER\s+-", text)
152 entries = (self.process_entry(t) for t in entry_texts if t.strip())
153 self.data.add_entries(entries)
154 return self.data