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.
@@ -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