Coverage for pybtexris/parsers.py: 100.00%
164 statements
« prev ^ index » next coverage.py v7.6.1, created at 2026-07-31 06:23 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2026-07-31 06:23 +0000
1import re
2from collections import defaultdict
3from pathlib import Path
4from pybtex import database
5from pybtex.database.input import BaseParser
6from pybtex.database import BibliographyData, Entry, Person
7import csv
8import unicodedata
11def clean_entry_key(entry_key):
12 """
13 ensure that the entry key is only in ASCII encoding
14 """
15 return unicodedata.normalize('NFKD', entry_key).encode('ASCII', 'ignore').decode("ascii")
18def get_entry_key(entry):
19 """
20 Builds an entry key from an entry
21 """
22 people = [x[0] for x in entry.persons.values()]
23 if people:
24 first_author = people[0]
25 entry_key = "-".join(first_author.last_names).replace(" ", ".")
26 elif "title" in entry.fields:
27 TRUNCATE_TITLE = 20
28 entry_key = entry.fields["title"].replace(" ", ".")[:TRUNCATE_TITLE]
29 else:
30 entry_key = "Unknown"
32 if "year" in entry.fields:
33 entry_key += entry.fields["year"]
35 entry_key = clean_entry_key(entry_key)
36 return entry_key
39data_dir = Path(__file__).parent / "data"
42class RISParser(BaseParser):
43 """
44 Parser for RIS files.
45 """
47 default_suffix = '.ris'
48 unicode_io = True
50 def __init__(self, *args, **kwargs):
51 super().__init__(*args, **kwargs)
52 self.ris_type_to_bibtex = {}
53 self.ris_type_description = {}
55 with open(data_dir / "types.csv") as f:
56 reader = csv.reader(f, delimiter=',')
58 # skip the header
59 next(reader, None)
60 for row in reader:
61 ris_types = row[0]
62 description = row[1]
63 bibtex_type = row[2]
65 for ris_type in ris_types.split(", "):
66 self.ris_type_to_bibtex[ris_type] = bibtex_type
67 self.ris_type_description[ris_type] = description
69 def parse_stream(self, stream):
70 text = stream.read()
71 return self.parse_string(text)
73 def process_entry(self, entry_text):
74 """
75 https://github.com/aurimasv/translators/wiki/RIS-Tag-Map
76 """
77 # Read entry into dictionary
78 ris_dict = defaultdict(list)
79 for line in entry_text.split('\n'):
80 m = re.match(r"^([A-Z0-9]{2})\s*-\s*(.*)$", line.strip())
81 if not m:
82 continue
84 code = m.group(1)
85 value = m.group(2)
87 ris_dict[code].append(value)
89 # Read type of entry
90 ris_type = ris_dict.pop("TY", ["GEN"])
91 ris_type = ris_type[0]
92 ris_description = None
93 if ris_type in self.ris_type_description.values():
94 index = list(self.ris_type_description.values()).index(ris_type)
95 ris_description = ris_type
96 ris_type = list(self.ris_type_description.keys())[index]
98 ris_type = ris_type.upper()
99 if not ris_description:
100 ris_description = self.ris_type_description[ris_type]
102 # Create Entry object
103 bibtex_type = self.ris_type_to_bibtex.get(ris_type, "misc")
104 entry = Entry(bibtex_type)
105 entry.fields["type"] = ris_description
107 # Read People
108 def add_person(code, role):
109 names = ris_dict.pop(code, [])
110 for name in names:
111 person = Person(name)
112 entry.add_person(person, role)
114 add_person("AU", "author")
115 add_person("A1", "author")
116 editor_role = [
117 "ANCIENT",
118 "BLOG",
119 "BOOK",
120 "CHAP",
121 "CLSWK",
122 "COMP",
123 "DATA",
124 "CPAPER",
125 "CONF",
126 "DICT",
127 "EDBOOK",
128 "EBOOK",
129 "ECHAP",
130 "ENCYC",
131 "MAP",
132 "MUSIC",
133 "MULTI",
134 "RPRT",
135 "SER",
136 "UNPB",
137 "ELEC",
138 ]
139 add_person("A2", "editor" if ris_type in editor_role else "author")
140 editor_role += ["ADVS", "SLIDE", "SOUND", "VIDEO"]
141 add_person("A3", "editor" if ris_type in editor_role else "author")
142 add_person("A4", "editor" if ris_type in editor_role else "author")
143 add_person("ED", "editor")
145 # Read Other Fields
146 def add_field(code, bibtex_field, delimiter=", "):
147 values = ris_dict.pop(code, [])
148 for value in values:
149 if bibtex_field in entry.fields:
150 entry.fields[bibtex_field] += f"{delimiter}{value}"
151 else:
152 entry.fields[bibtex_field] = value
154 add_field("TI", "title")
155 if "title" not in entry.fields:
156 add_field("T1", "title")
157 add_field("JO", "journal")
159 bibtex_t2 = None
160 if ris_type in ["ABST", "INPR", "JFULL", "JOUR", "EJOUR"]:
161 bibtex_t2 = "journal"
162 elif ris_type in ["ANCIENT", "CHAP", "ECHAP"]:
163 bibtex_t2 = "booktitle"
164 elif ris_type in ["BOOK", "CTLG", "CLSWK", "COMP", "DATA", "MPCT", "MAP", "MULTI", "RPRT", "UNPB", "ELEC"]:
165 bibtex_t2 = "series"
166 if bibtex_t2:
167 add_field("T2", bibtex_t2)
169 bibtex_bt = bibtex_t2
170 if ris_type == "BOOK":
171 bibtex_bt = "title"
172 if bibtex_bt:
173 add_field("BT", bibtex_bt)
175 bibtex_t3 = None
176 if ris_type in [
177 "BOOK",
178 "CTLG",
179 "CLSWK",
180 "COMP",
181 "DATA",
182 "MPCT",
183 "MAP",
184 "MULTI",
185 "RPRT",
186 "UNPB",
187 "ELEC",
188 "ADVS",
189 "SLIDE",
190 "SOUND",
191 "VIDEO",
192 "CHAP",
193 "CONF",
194 "DATA",
195 "EBOOK",
196 "ECHAP",
197 "GOVDOC",
198 "MUSIC",
199 "SER",
200 ]:
201 bibtex_t3 = "series"
202 if bibtex_t3:
203 add_field("T3", bibtex_t3)
205 add_field("PB", "publisher")
206 add_field("CY", "address")
207 add_field("IS", "number")
208 add_field("DO", "doi")
209 add_field("VL", "volume")
210 add_field("SP", "pages")
211 add_field("UR", "url")
212 add_field("KW", "keywords", delimiter=" | ")
213 add_field("N1", "note", delimiter=" | ")
214 add_field("N2", "note", delimiter=" | ")
215 add_field("PY", "year")
216 add_field("AB", "abstract")
217 if "year" not in entry.fields:
218 add_field("Y1", "year")
220 # Read ISBN or ISSN
221 serial_numbers = ris_dict.pop("SN", [])
222 for serial_number in serial_numbers:
223 sn_digits = re.sub(r"\D", "", serial_number)
224 sn_field = "issn" if len(sn_digits) == 8 else "isbn"
225 entry.fields[sn_field] = serial_number
227 entry_key = ris_dict.pop("ID", [None])
228 entry_key = entry_key[0]
230 # Check if DA field could be a month
231 dates = ris_dict.get("DA", [])
232 for date in dates:
233 digital_month = date.isdigit() and (1 <= int(date) <= 12)
234 text_month = date[:3].lower() in [
235 "jan",
236 "feb",
237 "mar",
238 "apr",
239 "may",
240 "jun",
241 "jul",
242 "aug",
243 "sep",
244 "oct",
245 "nov",
246 "dec",
247 ]
248 if digital_month or text_month:
249 entry.fields["month"] = date
250 ris_dict.pop("DA")
252 end_pages = ris_dict.pop("EP", [])
253 for end_page in end_pages:
254 if "pages" in entry.fields:
255 entry.fields["pages"] += f"--{end_page}"
256 else:
257 entry.fields["pages"] = end_page
259 # Add the remaining fields with the RIS code as the field name
260 ris_dict.pop("ER", None)
261 for code, values in ris_dict.items():
262 entry.fields[code] = ", ".join(values)
264 if not entry_key:
265 entry_key = get_entry_key(entry)
266 else:
267 entry_key = clean_entry_key(entry_key)
269 return entry_key, entry
271 def parse_string(self, text):
272 self.unnamed_entry_counter = 1
273 self.command_start = 0
275 entry_texts = re.split(r"ER\s+-", text)
276 entries = (self.process_entry(t) for t in entry_texts if t.strip())
277 self.data.add_entries(entries)
278 return self.data
281class SuffixParser(BaseParser):
282 """
283 A parser which chooses the parser based on the suffix of each file given to it.
284 """
286 def parse_file(self, filename, file_suffix=None):
287 if file_suffix is not None:
288 filename = str(filename) + file_suffix
290 file_data = database.parse_file(filename)
291 self.data.add_entries(file_data.entries.items())
292 if file_data._preamble:
293 self.data._preamble.extend(file_data._preamble)
295 return file_data