bibdeskparser 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bibdeskparser/__init__.py +23 -0
- bibdeskparser/bdskfile.py +351 -0
- bibdeskparser/editing.py +673 -0
- bibdeskparser/entry.py +619 -0
- bibdeskparser/exporting.py +331 -0
- bibdeskparser/groups.py +119 -0
- bibdeskparser/header.py +236 -0
- bibdeskparser/library.py +1565 -0
- bibdeskparser/macros.py +109 -0
- bibdeskparser/middleware.py +174 -0
- bibdeskparser/names.py +96 -0
- bibdeskparser/render.py +666 -0
- bibdeskparser/texmap.py +467 -0
- bibdeskparser/writer.py +234 -0
- bibdeskparser-0.1.0.dist-info/METADATA +144 -0
- bibdeskparser-0.1.0.dist-info/RECORD +17 -0
- bibdeskparser-0.1.0.dist-info/WHEEL +4 -0
bibdeskparser/texmap.py
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""BibDesk's TeX <-> Unicode string conversion.
|
|
2
|
+
|
|
3
|
+
BibDesk converts between TeX markup and Unicode when reading and writing
|
|
4
|
+
`.bib` files:
|
|
5
|
+
|
|
6
|
+
- `detexify`: TeX -> Unicode (BibDesk applies this on *read*, for display)
|
|
7
|
+
- `texify`: Unicode -> TeX (BibDesk applies this on *write*, to the `.bib`
|
|
8
|
+
file)
|
|
9
|
+
|
|
10
|
+
This module replicates that conversion. The tables are transcribed
|
|
11
|
+
byte-for-byte from BibDesk's `CharacterConversion.plist`, and the
|
|
12
|
+
functions mirror the algorithms in BibDesk's `BDSKConverter.m`, so the
|
|
13
|
+
conversion matches BibDesk exactly: for a `.bib` file written by BibDesk,
|
|
14
|
+
`detexify` (read) followed by `texify` (write) reproduces the original
|
|
15
|
+
file. Characters that BibDesk cannot express in TeX (e.g., `π`, `ℏ`, `₂`)
|
|
16
|
+
pass through untouched in both directions.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import unicodedata
|
|
20
|
+
|
|
21
|
+
# All members whose name does not start with an underscore must be listed
|
|
22
|
+
# either in __all__ or in __private__
|
|
23
|
+
__all__ = []
|
|
24
|
+
__private__ = ["detexify", "texify", "skip_texify"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Roman to TeX + One-Way Conversions from `CharacterConversion.plist`
|
|
28
|
+
# (used by texify; the One-Way entries convert forward only)
|
|
29
|
+
_TEXIFY = {
|
|
30
|
+
"¡": "{!'}",
|
|
31
|
+
"§": "{\\S}",
|
|
32
|
+
"©": "{\\copyright}",
|
|
33
|
+
"®": "{\\textregistered}",
|
|
34
|
+
"Á": "{\\'A}",
|
|
35
|
+
"Â": "{\\^A}",
|
|
36
|
+
"Ã": "{\\~A}",
|
|
37
|
+
"Ä": '{\\"A}',
|
|
38
|
+
"Å": "{\\AA}",
|
|
39
|
+
"Æ": "{\\AE}",
|
|
40
|
+
"Ç": "{\\c C}",
|
|
41
|
+
"É": "{\\'E}",
|
|
42
|
+
"Ê": "{\\^E}",
|
|
43
|
+
"Ë": '{\\"E}',
|
|
44
|
+
"Ì": "{\\`I}",
|
|
45
|
+
"Í": "{\\'I}",
|
|
46
|
+
"Î": "{\\^I}",
|
|
47
|
+
"Ï": '{\\"I}',
|
|
48
|
+
"Ó": "{\\'O}",
|
|
49
|
+
"Ô": "{\\^O}",
|
|
50
|
+
"Ö": '{\\"O}',
|
|
51
|
+
"Ø": "{\\O}",
|
|
52
|
+
"Ú": "{\\'U}",
|
|
53
|
+
"Û": "{\\^U}",
|
|
54
|
+
"Ü": '{\\"U}',
|
|
55
|
+
"ß": "{\\ss}",
|
|
56
|
+
"à": "{\\`a}",
|
|
57
|
+
"á": "{\\'a}",
|
|
58
|
+
"â": "{\\^a}",
|
|
59
|
+
"ã": "{\\~a}",
|
|
60
|
+
"ä": '{\\"a}',
|
|
61
|
+
"å": "{\\aa}",
|
|
62
|
+
"æ": "{\\ae}",
|
|
63
|
+
"ç": "{\\c c}",
|
|
64
|
+
"è": "{\\`e}",
|
|
65
|
+
"é": "{\\'e}",
|
|
66
|
+
"ê": "{\\^e}",
|
|
67
|
+
"ë": '{\\"e}',
|
|
68
|
+
"ì": "{\\`\\i}",
|
|
69
|
+
"í": "{\\'\\i}",
|
|
70
|
+
"î": "{\\^\\i}",
|
|
71
|
+
"ï": '{\\"\\i}',
|
|
72
|
+
"ñ": "{\\~n}",
|
|
73
|
+
"ò": "{\\`o}",
|
|
74
|
+
"ó": "{\\'o}",
|
|
75
|
+
"ô": "{\\^o}",
|
|
76
|
+
"õ": "{\\~o}",
|
|
77
|
+
"ö": '{\\"o}',
|
|
78
|
+
"ø": "{\\o}",
|
|
79
|
+
"ù": "{\\`u}",
|
|
80
|
+
"ú": "{\\'u}",
|
|
81
|
+
"û": "{\\^u}",
|
|
82
|
+
"ü": '{\\"u}',
|
|
83
|
+
"ÿ": '{\\"y}',
|
|
84
|
+
"Ā": "{\\=A}",
|
|
85
|
+
"ā": "{\\=a}",
|
|
86
|
+
"ć": "{\\'c}",
|
|
87
|
+
"Č": "{\\v C}",
|
|
88
|
+
"č": "{\\v c}",
|
|
89
|
+
"ě": "{\\v e}",
|
|
90
|
+
"Ī": "{\\=I}",
|
|
91
|
+
"ī": "{\\=\\i}",
|
|
92
|
+
"Ł": "{\\L}",
|
|
93
|
+
"ł": "{\\l}",
|
|
94
|
+
"ő": "{\\H o}",
|
|
95
|
+
"Œ": "{\\OE}",
|
|
96
|
+
"œ": "{\\oe}",
|
|
97
|
+
"ş": "{\\c s}",
|
|
98
|
+
"š": "{\\v s}",
|
|
99
|
+
"Ū": "{\\=U}",
|
|
100
|
+
"ū": "{\\=u}",
|
|
101
|
+
"Ÿ": '{\\"Y}',
|
|
102
|
+
"Ž": "{\\v Z}",
|
|
103
|
+
"ž": "{\\v z}",
|
|
104
|
+
"Ḍ": "{\\d D}",
|
|
105
|
+
"ḍ": "{\\d d}",
|
|
106
|
+
"Ḥ": "{\\d H}",
|
|
107
|
+
"ḥ": "{\\d h}",
|
|
108
|
+
"Ṣ": "{\\d S}",
|
|
109
|
+
"ṣ": "{\\d s}",
|
|
110
|
+
"Ṭ": "{\\d T}",
|
|
111
|
+
"ṭ": "{\\d t}",
|
|
112
|
+
"Ẓ": "{\\d Z}",
|
|
113
|
+
"ẓ": "{\\d z}",
|
|
114
|
+
"…": "{\\ldots}",
|
|
115
|
+
"™": "{\\texttrademark}",
|
|
116
|
+
"\u00a0": " ",
|
|
117
|
+
"\u00ad": "-",
|
|
118
|
+
"°": "$\\,^{\\circ}$",
|
|
119
|
+
"±": "$\\pm$",
|
|
120
|
+
"–": "--",
|
|
121
|
+
"—": "---",
|
|
122
|
+
"‘": "`",
|
|
123
|
+
"’": "'",
|
|
124
|
+
"‛": "`",
|
|
125
|
+
"“": "``",
|
|
126
|
+
"”": "''",
|
|
127
|
+
"‟": "``",
|
|
128
|
+
"•": "*",
|
|
129
|
+
"ff": "ff",
|
|
130
|
+
"fi": "fi",
|
|
131
|
+
"fl": "fl",
|
|
132
|
+
"ffi": "ffi",
|
|
133
|
+
"ffl": "ffl",
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# TeX to Roman from `CharacterConversion.plist` (used by detexify)
|
|
137
|
+
_DETEXIFY = {
|
|
138
|
+
"{!'}": "¡",
|
|
139
|
+
'{\\"A}': "Ä",
|
|
140
|
+
'{\\"E}': "Ë",
|
|
141
|
+
'{\\"I}': "Ï",
|
|
142
|
+
'{\\"O}': "Ö",
|
|
143
|
+
'{\\"U}': "Ü",
|
|
144
|
+
'{\\"Y}': "Ÿ",
|
|
145
|
+
'{\\"\\i}': "ï",
|
|
146
|
+
'{\\"a}': "ä",
|
|
147
|
+
'{\\"e}': "ë",
|
|
148
|
+
'{\\"o}': "ö",
|
|
149
|
+
'{\\"u}': "ü",
|
|
150
|
+
'{\\"y}': "ÿ",
|
|
151
|
+
"{\\'A}": "Á",
|
|
152
|
+
"{\\'E}": "É",
|
|
153
|
+
"{\\'I}": "Í",
|
|
154
|
+
"{\\'O}": "Ó",
|
|
155
|
+
"{\\'U}": "Ú",
|
|
156
|
+
"{\\'\\i}": "í",
|
|
157
|
+
"{\\'a}": "á",
|
|
158
|
+
"{\\'c}": "ć",
|
|
159
|
+
"{\\'e}": "é",
|
|
160
|
+
"{\\'o}": "ó",
|
|
161
|
+
"{\\'u}": "ú",
|
|
162
|
+
"{\\=A}": "Ā",
|
|
163
|
+
"{\\=I}": "Ī",
|
|
164
|
+
"{\\=U}": "Ū",
|
|
165
|
+
"{\\=\\i}": "ī",
|
|
166
|
+
"{\\=a}": "ā",
|
|
167
|
+
"{\\=u}": "ū",
|
|
168
|
+
"{\\AA}": "Å",
|
|
169
|
+
"{\\AE}": "Æ",
|
|
170
|
+
"{\\H o}": "ő",
|
|
171
|
+
"{\\OE}": "Œ",
|
|
172
|
+
"{\\O}": "Ø",
|
|
173
|
+
"{\\S}": "§",
|
|
174
|
+
"{\\^A}": "Â",
|
|
175
|
+
"{\\^E}": "Ê",
|
|
176
|
+
"{\\^I}": "Î",
|
|
177
|
+
"{\\^O}": "Ô",
|
|
178
|
+
"{\\^U}": "Û",
|
|
179
|
+
"{\\^\\i}": "î",
|
|
180
|
+
"{\\^a}": "â",
|
|
181
|
+
"{\\^e}": "ê",
|
|
182
|
+
"{\\^o}": "ô",
|
|
183
|
+
"{\\^u}": "û",
|
|
184
|
+
"{\\`I}": "Ì",
|
|
185
|
+
"{\\`\\i}": "ì",
|
|
186
|
+
"{\\`a}": "à",
|
|
187
|
+
"{\\`e}": "è",
|
|
188
|
+
"{\\`o}": "ò",
|
|
189
|
+
"{\\`u}": "ù",
|
|
190
|
+
"{\\aa}": "å",
|
|
191
|
+
"{\\ae}": "æ",
|
|
192
|
+
"{\\c C}": "Ç",
|
|
193
|
+
"{\\c c}": "ç",
|
|
194
|
+
"{\\c s}": "ş",
|
|
195
|
+
"{\\cc}": "ç",
|
|
196
|
+
"{\\copyright}": "©",
|
|
197
|
+
"{\\d D}": "Ḍ",
|
|
198
|
+
"{\\d H}": "Ḥ",
|
|
199
|
+
"{\\d S}": "Ṣ",
|
|
200
|
+
"{\\d T}": "Ṭ",
|
|
201
|
+
"{\\d Z}": "Ẓ",
|
|
202
|
+
"{\\d d}": "ḍ",
|
|
203
|
+
"{\\d h}": "ḥ",
|
|
204
|
+
"{\\d s}": "ṣ",
|
|
205
|
+
"{\\d t}": "ṭ",
|
|
206
|
+
"{\\d z}": "ẓ",
|
|
207
|
+
"{\\ldots}": "…",
|
|
208
|
+
"{\\L}": "Ł",
|
|
209
|
+
"{\\l}": "ł",
|
|
210
|
+
"{\\oe}": "œ",
|
|
211
|
+
"{\\o}": "ø",
|
|
212
|
+
"{\\ss}": "ß",
|
|
213
|
+
"{\\textregistered}": "®",
|
|
214
|
+
"{\\texttrademark}": "™",
|
|
215
|
+
"{\\v C}": "Č",
|
|
216
|
+
"{\\v Z}": "Ž",
|
|
217
|
+
"{\\v c}": "č",
|
|
218
|
+
"{\\v e}": "ě",
|
|
219
|
+
"{\\v s}": "š",
|
|
220
|
+
"{\\v z}": "ž",
|
|
221
|
+
"{\\~A}": "Ã",
|
|
222
|
+
"{\\~a}": "ã",
|
|
223
|
+
"{\\~n}": "ñ",
|
|
224
|
+
"{\\~o}": "õ",
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# accent letter/symbol -> combining mark (detexify accent algorithm)
|
|
228
|
+
_DETEX_ACCENTS = {
|
|
229
|
+
'"': "\u0308", # combining diaeresis
|
|
230
|
+
"'": "\u0301", # combining acute accent
|
|
231
|
+
".": "\u0307", # combining dot above
|
|
232
|
+
"=": "\u0304", # combining macron
|
|
233
|
+
"H": "\u030b", # combining double acute accent
|
|
234
|
+
"^": "\u0302", # combining circumflex accent
|
|
235
|
+
"`": "\u0300", # combining grave accent
|
|
236
|
+
"b": "\u0331", # combining macron below
|
|
237
|
+
"c": "\u0327", # combining cedilla
|
|
238
|
+
"d": "\u0323", # combining dot below
|
|
239
|
+
"k": "\u0328", # combining ogonek
|
|
240
|
+
"r": "\u030a", # combining ring above
|
|
241
|
+
"u": "\u0306", # combining breve
|
|
242
|
+
"v": "\u030c", # combining caron
|
|
243
|
+
"~": "\u0303", # combining tilde
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# combining mark -> accent letter/symbol, with a trailing space
|
|
247
|
+
# for letter accents (texify accent algorithm)
|
|
248
|
+
_TEX_ACCENTS = {
|
|
249
|
+
"\u0300": "`", # combining grave accent
|
|
250
|
+
"\u0301": "'", # combining acute accent
|
|
251
|
+
"\u0302": "^", # combining circumflex accent
|
|
252
|
+
"\u0303": "~", # combining tilde
|
|
253
|
+
"\u0304": "=", # combining macron
|
|
254
|
+
"\u0306": "u ", # combining breve
|
|
255
|
+
"\u0307": ".", # combining dot above
|
|
256
|
+
"\u0308": '"', # combining diaeresis
|
|
257
|
+
"\u030a": "r ", # combining ring above
|
|
258
|
+
"\u030b": "H ", # combining double acute accent
|
|
259
|
+
"\u030c": "v ", # combining caron
|
|
260
|
+
"\u0331": "b ", # combining macron below
|
|
261
|
+
"\u0323": "d ", # combining dot below
|
|
262
|
+
"\u0327": "c ", # combining cedilla
|
|
263
|
+
"\u0328": "k ", # combining ogonek
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# BibDesk's finalCharacterSet: the codepoints that texify will
|
|
267
|
+
# attempt to convert
|
|
268
|
+
# fmt: off
|
|
269
|
+
_FINAL = frozenset([
|
|
270
|
+
160, 161, 167, 169, 173, 174, 176, 177, 192, 193, 194, 195, 196, 197,
|
|
271
|
+
198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 209, 210, 211, 212,
|
|
272
|
+
213, 214, 216, 217, 218, 219, 220, 221, 223, 224, 225, 226, 227, 228,
|
|
273
|
+
229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 241, 242, 243,
|
|
274
|
+
244, 245, 246, 248, 249, 250, 251, 252, 253, 255, 256, 257, 258, 259,
|
|
275
|
+
260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 274, 275,
|
|
276
|
+
276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289,
|
|
277
|
+
290, 291, 292, 293, 296, 297, 298, 299, 300, 301, 302, 303, 304, 308,
|
|
278
|
+
309, 310, 311, 313, 314, 315, 316, 317, 318, 321, 322, 323, 324, 325,
|
|
279
|
+
326, 327, 328, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342,
|
|
280
|
+
343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356,
|
|
281
|
+
357, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372,
|
|
282
|
+
373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 416, 417, 431, 432,
|
|
283
|
+
461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474,
|
|
284
|
+
475, 476, 478, 479, 480, 481, 482, 483, 486, 487, 488, 489, 490, 491,
|
|
285
|
+
492, 493, 494, 495, 496, 500, 501, 504, 505, 506, 507, 508, 509, 510,
|
|
286
|
+
511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524,
|
|
287
|
+
525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538,
|
|
288
|
+
539, 542, 543, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560,
|
|
289
|
+
561, 562, 563, 7680, 7681, 7682, 7683, 7684, 7685, 7686, 7687, 7688,
|
|
290
|
+
7689, 7690, 7691, 7692, 7693, 7694, 7695, 7696, 7697, 7698, 7699,
|
|
291
|
+
7700, 7701, 7702, 7703, 7704, 7705, 7706, 7707, 7708, 7709, 7710,
|
|
292
|
+
7711, 7712, 7713, 7714, 7715, 7716, 7717, 7718, 7719, 7720, 7721,
|
|
293
|
+
7722, 7723, 7724, 7725, 7726, 7727, 7728, 7729, 7730, 7731, 7732,
|
|
294
|
+
7733, 7734, 7735, 7736, 7737, 7738, 7739, 7740, 7741, 7742, 7743,
|
|
295
|
+
7744, 7745, 7746, 7747, 7748, 7749, 7750, 7751, 7752, 7753, 7754,
|
|
296
|
+
7755, 7756, 7757, 7758, 7759, 7760, 7761, 7762, 7763, 7764, 7765,
|
|
297
|
+
7766, 7767, 7768, 7769, 7770, 7771, 7772, 7773, 7774, 7775, 7776,
|
|
298
|
+
7777, 7778, 7779, 7780, 7781, 7782, 7783, 7784, 7785, 7786, 7787,
|
|
299
|
+
7788, 7789, 7790, 7791, 7792, 7793, 7794, 7795, 7796, 7797, 7798,
|
|
300
|
+
7799, 7800, 7801, 7802, 7803, 7804, 7805, 7806, 7807, 7808, 7809,
|
|
301
|
+
7810, 7811, 7812, 7813, 7814, 7815, 7816, 7817, 7818, 7819, 7820,
|
|
302
|
+
7821, 7822, 7823, 7824, 7825, 7826, 7827, 7828, 7829, 7830, 7831,
|
|
303
|
+
7832, 7833, 7835, 7840, 7841, 7842, 7843, 7844, 7845, 7846, 7847,
|
|
304
|
+
7848, 7849, 7850, 7851, 7852, 7853, 7854, 7855, 7856, 7857, 7858,
|
|
305
|
+
7859, 7860, 7861, 7862, 7863, 7864, 7865, 7866, 7867, 7868, 7869,
|
|
306
|
+
7870, 7871, 7872, 7873, 7874, 7875, 7876, 7877, 7878, 7879, 7880,
|
|
307
|
+
7881, 7882, 7883, 7884, 7885, 7886, 7887, 7888, 7889, 7890, 7891,
|
|
308
|
+
7892, 7893, 7894, 7895, 7896, 7897, 7898, 7899, 7900, 7901, 7902,
|
|
309
|
+
7903, 7904, 7905, 7906, 7907, 7908, 7909, 7910, 7911, 7912, 7913,
|
|
310
|
+
7914, 7915, 7916, 7917, 7918, 7919, 7920, 7921, 7922, 7923, 7924,
|
|
311
|
+
7925, 7926, 7927, 7928, 7929, 8211, 8212, 8216, 8217, 8219, 8220,
|
|
312
|
+
8221, 8223, 8226, 8230, 8482, 64256, 64257, 64258, 64259, 64260
|
|
313
|
+
])
|
|
314
|
+
# fmt: on
|
|
315
|
+
|
|
316
|
+
# base letters that may carry a TeX accent
|
|
317
|
+
_BASE = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _tex_accent_to_char(tmp):
|
|
321
|
+
r"""Convert a `{\<accent><letter>}` token to a composed character.
|
|
322
|
+
|
|
323
|
+
Mirrors `convertTeXStringToComposedCharacter()` in BibDesk's
|
|
324
|
+
`BDSKConverter.m`. Returns the single composed Unicode character for
|
|
325
|
+
`tmp`, or `None` if `tmp` is not a valid accent token.
|
|
326
|
+
"""
|
|
327
|
+
if len(tmp) < 4 or tmp[0] != "{" or tmp[1] != "\\":
|
|
328
|
+
return None
|
|
329
|
+
accent_ch = tmp[2]
|
|
330
|
+
accent = _DETEX_ACCENTS.get(accent_ch)
|
|
331
|
+
if accent is None:
|
|
332
|
+
return None
|
|
333
|
+
ch = tmp[3]
|
|
334
|
+
if accent_ch.isalpha() and ch not in (" ", "\\"):
|
|
335
|
+
# letter accents (e.g. {\v S}) must be followed by space/backslash
|
|
336
|
+
return None
|
|
337
|
+
letter_start = 4 if ch == " " else 3
|
|
338
|
+
end = tmp.find("}", letter_start)
|
|
339
|
+
if end == -1:
|
|
340
|
+
return None
|
|
341
|
+
character = tmp[letter_start:end]
|
|
342
|
+
if character in ("\\i", "\\j"):
|
|
343
|
+
character = character[1:] # dotless i/j -> plain i/j
|
|
344
|
+
if len(character) != 1:
|
|
345
|
+
return None
|
|
346
|
+
composed = unicodedata.normalize("NFC", character + accent)
|
|
347
|
+
return composed if len(composed) == 1 else None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def detexify(s):
|
|
351
|
+
r"""Convert TeX markup in `s` to Unicode.
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
>>> from bibdeskparser.texmap import detexify
|
|
355
|
+
>>> detexify('Universit{\\"a}t T{\\"u}bingen')
|
|
356
|
+
'Universität Tübingen'
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Mirrors `copyStringByDeTeXifyingString:` in BibDesk's
|
|
361
|
+
`BDSKConverter.m`; BibDesk applies this conversion to every field
|
|
362
|
+
value when reading a `.bib` file.
|
|
363
|
+
|
|
364
|
+
# Arguments
|
|
365
|
+
|
|
366
|
+
* `s`: The string to convert.
|
|
367
|
+
|
|
368
|
+
Only `{\...}` sequences are converted; the function is a no-op
|
|
369
|
+
unless `s` contains `{\`.
|
|
370
|
+
"""
|
|
371
|
+
if not s or "{\\" not in s:
|
|
372
|
+
return s
|
|
373
|
+
out = s
|
|
374
|
+
i = out.find("{\\")
|
|
375
|
+
converted = False
|
|
376
|
+
while i != -1:
|
|
377
|
+
close = out.find("}", i + 2)
|
|
378
|
+
if close == -1:
|
|
379
|
+
break
|
|
380
|
+
tmp = out[i : close + 1]
|
|
381
|
+
repl = _DETEXIFY.get(tmp) or _tex_accent_to_char(tmp)
|
|
382
|
+
if repl is not None:
|
|
383
|
+
out = out[:i] + repl + out[close + 1 :]
|
|
384
|
+
converted = True
|
|
385
|
+
# advance one char past the current '{' (matches BibDesk's
|
|
386
|
+
# search reset)
|
|
387
|
+
i = out.find("{\\", i + 1)
|
|
388
|
+
return out if converted else s
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _char_to_tex_accent(ch):
|
|
392
|
+
r"""Convert a composed character to a `{\<accent><letter>}` token.
|
|
393
|
+
|
|
394
|
+
Mirrors `convertComposedCharacterToTeX()` in BibDesk's
|
|
395
|
+
`BDSKConverter.m`. Returns the TeX form of the single character `ch`
|
|
396
|
+
(or `ch` itself for a plain base letter), or `None` if `ch` cannot be
|
|
397
|
+
expressed as a TeX accent.
|
|
398
|
+
"""
|
|
399
|
+
d = unicodedata.normalize("NFD", ch)
|
|
400
|
+
if len(d) == 0 or d[0] not in _BASE:
|
|
401
|
+
return None
|
|
402
|
+
if len(d) == 1:
|
|
403
|
+
return ch # base letter, nothing to do
|
|
404
|
+
if len(d) > 2 or d[1] not in _TEX_ACCENTS:
|
|
405
|
+
return None
|
|
406
|
+
accent = _TEX_ACCENTS[d[1]]
|
|
407
|
+
character = d[0]
|
|
408
|
+
if character in ("i", "j") and accent not in ("c ", "d ", "b ", "k "):
|
|
409
|
+
character = "\\" + character # dotless \i / \j
|
|
410
|
+
return "{\\" + accent + character + "}"
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def texify(s):
|
|
414
|
+
r"""Convert Unicode characters in `s` to TeX markup.
|
|
415
|
+
|
|
416
|
+
```python
|
|
417
|
+
>>> from bibdeskparser.texmap import texify
|
|
418
|
+
>>> texify("Universität Tübingen")
|
|
419
|
+
'Universit{\\"a}t T{\\"u}bingen'
|
|
420
|
+
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Mirrors `copyStringByTeXifyingString:` in BibDesk's
|
|
424
|
+
`BDSKConverter.m`; BibDesk applies this conversion to every field
|
|
425
|
+
value when writing a `.bib` file.
|
|
426
|
+
|
|
427
|
+
# Arguments
|
|
428
|
+
|
|
429
|
+
* `s`: The string to convert.
|
|
430
|
+
|
|
431
|
+
The string is first NFC-normalized; only characters whose codepoint
|
|
432
|
+
is in BibDesk's "final character set" are converted, and any other
|
|
433
|
+
character (e.g., `π`, `ℏ`, `₂`) passes through untouched.
|
|
434
|
+
"""
|
|
435
|
+
if not s:
|
|
436
|
+
return s
|
|
437
|
+
s = unicodedata.normalize("NFC", s)
|
|
438
|
+
out = []
|
|
439
|
+
changed = False
|
|
440
|
+
for ch in s:
|
|
441
|
+
if ord(ch) in _FINAL:
|
|
442
|
+
repl = _TEXIFY.get(ch)
|
|
443
|
+
if repl is None:
|
|
444
|
+
repl = _char_to_tex_accent(ch)
|
|
445
|
+
if repl is not None and repl != ch:
|
|
446
|
+
out.append(repl)
|
|
447
|
+
changed = True
|
|
448
|
+
continue
|
|
449
|
+
out.append(ch)
|
|
450
|
+
return "".join(out) if changed else s
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def skip_texify(key):
|
|
454
|
+
r"""Whether the field `key` is excluded from TeX conversion.
|
|
455
|
+
|
|
456
|
+
# Arguments
|
|
457
|
+
|
|
458
|
+
* `key`: The field key, e.g. `"url"` or `"bdsk-file-1"`. Matching is
|
|
459
|
+
case-insensitive.
|
|
460
|
+
|
|
461
|
+
Returns `True` if the (lowercased) `key` contains `"url"` or starts
|
|
462
|
+
with `"bdsk-file"`. BibDesk does not TeXify URL fields, and
|
|
463
|
+
`bdsk-file` fields hold base64 data. Both are pure ASCII without
|
|
464
|
+
`{\` sequences, so this only matters as a safety guard.
|
|
465
|
+
"""
|
|
466
|
+
k = key.lower()
|
|
467
|
+
return "url" in k or k.startswith("bdsk-file")
|
bibdeskparser/writer.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
r"""Serialization of a `bibtexparser` library in BibDesk's file format.
|
|
2
|
+
|
|
3
|
+
BibDesk lays out a `.bib` file as the header comment, the `@string`
|
|
4
|
+
definitions, the entries, and finally the group `@comment` blocks.
|
|
5
|
+
Blocks are separated by one blank line, except for two blank lines
|
|
6
|
+
before the `@string` section and before the first entry. Entry fields
|
|
7
|
+
are written one per line, indented with a tab, with the closing brace
|
|
8
|
+
fused onto the last field line.
|
|
9
|
+
|
|
10
|
+
`render_library` serializes a parsed library back to that exact
|
|
11
|
+
format: for an unmodified library, the output is byte-identical to the
|
|
12
|
+
original file.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
>>> import bibtexparser
|
|
16
|
+
>>> from bibdeskparser.middleware import parse_stack
|
|
17
|
+
>>> from bibdeskparser.writer import render_library
|
|
18
|
+
>>> source = (
|
|
19
|
+
... '@article{key1,\n'
|
|
20
|
+
... '\tauthor = {Gr{\\"u}n, Anna},\n'
|
|
21
|
+
... '\tyear = {2026}}\n'
|
|
22
|
+
... )
|
|
23
|
+
>>> library = bibtexparser.parse_string(source, parse_stack=parse_stack())
|
|
24
|
+
>>> library.entries[0]["author"] # Unicode model ...
|
|
25
|
+
'{Grün, Anna}'
|
|
26
|
+
>>> render_library(library) == source # ... but byte-exact output
|
|
27
|
+
True
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from bibtexparser.model import (
|
|
33
|
+
Entry,
|
|
34
|
+
ExplicitComment,
|
|
35
|
+
ImplicitComment,
|
|
36
|
+
ParsingFailedBlock,
|
|
37
|
+
String,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from .bdskfile import BibDeskFile
|
|
41
|
+
from .header import restore_trailing_space
|
|
42
|
+
from .middleware import TeXifyMiddleware
|
|
43
|
+
|
|
44
|
+
__all__ = []
|
|
45
|
+
|
|
46
|
+
# All members whose name does not start with an underscore must be listed
|
|
47
|
+
# either in __all__ or in __private__
|
|
48
|
+
__private__ = [
|
|
49
|
+
"render_library",
|
|
50
|
+
"serialize_block",
|
|
51
|
+
"bibdesk_field_order",
|
|
52
|
+
"separator",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
#: First line of the header comment in files written by BibDesk.
|
|
57
|
+
_BIBDESK_HEADER_PREFIX = (
|
|
58
|
+
"%% This BibTeX bibliography file was created using BibDesk"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def serialize_block(block):
|
|
63
|
+
r"""Serialize a single block in BibDesk's exact format.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
serialize_block(block)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Returns the string representation (without trailing newline) of a
|
|
70
|
+
`bibtexparser` block:
|
|
71
|
+
|
|
72
|
+
* `ImplicitComment`: the comment text, verbatim. If the comment is
|
|
73
|
+
a BibDesk header, the trailing space that the parser rstripped
|
|
74
|
+
from the last line is restored.
|
|
75
|
+
* `String`: `@string{key = value}`.
|
|
76
|
+
* `ExplicitComment`: `@comment{...}`, with the comment body
|
|
77
|
+
written verbatim.
|
|
78
|
+
* `Entry`: the entry type and key, then one tab-indented line per
|
|
79
|
+
field, with the closing brace directly after the last field
|
|
80
|
+
value. {any}`bibdeskparser.bdskfile.BibDeskFile` values are
|
|
81
|
+
serialized via their
|
|
82
|
+
{any}`~bibdeskparser.bdskfile.BibDeskFile.to_field_value` method.
|
|
83
|
+
* `ParsingFailedBlock` (e.g., a `DuplicateBlockKeyBlock` for an
|
|
84
|
+
entry with a duplicate key): the block's `raw` source slice,
|
|
85
|
+
verbatim, so that files with duplicate keys still round-trip.
|
|
86
|
+
|
|
87
|
+
Raises {any}`TypeError` for any other block type.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
>>> from bibtexparser.model import Entry, Field, String
|
|
91
|
+
>>> from bibdeskparser.writer import serialize_block
|
|
92
|
+
>>> print(serialize_block(String(key="jpb", value="{J. Phys. B}")))
|
|
93
|
+
@string{jpb = {J. Phys. B}}
|
|
94
|
+
>>> entry = Entry(
|
|
95
|
+
... entry_type="article",
|
|
96
|
+
... key="key1",
|
|
97
|
+
... fields=[
|
|
98
|
+
... Field(key="author", value="{Goerz}"),
|
|
99
|
+
... Field(key="year", value="{2026}"),
|
|
100
|
+
... ],
|
|
101
|
+
... )
|
|
102
|
+
>>> serialize_block(entry)
|
|
103
|
+
'@article{key1,\n\tauthor = {Goerz},\n\tyear = {2026}}'
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
"""
|
|
107
|
+
if isinstance(block, ImplicitComment):
|
|
108
|
+
text = block.comment
|
|
109
|
+
if text.startswith(_BIBDESK_HEADER_PREFIX):
|
|
110
|
+
text = restore_trailing_space(text)
|
|
111
|
+
return text
|
|
112
|
+
if isinstance(block, String):
|
|
113
|
+
return f"@string{{{block.key} = {block.value}}}"
|
|
114
|
+
if isinstance(block, ExplicitComment):
|
|
115
|
+
return f"@comment{{{block.comment}}}"
|
|
116
|
+
if isinstance(block, Entry):
|
|
117
|
+
if not block.fields:
|
|
118
|
+
return f"@{block.entry_type}{{{block.key}}}"
|
|
119
|
+
lines = [f"@{block.entry_type}{{{block.key},"]
|
|
120
|
+
last = len(block.fields) - 1
|
|
121
|
+
for i, field in enumerate(block.fields):
|
|
122
|
+
value = field.value
|
|
123
|
+
if isinstance(value, BibDeskFile):
|
|
124
|
+
value = value.to_field_value()
|
|
125
|
+
tail = "}" if i == last else ","
|
|
126
|
+
lines.append(f"\t{field.key} = {value}{tail}")
|
|
127
|
+
return "\n".join(lines)
|
|
128
|
+
if isinstance(block, ParsingFailedBlock):
|
|
129
|
+
return block.raw
|
|
130
|
+
raise TypeError(f"Unhandled block type: {type(block).__name__}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _effective_type(block):
|
|
134
|
+
"""The block type to use for separator purposes.
|
|
135
|
+
|
|
136
|
+
A `ParsingFailedBlock` counts as the block it wraps
|
|
137
|
+
(`ignore_error_block`), or as an `Entry` if it wraps nothing, since
|
|
138
|
+
failed blocks are almost always broken entries (e.g., duplicate
|
|
139
|
+
keys)."""
|
|
140
|
+
if isinstance(block, ParsingFailedBlock):
|
|
141
|
+
inner = block.ignore_error_block
|
|
142
|
+
return type(inner) if inner is not None else Entry
|
|
143
|
+
return type(block)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def separator(prev_block, cur_block):
|
|
147
|
+
"""The blank-line separator BibDesk puts between two blocks.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
separator(prev_block, cur_block)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Returns `"\\n\\n\\n"` (two blank lines) between the header comment
|
|
154
|
+
and the first `@string`, and between the last `@string` and the
|
|
155
|
+
first entry, i.e., when `prev_block` is an `ImplicitComment` and
|
|
156
|
+
`cur_block` is a `String`, or when `prev_block` is a `String` and
|
|
157
|
+
`cur_block` is an `Entry`. Returns `"\\n\\n"` (one blank line) for
|
|
158
|
+
every other transition. A `ParsingFailedBlock` counts as the block
|
|
159
|
+
it wraps (usually an `Entry` with a duplicate key).
|
|
160
|
+
"""
|
|
161
|
+
prev_type = _effective_type(prev_block)
|
|
162
|
+
cur_type = _effective_type(cur_block)
|
|
163
|
+
double = (
|
|
164
|
+
issubclass(prev_type, ImplicitComment) and issubclass(cur_type, String)
|
|
165
|
+
) or (issubclass(prev_type, String) and issubclass(cur_type, Entry))
|
|
166
|
+
return "\n\n\n" if double else "\n\n"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def render_library(library):
|
|
170
|
+
"""Serialize a library to the exact text BibDesk would write.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
render_library(library)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Returns the full text of the `.bib` file for `library`: all blocks
|
|
177
|
+
serialized with `serialize_block`, joined with `separator`, with a
|
|
178
|
+
final newline. Before serializing, the library is passed through
|
|
179
|
+
`TeXifyMiddleware` on a copy (`allow_inplace_modification=False`),
|
|
180
|
+
so Unicode field values are written as TeX and the caller's
|
|
181
|
+
library is left untouched.
|
|
182
|
+
"""
|
|
183
|
+
library = TeXifyMiddleware(allow_inplace_modification=False).transform(
|
|
184
|
+
library
|
|
185
|
+
)
|
|
186
|
+
pieces = []
|
|
187
|
+
prev = None
|
|
188
|
+
for block in library.blocks:
|
|
189
|
+
if prev is not None:
|
|
190
|
+
pieces.append(separator(prev, block))
|
|
191
|
+
pieces.append(serialize_block(block))
|
|
192
|
+
prev = block
|
|
193
|
+
pieces.append("\n")
|
|
194
|
+
return "".join(pieces)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def bibdesk_field_order(fields):
|
|
198
|
+
"""Sort entry fields the way BibDesk does.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
bibdesk_field_order(fields)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Returns the list of `bibtexparser` `Field` objects in `fields`
|
|
205
|
+
sorted case-insensitively alphabetically by key, except that all
|
|
206
|
+
`bdsk-*` fields (file attachments, URLs) come after all other
|
|
207
|
+
fields (each group sorted alphabetically). The sort is stable and
|
|
208
|
+
purely lexical, so, e.g., `bdsk-file-10` sorts *before*
|
|
209
|
+
`bdsk-file-2` (`"1" < "2"`), matching the field order in files
|
|
210
|
+
written by BibDesk.
|
|
211
|
+
|
|
212
|
+
This helper is not used by `render_library` (which writes fields
|
|
213
|
+
in their existing order); it is intended for normalizing the field
|
|
214
|
+
order of newly created or modified entries.
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
>>> from bibtexparser.model import Field
|
|
218
|
+
>>> from bibdeskparser.writer import bibdesk_field_order
|
|
219
|
+
>>> fields = [
|
|
220
|
+
... Field(key="bdsk-url-1", value="{http://example.org}"),
|
|
221
|
+
... Field(key="year", value="{2026}"),
|
|
222
|
+
... Field(key="author", value="{Goerz, Michael}"),
|
|
223
|
+
... ]
|
|
224
|
+
>>> [field.key for field in bibdesk_field_order(fields)]
|
|
225
|
+
['author', 'year', 'bdsk-url-1']
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def sort_key(field):
|
|
231
|
+
key = field.key.lower()
|
|
232
|
+
return (key.startswith("bdsk-"), key)
|
|
233
|
+
|
|
234
|
+
return sorted(fields, key=sort_key)
|