Skip to content

Commit

Permalink
Check for ANSI colors in all plain text outputs
Browse files Browse the repository at this point in the history
Closes #330.

Co-authored-by: jfbu <jfbu@free.fr>
  • Loading branch information
mgeier and jfbu committed Nov 13, 2019
1 parent 5faf5bb commit 791c6ea
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 87 deletions.
48 changes: 46 additions & 2 deletions doc/pre-executed.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,50 @@
"source": [
"1 / 0"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Client-specific Outputs\n",
"\n",
"When `nbsphinx` executes notebooks,\n",
"it uses the `nbconvert` module to do so.\n",
"Certain Jupyter clients might produce output\n",
"that differs from what `nbconvert` would produce.\n",
"To preserve those original outputs,\n",
"the notebook has to be executed and saved\n",
"before running Sphinx.\n",
"\n",
"For example,\n",
"the JupyterLab help system shows the help text as cell outputs,\n",
"while executing with `nbconvert` doesn't produce any output."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[0;31mSignature:\u001b[0m \u001b[0msorted\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0miterable\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m/\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mreverse\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mDocstring:\u001b[0m\n",
"Return a new list containing all items from the iterable in ascending order.\n",
"\n",
"A custom key function can be supplied to customize the sort order, and the\n",
"reverse flag can be set to request the result in descending order.\n",
"\u001b[0;31mType:\u001b[0m builtin_function_or_method\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"sorted?"
]
}
],
"metadata": {
Expand All @@ -166,13 +210,13 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.4"
"version": "3.7.5rc1"
},
"widgets": {
"state": {},
"version": "0.2.0"
}
},
"nbformat": 4,
"nbformat_minor": 1
"nbformat_minor": 4
}
155 changes: 70 additions & 85 deletions src/nbsphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,19 +111,13 @@
{%- if not cell.outputs %}
:no-output:
{%- endif %}
{%- if cell.source.strip() %}
{{ cell.source.strip('\n') | indent }}
{%- endif %}
{% endblock input %}
{% macro insert_nboutput(datatype, output, cell) -%}
.. nboutput::
{%- if datatype == 'text/plain' %}{# nothing #}
{%- elif datatype == 'ansi' %} ansi
{%- else %} rst
{%- endif %}
{%- if output.output_type == 'execute_result' and cell.execution_count %}
:execution-count: {{ cell.execution_count }}
{%- endif %}
Expand All @@ -133,10 +127,25 @@
{%- if output.name == 'stderr' %}
:class: stderr
{%- endif %}
{%- if datatype == 'text/plain' -%}
{{ insert_empty_lines(output.data[datatype]) }}
{%- if datatype != 'text/plain' %}
:fancy:
{%- endif %}
{%- if datatype == 'text/plain' %}
.. rst-class:: highlight
.. raw:: html
<pre>
{{ output.data[datatype] | ansi2html | indent | indent }}
</pre>
.. raw:: latex
\\begin{sphinxVerbatim}[commandchars=\\\\\\{\\}]
{{ output.data[datatype] | escape_latex | ansi2latex | indent | indent }}
\\end{sphinxVerbatim}
{{ output.data[datatype].strip('\n') | indent }}
{%- elif datatype in ['image/svg+xml', 'image/png', 'image/jpeg', 'application/pdf'] %}
.. image:: {{ output.metadata.filenames[datatype] | posix_path }}
Expand Down Expand Up @@ -179,38 +188,6 @@
.. raw:: html
<script type="{{ datatype }}">{{ output.data[datatype] | json_dumps }}</script>
{%- elif datatype == 'ansi' %}
.. rst-class:: highlight
.. raw:: html
<pre>
{{ output.data[datatype] | ansi2html | indent | indent }}
</pre>
.. raw:: latex
%
{
\\kern-\\sphinxverbatimsmallskipamount\\kern-\\baselineskip
\\kern+\\FrameHeightAdjust\\kern-\\fboxrule
\\vspace{\\nbsphinxcodecellspacing}
\\sphinxsetup{VerbatimBorderColor={named}{nbsphinx-code-border}}
{%- if output.name == 'stderr' %}
\\sphinxsetup{VerbatimColor={named}{nbsphinx-stderr}}
{%- else %}
\\sphinxsetup{VerbatimColor={named}{white}}
{%- endif %}
\\fvset{hllines={, ,}}%
\\begin{sphinxVerbatim}[commandchars=\\\\\\{\\}]
{{ output.data[datatype] | escape_latex | ansi2latex | indent | indent }}
\\end{sphinxVerbatim}
}
% The following \\relax is needed to avoid problems with adjacent ANSI
% cells and some other stuff (e.g. bullet lists) following ANSI cells.
% See https://github.com/sphinx-doc/sphinx/issues/3594
\\relax
{% else %}
.. nbwarning:: Data type cannot be displayed: {{ datatype }}
Expand Down Expand Up @@ -419,6 +396,20 @@
\expandafter\includegraphics\expandafter[\spx@includegraphics@options]{#2}%
}% end of "\MakeFrame"-safe variant of \sphinxincludegraphics
\makeatother
\makeatletter
\renewcommand*\sphinx@verbatim@nolig@list{\do\'\do\`}
\begingroup
\catcode`'=\active
\let\nbsphinx@noligs\@noligs
\g@addto@macro\nbsphinx@noligs{\let'\PYGZsq}
\endgroup
\makeatother
\renewcommand*\sphinxbreaksbeforeactivelist{\do\<\do\"\do\'}
\renewcommand*\sphinxbreaksafteractivelist{\do\.\do\,\do\:\do\;\do\?\do\!\do\/\do\>\do\-}
\makeatletter
\fvset{codes*=\sphinxbreaksattexescapedchars\do\^\^\let\@noligs\nbsphinx@noligs}
\makeatother
"""


Expand Down Expand Up @@ -918,26 +909,23 @@ class NotebookError(sphinx.errors.SphinxError):


class CodeAreaNode(docutils.nodes.Element):
"""Input area or output area of a Jupyter notebook code cell."""
"""Input area or plain-text output area of a Jupyter notebook code cell."""


class FancyOutputNode(docutils.nodes.Element):
"""A custom node for non-code output of code cells."""
"""A custom node for non-plain-text output of code cells."""


def _create_code_nodes(directive):
"""Create nodes for an input or output code cell."""
fancy_output = False
language = 'none'
directive.state.document['nbsphinx_include_css'] = True
execution_count = directive.options.get('execution-count')
config = directive.state.document.settings.env.config
if isinstance(directive, NbInput):
outer_classes = ['nbinput']
if 'no-output' in directive.options:
outer_classes.append('nblast')
inner_classes = ['input_area']
if directive.arguments:
language = directive.arguments[0]
prompt_template = config.nbsphinx_input_prompt
if not execution_count:
execution_count = ' '
Expand All @@ -946,11 +934,8 @@ def _create_code_nodes(directive):
if 'more-to-come' not in directive.options:
outer_classes.append('nblast')
inner_classes = ['output_area']
# 'class' can be 'stderr'
inner_classes.append(directive.options.get('class', ''))
prompt_template = config.nbsphinx_output_prompt
if directive.arguments and directive.arguments[0] in ['rst', 'ansi']:
fancy_output = True
else:
assert False

Expand All @@ -965,21 +950,24 @@ def _create_code_nodes(directive):
# NB: Prompts are added manually in LaTeX output
outer_node += sphinx.addnodes.only('', prompt_node, expr='html')

if fancy_output:
if isinstance(directive, NbInput):
text = '\n'.join(directive.content.data)
if directive.arguments:
language = directive.arguments[0]
else:
language = 'none'
inner_node = docutils.nodes.literal_block(
text, text, language=language, classes=inner_classes)
else:
inner_node = docutils.nodes.container(classes=inner_classes)
sphinx.util.nodes.nested_parse_with_titles(
directive.state, directive.content, inner_node)
if directive.arguments[0] == 'rst':
outer_node += FancyOutputNode('', inner_node, prompt=prompt)
elif directive.arguments[0] == 'ansi':
outer_node += inner_node
else:
assert False

if 'fancy' in directive.options:
outer_node += FancyOutputNode('', inner_node, prompt=prompt)
else:
text = '\n'.join(directive.content.data)
inner_node = docutils.nodes.literal_block(
text, text, language=language, classes=inner_classes)
codearea_node = CodeAreaNode('', inner_node, prompt=prompt)
codearea_node = CodeAreaNode(
'', inner_node, prompt=prompt, stderr='stderr' in inner_classes)
# See http://stackoverflow.com/q/34050044/.
for attr in 'empty-lines-before', 'empty-lines-after':
value = directive.options.get(attr, 0)
Expand Down Expand Up @@ -1011,28 +999,24 @@ class NbInput(rst.Directive):

def run(self):
"""This is called by the reST parser."""
self.state.document['nbsphinx_include_css'] = True
return _create_code_nodes(self)


class NbOutput(rst.Directive):
"""A notebook output cell with optional prompt."""

required_arguments = 0
optional_arguments = 1 # 'rst' or nothing (which means literal text)
final_argument_whitespace = False
option_spec = {
'execution-count': rst.directives.positive_int,
'more-to-come': rst.directives.flag,
'empty-lines-before': rst.directives.nonnegative_int,
'empty-lines-after': rst.directives.nonnegative_int,
'fancy': rst.directives.flag,
'class': rst.directives.unchanged,
}
has_content = True

def run(self):
"""This is called by the reST parser."""
self.state.document['nbsphinx_include_css'] = True
return _create_code_nodes(self)


Expand Down Expand Up @@ -1250,12 +1234,12 @@ def _get_empty_lines(text):
def _get_output_type(output):
"""Choose appropriate output data types for HTML and LaTeX."""
if output.output_type == 'stream':
html_datatype = latex_datatype = 'ansi'
html_datatype = latex_datatype = 'text/plain'
text = output.text
output.data = {'ansi': text[:-1] if text.endswith('\n') else text}
output.data = {'text/plain': text[:-1] if text.endswith('\n') else text}
elif output.output_type == 'error':
html_datatype = latex_datatype = 'ansi'
output.data = {'ansi': '\n'.join(output.traceback)}
html_datatype = latex_datatype = 'text/plain'
output.data = {'text/plain': '\n'.join(output.traceback)}
else:
for datatype in DISPLAY_DATA_PRIORITY_HTML:
if datatype in output.data:
Expand Down Expand Up @@ -1679,12 +1663,10 @@ def depart_codearea_latex(self, node):
* Add prompt
"""
lines = ''.join(self.popbody()).strip('\n').split('\n')
out = []
lines = ''.join(self.popbody()).split('\n')
assert lines[0] == ''
out.append(lines[0])
out.append('')
out.append('{') # Start a scope for colors
prompt = node['prompt']
if 'nbinput' in node.parent['classes']:
promptcolor = 'nbsphinxin'
out.append(r'\sphinxsetup{VerbatimColor={named}{nbsphinx-code-bg}}')
Expand All @@ -1693,34 +1675,37 @@ def depart_codearea_latex(self, node):
\kern-\sphinxverbatimsmallskipamount\kern-\baselineskip
\kern+\FrameHeightAdjust\kern-\fboxrule
\vspace{\nbsphinxcodecellspacing}
\sphinxsetup{VerbatimColor={named}{white}}
""")
promptcolor = 'nbsphinxout'
if node['stderr']:
out.append(r'\sphinxsetup{VerbatimColor={named}{nbsphinx-stderr}}')
else:
out.append(r'\sphinxsetup{VerbatimColor={named}{white}}')

out.append(
r'\sphinxsetup{VerbatimBorderColor={named}{nbsphinx-code-border}}')
if lines[1].startswith(r'\fvset{'): # Sphinx >= 1.6.6 and < 1.8.3
out.append(lines[1])
del lines[1]
assert 'Verbatim' in lines[1]
out.append(lines[1])
if lines[0].startswith(r'\fvset{'): # Sphinx >= 1.6.6 and < 1.8.3
out.append(lines[0])
del lines[0]
assert 'Verbatim' in lines[0]
out.append(lines[0])
code_lines = (
[''] * node.get('empty-lines-before', 0) +
lines[2:-2] +
lines[1:-1] +
[''] * node.get('empty-lines-after', 0)
)
prompt = node['prompt']
if prompt:
prompt = nbconvert.filters.latex.escape_latex(prompt)
prefix = r'\llap{\color{' + promptcolor + '}' + prompt + \
r'\,\hspace{\fboxrule}\hspace{\fboxsep}}'
assert code_lines
code_lines[0] = prefix + code_lines[0]
out.extend(code_lines)
assert 'Verbatim' in lines[-2]
out.append(lines[-2])
out.append('}') # End of scope for colors
assert lines[-1] == ''
assert 'Verbatim' in lines[-1]
out.append(lines[-1])
out.append('}') # End of scope for colors
out.append('')
self.body.append('\n'.join(out))


Expand Down

0 comments on commit 791c6ea

Please sign in to comment.