1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
|
"""
New Doctree Directives
~~~~~~~~~~~~~~~~~~~~~~
.. autoclass:: BibliographyDirective
.. automethod:: run
.. automethod:: process_bibfile
.. automethod:: update_bibfile_cache
.. automethod:: parse_bibfile
.. autofunction:: process_start_option
"""
import ast # parse(), used for filter
import os.path # getmtime()
from docutils.parsers.rst import directives # for Directive.option_spec
from sphinx.util.compat import Directive
from sphinx.util.console import bold, standout
from pybtex.database.input import bibtex
from pybtex.database import BibliographyData
from sphinxcontrib.bibtex.cache import BibliographyCache, BibfileCache
from sphinxcontrib.bibtex.nodes import bibliography
# register the latex codec
import latexcodec # noqa
def process_start_option(value):
"""Process and validate the start option value
of a :rst:dir:`bibliography` directive.
If *value* is ``continue`` then this function returns -1,
otherwise *value* is converted into a positive integer.
"""
if value == "continue":
return -1
else:
return directives.positive_int(value)
class BibliographyDirective(Directive):
"""Class for processing the :rst:dir:`bibliography` directive.
Parses the bibliography files, and produces a
:class:`~sphinxcontrib.bibtex.nodes.bibliography` node.
.. seealso::
Further processing of the resulting
:class:`~sphinxcontrib.bibtex.nodes.bibliography` node is done
by
:class:`~sphinxcontrib.bibtex.transforms.BibliographyTransform`.
"""
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
has_content = False
option_spec = {
'cited': directives.flag,
'notcited': directives.flag,
'all': directives.flag,
'filter': directives.unchanged,
'style': directives.unchanged,
'list': directives.unchanged,
'enumtype': directives.unchanged,
'start': process_start_option,
'encoding': directives.encoding,
'disable-curly-bracket-strip': directives.flag,
'labelprefix': directives.unchanged,
'keyprefix': directives.unchanged,
}
def run(self):
"""Process .bib files, set file dependencies, and create a
node that is to be transformed to the entries of the
bibliography.
"""
env = self.state.document.settings.env
# create id and cache for this node
# this id will be stored with the node
# and is used to look up additional data in env.bibtex_cache
# (implementation note: new_serialno only guarantees unique
# ids within a single document, but we need the id to be
# unique across all documents, so we also include the docname
# in the id)
id_ = 'bibtex-bibliography-%s-%s' % (
env.docname, env.new_serialno('bibtex'))
if "filter" in self.options:
if "all" in self.options:
env.app.warn(standout(":filter: overrides :all:"))
if "notcited" in self.options:
env.app.warn(standout(":filter: overrides :notcited:"))
if "cited" in self.options:
env.app.warn(standout(":filter: overrides :cited:"))
try:
filter_ = ast.parse(self.options["filter"])
except SyntaxError:
env.app.warn(
standout("syntax error in :filter: expression") +
" (" + self.options["filter"] + "); "
"the option will be ignored"
)
filter_ = ast.parse("cited")
elif "all" in self.options:
filter_ = ast.parse("True")
elif "notcited" in self.options:
filter_ = ast.parse("not cited")
else:
# the default filter: include only cited entries
filter_ = ast.parse("cited")
bibcache = BibliographyCache(
list_=self.options.get("list", "citation"),
enumtype=self.options.get("enumtype", "arabic"),
start=self.options.get("start", 1),
style=self.options.get(
"style", env.app.config.bibtex_default_style),
filter_=filter_,
encoding=self.options.get(
'encoding',
'latex+' + self.state.document.settings.input_encoding),
curly_bracket_strip=(
'disable-curly-bracket-strip' not in self.options),
labelprefix=self.options.get("labelprefix", ""),
keyprefix=self.options.get("keyprefix", ""),
labels={},
bibfiles=[],
)
if (bibcache.list_ not in set(["bullet", "enumerated", "citation"])):
env.app.warn(
"unknown bibliography list type '{0}'.".format(bibcache.list_))
for bibfile in self.arguments[0].split():
# convert to normalized absolute path to ensure that the same file
# only occurs once in the cache
bibfile = os.path.normpath(env.relfn2path(bibfile.strip())[1])
self.process_bibfile(bibfile, bibcache.encoding)
env.note_dependency(bibfile)
bibcache.bibfiles.append(bibfile)
env.bibtex_cache.set_bibliography_cache(env.docname, id_, bibcache)
return [bibliography('', ids=[id_])]
def parse_bibfile(self, bibfile, encoding):
"""Parse *bibfile*, and return parsed data.
:param bibfile: The bib file name.
:type bibfile: ``str``
:return: The parsed bibliography data.
:rtype: :class:`pybtex.database.BibliographyData`
"""
app = self.state.document.settings.env.app
parser = bibtex.Parser(encoding)
app.info(
bold("parsing bibtex file {0}... ".format(bibfile)), nonl=True)
parser.parse_file(bibfile)
app.info("parsed {0} entries"
.format(len(parser.data.entries)))
return parser.data
def update_bibfile_cache(self, bibfile, mtime, encoding):
"""Parse *bibfile* (see :meth:`parse_bibfile`), and store the
parsed data, along with modification time *mtime*, in the
bibtex cache.
:param bibfile: The bib file name.
:type bibfile: ``str``
:param mtime: The bib file's modification time.
:type mtime: ``float``
:return: The parsed bibliography data.
:rtype: :class:`pybtex.database.BibliographyData`
"""
data = self.parse_bibfile(bibfile, encoding)
env = self.state.document.settings.env
env.bibtex_cache.bibfiles[bibfile] = BibfileCache(
mtime=mtime,
data=data)
return data
def process_bibfile(self, bibfile, encoding):
"""Check if ``env.bibtex_cache.bibfiles[bibfile]`` is still
up to date. If not, parse the *bibfile* (see
:meth:`update_bibfile_cache`), and store parsed data in the
bibtex cache.
:param bibfile: The bib file name.
:type bibfile: ``str``
:return: The parsed bibliography data.
:rtype: :class:`pybtex.database.BibliographyData`
"""
env = self.state.document.settings.env
cache = env.bibtex_cache.bibfiles
# get modification time of bibfile
try:
mtime = os.path.getmtime(bibfile)
except OSError:
env.app.warn(
standout("could not open bibtex file {0}.".format(bibfile)))
cache[bibfile] = BibfileCache( # dummy cache
mtime=-float("inf"), data=BibliographyData())
return cache[bibfile].data
# get cache and check if it is still up to date
# if it is not up to date, parse the bibtex file
# and store it in the cache
env.app.info(
bold("checking for {0} in bibtex cache... ".format(bibfile)),
nonl=True)
try:
bibfile_cache = cache[bibfile]
except KeyError:
env.app.info("not found")
self.update_bibfile_cache(bibfile, mtime, encoding)
else:
if mtime != bibfile_cache.mtime:
env.app.info("out of date")
self.update_bibfile_cache(bibfile, mtime, encoding)
else:
env.app.info('up to date')
return cache[bibfile].data
|