chrisjsewell / ipypublish
1
"""
2
adapted from nbsphinx
3
"""
4 3
import docutils
5 3
from docutils import nodes  # noqa E501
6 3
from docutils.parsers import rst
7 3
from docutils.statemachine import StringList
8

9 3
from ipypublish.sphinx.utils import import_sphinx
10 3
from ipypublish.sphinx.notebook.nodes import (
11
    AdmonitionNode,
12
    CodeAreaNode,
13
    FancyOutputNode,
14
)
15

16

17 3
class NbAdmonition(rst.Directive):
18
    """Base class for NbInfo and NbWarning."""
19

20 3
    required_arguments = 0
21 3
    optional_arguments = 0
22 3
    option_spec = {}
23 3
    has_content = True
24

25 3
    def run(self):
26
        """This is called by the reST parser."""
27 0
        node = AdmonitionNode(classes=["admonition", self._class])
28 0
        self.state.nested_parse(self.content, self.content_offset, node)
29 0
        return [node]
30

31

32 3
class NbWarning(NbAdmonition):
33
    """A warning box."""
34

35 3
    _class = "warning"
36

37

38 3
class NbInfo(NbAdmonition):
39
    """An information box."""
40

41 3
    _class = "note"
42

43

44 3
class NBInputToggle(rst.Directive):
45
    """ a toggle button for nbinput cells """
46

47 3
    _class = "nbinput-toggle-all"
48 3
    _default_text = "Toggle Input Cells"
49

50 3
    required_arguments = 0
51 3
    optional_arguments = 0
52 3
    option_spec = {}
53 3
    has_content = True  # button text
54

55 3
    def run(self):
56
        """This is called by the reST parser."""
57 3
        node = nodes.container()
58 3
        node["classes"].append(self._class)
59 3
        if self.content:
60 3
            self.state.nested_parse(self.content, self.content_offset, node)
61
        else:
62 0
            text = (
63
                self.arguments[0]
64
                if self.arguments and self.arguments[0]
65
                else self._default_text
66
            )
67 0
            paragraph = nodes.paragraph(text=text)
68 0
            node += paragraph
69

70 3
        return [node]
71

72

73 3
class NBOutputToggle(NBInputToggle):
74
    """ a toggle button for nboutput cells """
75

76 3
    _class = "nboutput-toggle-all"
77 3
    _default_text = "Toggle Output Cells"
78

79

80 3
class NbInput(rst.Directive):
81
    """A notebook input cell with prompt and code area."""
82

83 3
    required_arguments = 0
84 3
    optional_arguments = 1  # lexer name
85 3
    final_argument_whitespace = False
86 3
    option_spec = {
87
        "execution-count": rst.directives.positive_int,
88
        "empty-lines-before": rst.directives.nonnegative_int,
89
        "empty-lines-after": rst.directives.nonnegative_int,
90
        "no-output": rst.directives.flag,
91
        "caption": rst.directives.unchanged,
92
        "name": rst.directives.unchanged,
93
        "add-toggle": rst.directives.flag,
94
    }
95 3
    has_content = True
96

97 3
    def run(self):
98
        """This is called by the reST parser."""
99 3
        self.state.document["ipysphinx_include_css"] = True
100 3
        return _create_nbcell_nodes(self)
101

102

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

106 3
    required_arguments = 0
107 3
    optional_arguments = 1  # 'rst' or nothing (which means literal text)
108 3
    final_argument_whitespace = False
109 3
    option_spec = {
110
        "execution-count": rst.directives.positive_int,
111
        "more-to-come": rst.directives.flag,
112
        "empty-lines-before": rst.directives.nonnegative_int,
113
        "empty-lines-after": rst.directives.nonnegative_int,
114
        "class": rst.directives.unchanged,
115
        "add-toggle": rst.directives.flag,
116
    }
117 3
    has_content = True
118

119 3
    def run(self):
120
        """This is called by the reST parser."""
121 3
        self.state.document["ipysphinx_include_css"] = True
122 3
        return _create_nbcell_nodes(self)
123

124

125 3
def _create_nbcell_nodes(directive):
126
    """Create nodes for an input or output notebook cell."""
127

128 3
    sphinx = import_sphinx()
129

130 3
    language = "none"
131 3
    prompt = ""
132 3
    fancy_output = False
133 3
    execution_count = directive.options.get("execution-count")
134 3
    config = directive.state.document.settings.env.config
135

136 3
    if isinstance(directive, NbInput):
137 3
        outer_classes = ["nbinput"]
138 3
        if "no-output" in directive.options:
139 3
            outer_classes.append("nblast")
140 3
        inner_classes = ["input_area"]
141 3
        if directive.arguments:
142 3
            language = directive.arguments[0]
143 3
        prompt_template = config.ipysphinx_input_prompt
144 3
        if not execution_count:
145 0
            execution_count = " "
146 3
    elif isinstance(directive, NbOutput):
147 3
        outer_classes = ["nboutput"]
148 3
        if "more-to-come" not in directive.options:
149 3
            outer_classes.append("nblast")
150 3
        inner_classes = ["output_area"]
151
        # 'class' can be 'stderr'
152 3
        inner_classes.append(directive.options.get("class", ""))
153 3
        prompt_template = config.ipysphinx_output_prompt
154 3
        if directive.arguments and directive.arguments[0] in ["rst", "ansi"]:
155 3
            fancy_output = True
156
    else:
157 0
        raise AssertionError("directive should be NbInput or NbOutput")
158

159 3
    outer_node = docutils.nodes.container(classes=outer_classes)
160

161
    # add prompts
162 3
    if config.ipysphinx_show_prompts and execution_count:
163 3
        prompt = prompt_template.format(count=execution_count)
164 3
        prompt_node = docutils.nodes.literal_block(
165
            prompt, prompt, language="none", classes=["prompt"]
166
        )
167 3
    elif config.ipysphinx_show_prompts:
168 3
        prompt = ""
169 3
        prompt_node = docutils.nodes.container(classes=["prompt", "empty"])
170 3
    if config.ipysphinx_show_prompts:
171
        # NB: Prompts are added manually in LaTeX output
172 3
        outer_node += sphinx.addnodes.only("", prompt_node, expr="html")
173

174 3
    if fancy_output:
175 3
        inner_node = docutils.nodes.container(classes=inner_classes)
176 3
        sphinx.util.nodes.nested_parse_with_titles(
177
            directive.state, directive.content, inner_node
178
        )
179 3
        outtype = directive.arguments[0]
180 3
        if outtype == "rst":
181 3
            outer_node += FancyOutputNode("", inner_node, prompt=prompt)
182 3
        elif outtype == "ansi":
183 3
            outer_node += inner_node
184
        else:
185 0
            raise AssertionError(
186
                "`.. nboutput:: type` should be 'rst' or 'ansi', "
187
                "not: {}".format(outtype)
188
            )
189
    else:
190 3
        text = "\n".join(directive.content.data)
191 3
        inner_node = docutils.nodes.literal_block(
192
            text, text, language=language, classes=inner_classes
193
        )
194 3
        codearea_node = CodeAreaNode("", inner_node, prompt=prompt)
195
        # create a literal text block (e.g. with the code-block directive),
196
        # that starts or ends with a blank line
197
        # (see http://stackoverflow.com/q/34050044/)
198 3
        for attr in "empty-lines-before", "empty-lines-after":
199 3
            value = directive.options.get(attr, 0)
200 3
            if value:
201 0
                codearea_node[attr] = value
202

203
        # add caption and label, see:
204 3
        if directive.options.get("caption", False):
205 0
            caption = directive.options.get("caption")
206 0
            wrapper = container_wrapper(directive, inner_node, caption, inner_classes)
207
            # add label
208 0
            directive.add_name(wrapper)
209 0
            outer_node += wrapper
210
        else:
211 3
            outer_node += codearea_node
212

213 3
    if isinstance(directive, NbInput) and (
214
        config.ipysphinx_input_toggle or "add-toggle" in directive.options
215
    ):
216 3
        directive.state.document["ipysphinx_include_js"] = True
217 3
        outer_node += sphinx.addnodes.only(
218
            "",
219
            docutils.nodes.container(classes=["toggle-nbinput", "empty"]),
220
            expr="html",
221
        )
222

223 3
    if isinstance(directive, NbOutput) and (
224
        config.ipysphinx_output_toggle or "add-toggle" in directive.options
225
    ):
226 3
        directive.state.document["ipysphinx_include_js"] = True
227 3
        outer_node += sphinx.addnodes.only(
228
            "",
229
            docutils.nodes.container(classes=["toggle-nboutput", "empty"]),
230
            expr="html",
231
        )
232

233 3
    return [outer_node]
234

235

236 3
def container_wrapper(directive, literal_node, caption, classes):
237
    """adapted from
238
    https://github.com/sphinx-doc/sphinx/blob/master/sphinx/directives/code.py
239
    """
240 0
    container_node = docutils.nodes.container(
241
        "", literal_block=True, classes=classes
242
    )  # ['literal-block-wrapper']
243 0
    parsed = docutils.nodes.Element()
244 0
    directive.state.nested_parse(
245
        StringList([caption], source=""), directive.content_offset, parsed
246
    )
247 0
    if isinstance(parsed[0], docutils.nodes.system_message):
248 0
        msg = "Invalid caption: %s" % parsed[0].astext()
249 0
        raise ValueError(msg)
250 0
    elif isinstance(parsed[0], docutils.nodes.Element):
251 0
        caption_node = docutils.nodes.caption(
252
            parsed[0].rawsource, "", *parsed[0].children
253
        )
254 0
        caption_node.source = literal_node.source
255 0
        caption_node.line = literal_node.line
256 0
        container_node += caption_node
257 0
        container_node += literal_node
258 0
        return container_node
259
    else:
260 0
        raise RuntimeError  # never reached

Read our documentation on viewing source code .

Loading