##// END OF EJS Templates
Merge pull request #4090 from ellisonbg/citation...
Brian E. Granger -
r12361:9f928047 merge
parent child Browse files
Show More
@@ -0,0 +1,72
1 """Citation handling for LaTeX output."""
2
3 #-----------------------------------------------------------------------------
4 # Copyright (c) 2013, the IPython Development Team.
5 #
6 # Distributed under the terms of the Modified BSD License.
7 #
8 # The full license is in the file COPYING.txt, distributed with this software.
9 #-----------------------------------------------------------------------------
10
11 #-----------------------------------------------------------------------------
12 # Code
13 #-----------------------------------------------------------------------------
14
15
16 __all__ = ['citation2latex']
17
18
19 def citation2latex(s):
20 """Parse citations in Markdown cells.
21
22 This looks for HTML tags having a data attribute names `data-cite`
23 and replaces it by the call to LaTeX cite command. The tranformation
24 looks like this:
25
26 `<cite data-cite="granger">(Granger, 2013)</cite>`
27
28 Becomes
29
30 `\\cite{granger}`
31
32 Any HTML tag can be used, which allows the citations to be formatted
33 in HTML in any manner.
34 """
35 try:
36 from lxml import html
37 except ImportError:
38 return s
39
40 tree = html.fragment_fromstring(s, create_parent='div')
41 _process_node_cite(tree)
42 s = html.tostring(tree)
43 if s.endswith('</div>'):
44 s = s[:-6]
45 if s.startswith('<div>'):
46 s = s[5:]
47 return s
48
49
50 def _process_node_cite(node):
51 """Do the citation replacement as we walk the lxml tree."""
52
53 def _get(o, name):
54 value = getattr(o, name, None)
55 return '' if value is None else value
56
57 if 'data-cite' in node.attrib:
58 cite = '\cite{%(ref)s}' % {'ref': node.attrib['data-cite']}
59 prev = node.getprevious()
60 if prev is not None:
61 prev.tail = _get(prev, 'tail') + cite + _get(node, 'tail')
62 else:
63 parent = node.getparent()
64 if parent is not None:
65 parent.text = _get(parent, 'text') + cite + _get(node, 'tail')
66 try:
67 node.getparent().remove(node)
68 except AttributeError:
69 pass
70 else:
71 for child in node:
72 _process_node_cite(child)
@@ -0,0 +1,58
1 #-----------------------------------------------------------------------------
2 # Copyright (c) 2013, the IPython Development Team.
3 #
4 # Distributed under the terms of the Modified BSD License.
5 #
6 # The full license is in the file COPYING.txt, distributed with this software.
7 #-----------------------------------------------------------------------------
8
9 #-----------------------------------------------------------------------------
10 # Imports
11 #-----------------------------------------------------------------------------
12
13 from ..citation import citation2latex
14
15 #-----------------------------------------------------------------------------
16 # Tests
17 #-----------------------------------------------------------------------------
18
19 test_md = """
20 # My Heading
21
22 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus ac magna non augue
23 porttitor scelerisque ac id diam <cite data-cite="granger">Granger</cite>. Mauris elit
24 velit, lobortis sed interdum at, vestibulum vitae libero <strong data-cite="fperez">Perez</strong>.
25 Lorem ipsum dolor sit amet, consectetur adipiscing elit
26 <em data-cite="takluyver">Thomas</em>. Quisque iaculis ligula ut ipsum mattis viverra.
27
28 <p>Here is a plain paragraph that should be unaffected.</p>
29
30 * One <cite data-cite="jdfreder">Jonathan</cite>.
31 * Two <cite data-cite="carreau">Matthias</cite>.
32 * Three <cite data-cite="ivanov">Paul</cite>.
33 """
34
35 test_md_parsed = """
36 # My Heading
37
38 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus ac magna non augue
39 porttitor scelerisque ac id diam \cite{granger}. Mauris elit
40 velit, lobortis sed interdum at, vestibulum vitae libero \cite{fperez}.
41 Lorem ipsum dolor sit amet, consectetur adipiscing elit
42 \cite{takluyver}. Quisque iaculis ligula ut ipsum mattis viverra.
43
44 <p>Here is a plain paragraph that should be unaffected.</p>
45
46 * One \cite{jdfreder}.
47 * Two \cite{carreau}.
48 * Three \cite{ivanov}.
49 """
50
51 def test_citation2latex():
52 """Are citations parsed properly?"""
53 try:
54 import lxml
55 except ImportError:
56 assert test_md == citation2latex(test_md)
57 else:
58 assert test_md_parsed == citation2latex(test_md)
@@ -68,6 +68,7 default_filters = {
68 'strip_math_space': filters.strip_math_space,
68 'strip_math_space': filters.strip_math_space,
69 'wrap_text': filters.wrap_text,
69 'wrap_text': filters.wrap_text,
70 'escape_latex': filters.escape_latex,
70 'escape_latex': filters.escape_latex,
71 'citation2latex': filters.citation2latex
71 }
72 }
72
73
73 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
@@ -3,4 +3,5 from .datatypefilter import *
3 from .highlight import *
3 from .highlight import *
4 from .latex import *
4 from .latex import *
5 from .markdown import *
5 from .markdown import *
6 from .strings import * No newline at end of file
6 from .strings import *
7 from .citation import * No newline at end of file
@@ -26,35 +26,107 from .base import PostProcessorBase
26 class PDFPostProcessor(PostProcessorBase):
26 class PDFPostProcessor(PostProcessorBase):
27 """Writer designed to write to PDF files"""
27 """Writer designed to write to PDF files"""
28
28
29 iteration_count = Integer(3, config=True, help="""
29 latex_count = Integer(3, config=True, help="""
30 How many times pdflatex will be called.
30 How many times pdflatex will be called.
31 """)
31 """)
32
32
33 command = List(["pdflatex", "{filename}"], config=True, help="""
33 latex_command = List(["pdflatex", "{filename}"], config=True, help="""
34 Shell command used to compile PDF.""")
34 Shell command used to compile PDF.""")
35
35
36 bib_command = List(["bibtex", "{filename}"], config=True, help="""
37 Shell command used to run bibtex.""")
38
36 verbose = Bool(False, config=True, help="""
39 verbose = Bool(False, config=True, help="""
37 Whether or not to display the output of the compile call.
40 Whether or not to display the output of the compile call.
38 """)
41 """)
39
42
40 def postprocess(self, input):
43 temp_file_exts = List(['.aux', '.bbl', '.blg', '.idx', '.log', '.out'],
41 """
44 config=True, help="""
42 Consume and write Jinja output a PDF.
45 Filename extensions of temp files to remove after running.
43 See files.py for more...
46 """)
44 """
47
45 command = [c.format(filename=input) for c in self.command]
48 def run_command(self, command_list, filename, count, log_function):
46 self.log.info("Building PDF: %s", command)
49 """Run command_list count times.
47 with open(os.devnull, 'rb') as null:
50
48 stdout = subprocess.PIPE if not self.verbose else None
51 Parameters
49 for index in range(self.iteration_count):
52 ----------
50 p = subprocess.Popen(command, stdout=stdout, stdin=null)
53 command_list : list
51 out, err = p.communicate()
54 A list of args to provide to Popen. Each element of this
52 if p.returncode:
55 list will be interpolated with the filename to convert.
53 if self.verbose:
56 filename : unicode
54 # verbose means I didn't capture stdout with PIPE,
57 The name of the file to convert.
55 # so it's already been displayed and `out` is None.
58 count : int
56 out = u''
59 How many times to run the command.
57 else:
60
58 out = out.decode('utf-8', 'replace')
61 Returns
59 self.log.critical(u"PDF conversion failed: %s\n%s", command, out)
62 -------
60 return
63 continue : bool
64 A boolean indicating if the command was successful (True)
65 or failed (False).
66 """
67 command = [c.format(filename=filename) for c in command_list]
68 times = 'time' if count == 1 else 'times'
69 self.log.info("Running %s %i %s: %s", command_list[0], count, times, command)
70 with open(os.devnull, 'rb') as null:
71 stdout = subprocess.PIPE if not self.verbose else None
72 for index in range(count):
73 p = subprocess.Popen(command, stdout=stdout, stdin=null)
74 out, err = p.communicate()
75 if p.returncode:
76 if self.verbose:
77 # verbose means I didn't capture stdout with PIPE,
78 # so it's already been displayed and `out` is None.
79 out = u''
80 else:
81 out = out.decode('utf-8', 'replace')
82 log_function(command, out)
83 return False # failure
84 return True # success
85
86 def run_latex(self, filename):
87 """Run pdflatex self.latex_count times."""
88
89 def log_error(command, out):
90 self.log.critical(u"%s failed: %s\n%s", command[0], command, out)
91
92 return self.run_command(self.latex_command, filename,
93 self.latex_count, log_error)
94
95 def run_bib(self, filename):
96 """Run bibtex self.latex_count times."""
97 filename = os.path.splitext(filename)[0]
98
99 def log_error(command, out):
100 self.log.warn('%s had problems, most likely because there were no citations',
101 command[0])
102 self.log.debug(u"%s output: %s\n%s", command[0], command, out)
103
104 return self.run_command(self.bib_command, filename, 1, log_error)
105
106 def clean_temp_files(self, filename):
107 """Remove temporary files created by pdflatex/bibtext."""
108 self.log.info("Removing temporary LaTeX files")
109 filename = os.path.splitext(filename)[0]
110 for ext in self.temp_file_exts:
111 try:
112 os.remove(filename+ext)
113 except OSError:
114 pass
115
116 def postprocess(self, filename):
117 """Build a PDF by running pdflatex and bibtex"""
118 self.log.info("Building PDF")
119 cont = self.run_latex(filename)
120 if cont:
121 cont = self.run_bib(filename)
122 else:
123 self.clean_temp_files(filename)
124 return
125 if cont:
126 cont = self.run_latex(filename)
127 self.clean_temp_files(filename)
128 filename = os.path.splitext(filename)[0]
129 if os.path.isfile(filename+'.pdf'):
130 self.log.info('PDF successfully created')
131 return
132
@@ -62,3 +62,7 class TestPDF(TestsBase):
62
62
63 # Check that the PDF was created.
63 # Check that the PDF was created.
64 assert os.path.isfile('a.pdf')
64 assert os.path.isfile('a.pdf')
65
66 # Make sure that temp files are cleaned up
67 for ext in processor.temp_file_exts:
68 assert not os.path.isfile('a'+ext)
@@ -103,11 +103,11 it introduces a new line
103 ((* endblock stream *))
103 ((* endblock stream *))
104
104
105 ((* block markdowncell scoped *))
105 ((* block markdowncell scoped *))
106 ((( cell.source | markdown2latex )))
106 ((( cell.source | citation2latex | markdown2latex )))
107 ((* endblock markdowncell *))
107 ((* endblock markdowncell *))
108
108
109 ((* block headingcell scoped -*))
109 ((* block headingcell scoped -*))
110 ((( ('#' * cell.level + cell.source) | replace('\n', ' ') | markdown2latex )))
110 ((( ('#' * cell.level + cell.source) | replace('\n', ' ') | citation2latex | markdown2latex )))
111 ((* endblock headingcell *))
111 ((* endblock headingcell *))
112
112
113 ((* block rawcell scoped *))
113 ((* block rawcell scoped *))
@@ -127,6 +127,10 unknown type ((( cell.type )))
127 ((( super() )))
127 ((( super() )))
128
128
129 ((* block bodyEnd *))
129 ((* block bodyEnd *))
130
131 ((* block bibliography *))
132 ((* endblock bibliography *))
133
130 \end{document}
134 \end{document}
131 ((* endblock bodyEnd *))
135 ((* endblock bodyEnd *))
132 ((* endblock body *))
136 ((* endblock body *))
@@ -192,6 +192,9 Note: For best display, use latex syntax highlighting. =))
192 \renewcommand{\indexname}{Index}
192 \renewcommand{\indexname}{Index}
193 \printindex
193 \printindex
194
194
195 ((* block bibliography *))
196 ((* endblock bibliography *))
197
195 % End of document
198 % End of document
196 \end{document}
199 \end{document}
197 ((* endblock bodyEnd *))
200 ((* endblock bodyEnd *))
@@ -229,7 +232,7 Note: For best display, use latex syntax highlighting. =))
229 in IPYNB file titles) do not make their way into latex. Sometimes this
232 in IPYNB file titles) do not make their way into latex. Sometimes this
230 causes latex to barf. =))
233 causes latex to barf. =))
231 ((*- endif -*))
234 ((*- endif -*))
232 {((( cell.source | markdown2latex )))}
235 {((( cell.source | citation2latex | markdown2latex )))}
233 ((*- endblock headingcell *))
236 ((*- endblock headingcell *))
234
237
235 %==============================================================================
238 %==============================================================================
@@ -239,7 +242,7 Note: For best display, use latex syntax highlighting. =))
239 % called since we know we want latex output.
242 % called since we know we want latex output.
240 %==============================================================================
243 %==============================================================================
241 ((*- block markdowncell scoped-*))
244 ((*- block markdowncell scoped-*))
242 ((( cell.source | markdown2latex )))
245 ((( cell.source | citation2latex | markdown2latex )))
243 ((*- endblock markdowncell -*))
246 ((*- endblock markdowncell -*))
244
247
245 %==============================================================================
248 %==============================================================================
@@ -125,6 +125,21 and using the command::
125
125
126 .. _notebook_format:
126 .. _notebook_format:
127
127
128 LaTeX citations
129 ---------------
130
131 ``nbconvert`` now has support for LaTeX citations. With this capability you
132 can:
133
134 * Manage citations using BibTeX.
135 * Cite those citations in Markdown cells using HTML data attributes.
136 * Have ``nbconvert`` generate proper LaTeX citations and run BibTeX.
137
138 For an example of how this works, please see the citations example in
139 the nbconvert-examples_ repository.
140
141 .. _nbconvert-examples: https://github.com/ipython/nbconvert-examples
142
128 Notebook JSON file format
143 Notebook JSON file format
129 -------------------------
144 -------------------------
130
145
General Comments 0
You need to be logged in to leave comments. Login now