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/macros.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
r"""Validation and normalization of BibDesk macro (`@string`) names.
|
|
2
|
+
|
|
3
|
+
BibDesk restricts the names of macros (BibTeX `@string` definitions) to a
|
|
4
|
+
subset of printable ASCII and treats them case-insensitively: its macro
|
|
5
|
+
editor forces names to lowercase as they are typed, and its macro table
|
|
6
|
+
(`BDSKMacroResolver`) looks names up by their lowercased form. This module
|
|
7
|
+
replicates those rules.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__all__ = []
|
|
11
|
+
|
|
12
|
+
# All members whose name does not start with an underscore must be listed
|
|
13
|
+
# either in __all__ or in __private__
|
|
14
|
+
__private__ = ["is_valid_macro_name", "normalize_macro_name"]
|
|
15
|
+
|
|
16
|
+
# Characters BibDesk allows in a field/macro name. Built exactly as
|
|
17
|
+
# `invalidCiteKeyCharSet` in `BDSKTypeManager.m` (which
|
|
18
|
+
# `invalidFieldNameCharacterSet` aliases): start from printable ASCII
|
|
19
|
+
# 32..125, then remove the BibTeX separators/specials. Everything outside
|
|
20
|
+
# this set (control chars, `~`, and all non-ASCII) is invalid.
|
|
21
|
+
_MACRO_NAME_CHARS = frozenset(chr(c) for c in range(32, 126)) - set(
|
|
22
|
+
" '\"@,\\#}{~%()="
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_valid_macro_name(name, normalized=True):
|
|
27
|
+
r"""Return whether `name` is a valid BibDesk macro (`@string`) name.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
is_valid_macro_name(name, normalized=True)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Mirrors `-[BDSKMacroTextViewController isPartialStringValid:...]`: a
|
|
34
|
+
name is valid iff every character is in BibDesk's allowed set (printable
|
|
35
|
+
ASCII 32..125 minus the BibTeX separators/specials `` '"@,\#}{~%()=``
|
|
36
|
+
and the space character) and it does not begin with a decimal digit.
|
|
37
|
+
|
|
38
|
+
* `name`: the macro name to check.
|
|
39
|
+
* `normalized`: if `True` (default), `name` must additionally be
|
|
40
|
+
non-empty and already lowercase, i.e. in the canonical form produced
|
|
41
|
+
by {any}`normalize_macro_name`. If `False`, the empty string is
|
|
42
|
+
accepted (BibDesk permits it as an in-progress edit) and uppercase
|
|
43
|
+
letters are allowed.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
>>> from bibdeskparser.macros import is_valid_macro_name
|
|
47
|
+
>>> is_valid_macro_name("pra")
|
|
48
|
+
True
|
|
49
|
+
>>> is_valid_macro_name("bad name") # space is not allowed
|
|
50
|
+
False
|
|
51
|
+
>>> is_valid_macro_name("2pac") # must not start with a digit
|
|
52
|
+
False
|
|
53
|
+
>>> is_valid_macro_name("PRA") # not lowercase
|
|
54
|
+
False
|
|
55
|
+
>>> is_valid_macro_name("PRA", normalized=False)
|
|
56
|
+
True
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
"""
|
|
60
|
+
if any(ch not in _MACRO_NAME_CHARS for ch in name):
|
|
61
|
+
return False
|
|
62
|
+
if name and name[0].isascii() and name[0].isdigit():
|
|
63
|
+
return False
|
|
64
|
+
if normalized:
|
|
65
|
+
if not name:
|
|
66
|
+
return False
|
|
67
|
+
if name != name.lower():
|
|
68
|
+
return False
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def normalize_macro_name(name):
|
|
73
|
+
"""Return the BibDesk-canonical form of a macro name.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
normalize_macro_name(name)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
BibDesk's macro editor forces names to lowercase as they are typed
|
|
80
|
+
(`*partialStringPtr = [partialString lowercaseString]`) while rejecting
|
|
81
|
+
any input that contains a disallowed character or starts with a digit.
|
|
82
|
+
This returns the lowercased name that BibDesk would store, matching its
|
|
83
|
+
case-insensitive macro table (`BDSKMacroResolver`), so two names that
|
|
84
|
+
differ only in case normalize to the same key.
|
|
85
|
+
|
|
86
|
+
* `name`: the macro name as written (e.g. from a parsed `@string`
|
|
87
|
+
block).
|
|
88
|
+
|
|
89
|
+
Raises {any}`ValueError` if `name` is empty or is not a valid macro
|
|
90
|
+
name according to {any}`is_valid_macro_name`.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
>>> from bibdeskparser.macros import normalize_macro_name
|
|
94
|
+
>>> normalize_macro_name("PRA")
|
|
95
|
+
'pra'
|
|
96
|
+
>>> normalize_macro_name("pra")
|
|
97
|
+
'pra'
|
|
98
|
+
>>> normalize_macro_name("bad name")
|
|
99
|
+
Traceback (most recent call last):
|
|
100
|
+
...
|
|
101
|
+
ValueError: invalid BibDesk macro name: 'bad name'
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
"""
|
|
105
|
+
if not name:
|
|
106
|
+
raise ValueError("macro name must not be empty")
|
|
107
|
+
if not is_valid_macro_name(name, normalized=False):
|
|
108
|
+
raise ValueError(f"invalid BibDesk macro name: {name!r}")
|
|
109
|
+
return name.lower()
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
r"""Bibtexparser middlewares replicating BibDesk's read/write conversion.
|
|
2
|
+
|
|
3
|
+
BibDesk normalizes `.bib` data when reading and writing: on *read*, TeX
|
|
4
|
+
accents in field and `@string` values are decoded to Unicode
|
|
5
|
+
(`detexify`), and `bdsk-file-N` field values are base64-encoded binary
|
|
6
|
+
plists (see {any}`bibdeskparser.bdskfile.BibDeskFile`). On *write*,
|
|
7
|
+
Unicode is re-encoded as TeX (`texify`). The `BibDesk Static Groups`
|
|
8
|
+
`@comment` is not handled here: its body stays a plain string, which
|
|
9
|
+
{any}`bibdeskparser.library.Library` decodes and re-encodes itself
|
|
10
|
+
(see `bibdeskparser.groups`).
|
|
11
|
+
|
|
12
|
+
This module packages those conversions as `bibtexparser` "block
|
|
13
|
+
middleware" classes (see the
|
|
14
|
+
[bibtexparser documentation](https://bibtexparser.readthedocs.io)).
|
|
15
|
+
Use `parse_stack` to get the standard read stack:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
>>> import bibtexparser
|
|
19
|
+
>>> from bibdeskparser.middleware import parse_stack
|
|
20
|
+
>>> library = bibtexparser.parse_string(
|
|
21
|
+
... '@article{key1, author = {Gr{\\"u}n, Anna}}',
|
|
22
|
+
... parse_stack=parse_stack(),
|
|
23
|
+
... )
|
|
24
|
+
>>> library.entries[0]["author"]
|
|
25
|
+
'{Grün, Anna}'
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from bibtexparser.middlewares.middleware import BlockMiddleware
|
|
31
|
+
|
|
32
|
+
from .bdskfile import BibDeskFile
|
|
33
|
+
from .texmap import detexify, skip_texify, texify
|
|
34
|
+
|
|
35
|
+
__all__ = []
|
|
36
|
+
|
|
37
|
+
# All members whose name does not start with an underscore must be listed
|
|
38
|
+
# either in __all__ or in __private__
|
|
39
|
+
__private__ = [
|
|
40
|
+
"DeTeXifyMiddleware",
|
|
41
|
+
"TeXifyMiddleware",
|
|
42
|
+
"BibDeskFileMiddleware",
|
|
43
|
+
"parse_stack",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DeTeXifyMiddleware(BlockMiddleware):
|
|
48
|
+
r"""Middleware converting TeX markup to Unicode (*read*).
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
DeTeXifyMiddleware(allow_inplace_modification=True)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Applies `detexify` to every string field value of every entry
|
|
55
|
+
(e.g., `Gr{\"u}n` becomes `Grün`), and to the value of every
|
|
56
|
+
`@string` definition. Field keys for which `skip_texify` is `True`
|
|
57
|
+
(URL and `bdsk-file` fields) are left untouched, as are non-string
|
|
58
|
+
values.
|
|
59
|
+
|
|
60
|
+
This replicates the `-stringByDeTeXifyingString:` normalization
|
|
61
|
+
that BibDesk applies to every field value on read.
|
|
62
|
+
|
|
63
|
+
* `allow_inplace_modification`: if `True` (default), transform the
|
|
64
|
+
given library's blocks in place instead of copying them (see the
|
|
65
|
+
`bibtexparser` `Middleware` base class).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def transform_entry(self, entry, library):
|
|
69
|
+
"""Detexify all string field values of `entry`."""
|
|
70
|
+
for field in entry.fields:
|
|
71
|
+
if not skip_texify(field.key) and isinstance(field.value, str):
|
|
72
|
+
field.value = detexify(field.value)
|
|
73
|
+
return entry
|
|
74
|
+
|
|
75
|
+
def transform_string(self, string, library):
|
|
76
|
+
"""Detexify the value of the `@string` definition `string`."""
|
|
77
|
+
if isinstance(string.value, str):
|
|
78
|
+
string.value = detexify(string.value)
|
|
79
|
+
return string
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TeXifyMiddleware(BlockMiddleware):
|
|
83
|
+
r"""Middleware converting Unicode to TeX markup (*write*).
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
TeXifyMiddleware(allow_inplace_modification=True)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The mirror image of `DeTeXifyMiddleware`: applies `texify` to every
|
|
90
|
+
string field value of every entry (e.g., `Grün` becomes
|
|
91
|
+
`Gr{\"u}n`), and to the value of every `@string` definition. Field
|
|
92
|
+
keys for which `skip_texify` is `True` (URL and `bdsk-file` fields)
|
|
93
|
+
are left untouched, as are non-string values (such as
|
|
94
|
+
{any}`bibdeskparser.bdskfile.BibDeskFile` objects).
|
|
95
|
+
|
|
96
|
+
This replicates the `-stringByTeXifyingString:` normalization that
|
|
97
|
+
BibDesk applies to every field value on write. It is typically used
|
|
98
|
+
with `allow_inplace_modification=False` so that serializing a
|
|
99
|
+
library does not modify the caller's Unicode model.
|
|
100
|
+
|
|
101
|
+
* `allow_inplace_modification`: if `True` (default), transform the
|
|
102
|
+
given library's blocks in place instead of copying them (see the
|
|
103
|
+
`bibtexparser` `Middleware` base class).
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def transform_entry(self, entry, library):
|
|
107
|
+
"""Texify all string field values of `entry`."""
|
|
108
|
+
for field in entry.fields:
|
|
109
|
+
if not skip_texify(field.key) and isinstance(field.value, str):
|
|
110
|
+
field.value = texify(field.value)
|
|
111
|
+
return entry
|
|
112
|
+
|
|
113
|
+
def transform_string(self, string, library):
|
|
114
|
+
"""Texify the value of the `@string` definition `string`."""
|
|
115
|
+
if isinstance(string.value, str):
|
|
116
|
+
string.value = texify(string.value)
|
|
117
|
+
return string
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class BibDeskFileMiddleware(BlockMiddleware):
|
|
121
|
+
"""Middleware decoding `bdsk-file-N` field values (*read*).
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
BibDeskFileMiddleware(allow_inplace_modification=True)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Replaces the string value of every field whose (lowercased) key
|
|
128
|
+
starts with `bdsk-file-` with the decoded
|
|
129
|
+
{any}`bibdeskparser.bdskfile.BibDeskFile` object. The writer
|
|
130
|
+
serializes such objects back via
|
|
131
|
+
{any}`bibdeskparser.bdskfile.BibDeskFile.to_field_value`, which is
|
|
132
|
+
byte-exact for unmodified attachments.
|
|
133
|
+
|
|
134
|
+
* `allow_inplace_modification`: if `True` (default), transform the
|
|
135
|
+
given library's blocks in place instead of copying them (see the
|
|
136
|
+
`bibtexparser` `Middleware` base class).
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def transform_entry(self, entry, library):
|
|
140
|
+
"""Decode all `bdsk-file-N` field values of `entry`."""
|
|
141
|
+
for field in entry.fields:
|
|
142
|
+
is_file_field = field.key.lower().startswith("bdsk-file-")
|
|
143
|
+
if is_file_field and isinstance(field.value, str):
|
|
144
|
+
field.value = BibDeskFile.from_field_value(field.value)
|
|
145
|
+
return entry
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def parse_stack():
|
|
149
|
+
"""Return the standard middleware stack for reading BibDesk files.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
parse_stack()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Returns a list of fresh middleware instances,
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
[
|
|
159
|
+
DeTeXifyMiddleware(),
|
|
160
|
+
BibDeskFileMiddleware(),
|
|
161
|
+
]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
suitable as the `parse_stack` argument of
|
|
165
|
+
`bibtexparser.parse_string` or `bibtexparser.parse_file`. Passing
|
|
166
|
+
an explicit stack (instead of `None`) also disables bibtexparser's
|
|
167
|
+
default middlewares, so values stay verbatim otherwise: `@string`
|
|
168
|
+
macros are not interpolated and enclosing braces are kept, exactly
|
|
169
|
+
like BibDesk's internal model.
|
|
170
|
+
"""
|
|
171
|
+
return [
|
|
172
|
+
DeTeXifyMiddleware(),
|
|
173
|
+
BibDeskFileMiddleware(),
|
|
174
|
+
]
|
bibdeskparser/names.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
r"""Read-only structured view of person-name fields (`author`, `editor`).
|
|
2
|
+
|
|
3
|
+
BibDesk stores the `author`/`editor` field as a flat, de-TeXified Unicode
|
|
4
|
+
string, e.g. `"Goerz, Michael H and Calarco, Tommaso"`, and only *derives*
|
|
5
|
+
a structured representation (first/von/last/Jr name parts) for display in
|
|
6
|
+
its UI; it never re-serializes that structured view back to the field.
|
|
7
|
+
This module mirrors that: `structured_names` builds a read-only
|
|
8
|
+
`list` of `NameParts` on demand, using `bibtexparser`'s own name-splitting
|
|
9
|
+
middlewares (`RemoveEnclosingMiddleware`, `SeparateCoAuthors`,
|
|
10
|
+
`SplitNameParts`), run on a throwaway single-field `Entry` wrapped in a
|
|
11
|
+
throwaway `bibtexparser.Library` so that the caller's real entry is never
|
|
12
|
+
touched.
|
|
13
|
+
|
|
14
|
+
`bibdeskparser` never uses this pipeline for *writing*: `bibtexparser`
|
|
15
|
+
also provides a `MergeNameParts` middleware that goes the other way
|
|
16
|
+
(`NameParts` -> string), but round-tripping through it is lossy. For
|
|
17
|
+
example, a LaTeX tie in `Koch, C.~P.` would be rewritten as `Koch, C. P.`,
|
|
18
|
+
silently losing the non-breaking space BibDesk's own editor preserves.
|
|
19
|
+
So `author`/`editor` stay plain strings in the dict interface, and
|
|
20
|
+
`structured_names` is only ever used to build the read-only
|
|
21
|
+
`Entry.author`/`Entry.editor` properties.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import bibtexparser
|
|
25
|
+
from bibtexparser.middlewares import (
|
|
26
|
+
RemoveEnclosingMiddleware,
|
|
27
|
+
SeparateCoAuthors,
|
|
28
|
+
SplitNameParts,
|
|
29
|
+
)
|
|
30
|
+
from bibtexparser.model import Entry, Field
|
|
31
|
+
|
|
32
|
+
__all__ = []
|
|
33
|
+
|
|
34
|
+
# All members whose name does not start with an underscore must be listed
|
|
35
|
+
# either in __all__ or in __private__
|
|
36
|
+
__private__ = ["structured_names"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def structured_names(value, allow_inplace_modification=True):
|
|
40
|
+
r"""Parse a raw `author`/`editor` field value into structured names.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
structured_names(value, allow_inplace_modification=True)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
* `value`: the raw field value string, e.g.
|
|
47
|
+
`"{Goerz, Michael H and Calarco, Tommaso}"` (with or without its
|
|
48
|
+
enclosing `{...}`/`"..."` delimiters) or `""`/`None` for a missing
|
|
49
|
+
field.
|
|
50
|
+
* `allow_inplace_modification`: passed through to the underlying
|
|
51
|
+
`bibtexparser` middlewares (see the `bibtexparser`
|
|
52
|
+
`Middleware` base class).
|
|
53
|
+
|
|
54
|
+
Returns a `list` of `bibtexparser.middlewares.names.NameParts`, each
|
|
55
|
+
with `.first`, `.von`, `.last`, `.jr` attributes (each a `list` of
|
|
56
|
+
strings), in the order the names appear in `value`. Returns `[]` if
|
|
57
|
+
`value` is empty or `None`.
|
|
58
|
+
|
|
59
|
+
This is a **read-only derived view**: BibDesk stores `author` and
|
|
60
|
+
`editor` as flat Unicode strings and only derives a structured
|
|
61
|
+
representation for display, so `bibdeskparser` never writes through
|
|
62
|
+
this pipeline (see the module docstring for why re-serializing via
|
|
63
|
+
`MergeNameParts` would be lossy).
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
>>> from bibdeskparser.names import structured_names
|
|
67
|
+
>>> names = structured_names(
|
|
68
|
+
... "Goerz, Michael H and Calarco, Tommaso and Koch, Christiane P"
|
|
69
|
+
... )
|
|
70
|
+
>>> [n.last for n in names]
|
|
71
|
+
[['Goerz'], ['Calarco'], ['Koch']]
|
|
72
|
+
>>> names[0].first
|
|
73
|
+
['Michael', 'H']
|
|
74
|
+
>>> structured_names("")
|
|
75
|
+
[]
|
|
76
|
+
>>> structured_names(None)
|
|
77
|
+
[]
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
"""
|
|
81
|
+
if not value:
|
|
82
|
+
return []
|
|
83
|
+
field = Field(key="author", value=value)
|
|
84
|
+
entry = Entry(entry_type="article", key="probe", fields=[field])
|
|
85
|
+
library = bibtexparser.Library([entry])
|
|
86
|
+
for middleware in (
|
|
87
|
+
RemoveEnclosingMiddleware(
|
|
88
|
+
allow_inplace_modification=allow_inplace_modification
|
|
89
|
+
),
|
|
90
|
+
SeparateCoAuthors(
|
|
91
|
+
allow_inplace_modification=allow_inplace_modification
|
|
92
|
+
),
|
|
93
|
+
SplitNameParts(allow_inplace_modification=allow_inplace_modification),
|
|
94
|
+
):
|
|
95
|
+
library = middleware.transform(library)
|
|
96
|
+
return library.entries[0].fields_dict["author"].value
|