1# ----------------------------------------------------------------------------- 2# Copyright (c) 2012 - 2021, Anaconda, Inc., and Bokeh Contributors. 3# All rights reserved. 4# 5# The full license is in the file LICENSE.txt, distributed with this software. 6# ----------------------------------------------------------------------------- 7""" Make javascript code blocks also include a live link to codepen.io for instant experiementation. 8 9This directive takes a title to use for the codepen example: 10 11.. code-block:: rest 12 13 .. bokehjs-content:: 14 :title: Some Code 15 16 alert('this is called in the codepen'); 17 18This directive is identical to the standard ``code-block`` directive 19that Sphinx supplies, with the addition of one new option: 20 21title : string 22 A title for the codepen. 23js_file : string 24 location of javascript source file 25include_html: string 26 if present, this code block will be emitted as a complete HTML template with 27 js inside a script block 28disable_codepen: string 29 if present, this code block will not have a 'try on codepen' button. Currently 30 necessary when 'include_html' is turned on. 31 32Examples 33-------- 34 35The inline example code above produces the following output: 36 37.. bokehjs-content:: 38 :title: Some Code 39 40 alert('this is called in the codepen'); 41 42""" 43 44# ----------------------------------------------------------------------------- 45# Boilerplate 46# ----------------------------------------------------------------------------- 47import logging # isort:skip 48 49log = logging.getLogger(__name__) 50 51# ----------------------------------------------------------------------------- 52# Imports 53# ----------------------------------------------------------------------------- 54 55# Standard library imports 56from os.path import basename, join 57 58# External imports 59from docutils import nodes 60from docutils.parsers.rst.directives import unchanged 61from sphinx.directives.code import CodeBlock, container_wrapper, dedent_lines 62from sphinx.errors import SphinxError 63from sphinx.locale import __ 64from sphinx.util import logging, parselinenos 65from sphinx.util.nodes import set_source_info 66 67# Bokeh imports 68from .templates import BJS_CODEPEN_INIT, BJS_EPILOGUE, BJS_HTML, BJS_PROLOGUE 69from .util import get_sphinx_resources 70 71if False: 72 # For type annotation 73 # from directives.code.CodeBlock.run 74 from typing import Any, Dict, List, Tuple # NOQA 75 from sphinx.application import Sphinx # NOQA 76 from sphinx.config import Config # NOQA 77 78# ----------------------------------------------------------------------------- 79# Globals and constants 80# ----------------------------------------------------------------------------- 81 82__all__ = ( 83 "bokehjs_content", 84 "BokehJSContent", 85 "html_depart_bokehjs_content", 86 "html_visit_bokehjs_content", 87 "setup", 88) 89 90# ----------------------------------------------------------------------------- 91# General API 92# ----------------------------------------------------------------------------- 93 94# ----------------------------------------------------------------------------- 95# Dev API 96# ----------------------------------------------------------------------------- 97 98 99class bokehjs_content(nodes.General, nodes.Element): 100 pass 101 102 103class BokehJSContent(CodeBlock): 104 105 has_content = True 106 optional_arguments = 1 107 required_arguments = 0 108 109 option_spec = CodeBlock.option_spec 110 option_spec.update(title=unchanged) 111 option_spec.update(js_file=unchanged) 112 option_spec.update(include_html=unchanged) 113 option_spec.update(disable_codepen=unchanged) 114 115 def get_codeblock_node(self, code, language): 116 """this is copied from sphinx.directives.code.CodeBlock.run 117 118 it has been changed to accept code and language as an arguments instead 119 of reading from self 120 121 """ 122 123 document = self.state.document 124 location = self.state_machine.get_source_and_line(self.lineno) 125 126 linespec = self.options.get("emphasize-lines") 127 if linespec: 128 try: 129 nlines = len(code.split("\n")) 130 hl_lines = parselinenos(linespec, nlines) 131 if any(i >= nlines for i in hl_lines): 132 emph_lines = self.options["emphasize-lines"] 133 log.warning(__(f"line number spec is out of range(1-{nlines}): {emph_lines!r}"), location=location) 134 135 hl_lines = [x + 1 for x in hl_lines if x < nlines] 136 except ValueError as err: 137 return [document.reporter.warning(str(err), line=self.lineno)] 138 else: 139 hl_lines = None 140 141 if "dedent" in self.options: 142 location = self.state_machine.get_source_and_line(self.lineno) 143 lines = code.split("\n") 144 lines = dedent_lines(lines, self.options["dedent"], location=location) 145 code = "\n".join(lines) 146 147 literal = nodes.literal_block(code, code) 148 literal["language"] = language 149 literal["linenos"] = "linenos" in self.options or "lineno-start" in self.options 150 literal["classes"] += self.options.get("class", []) 151 extra_args = literal["highlight_args"] = {} 152 if hl_lines is not None: 153 extra_args["hl_lines"] = hl_lines 154 if "lineno-start" in self.options: 155 extra_args["linenostart"] = self.options["lineno-start"] 156 set_source_info(self, literal) 157 158 caption = self.options.get("caption") 159 if caption: 160 try: 161 literal = container_wrapper(self, literal, caption) 162 except ValueError as exc: 163 return [document.reporter.warning(str(exc), line=self.lineno)] 164 165 # literal will be note_implicit_target that is linked from caption and numref. 166 # when options['name'] is provided, it should be primary ID. 167 self.add_name(literal) 168 169 return [literal] 170 171 def get_js_source(self): 172 env = self.state.document.settings.env 173 js_file = self.options.get("js_file", False) 174 # js_file *or* js code content, but not both 175 if js_file and self.content: 176 raise SphinxError("bokehjs-content:: directive can't have both js_file and content") 177 178 if js_file: 179 log.debug(f"[bokehjs-content] handling external example in {env.docname!r}: {js_file}") 180 path = js_file 181 if not js_file.startswith("/"): 182 path = join(env.app.srcdir, path) 183 js_source = open(path).read() 184 else: 185 log.debug(f"[bokehjs-content] handling inline example in {env.docname!r}") 186 js_source = "\n".join(self.content) 187 188 return js_source 189 190 def get_code_language(self): 191 """ 192 This is largely copied from bokeh.sphinxext.bokeh_plot.run 193 """ 194 js_source = self.get_js_source() 195 if self.options.get("include_html", False): 196 resources = get_sphinx_resources(include_bokehjs_api=True) 197 html_source = BJS_HTML.render(css_files=resources.css_files, js_files=resources.js_files, hashes=resources.hashes, bjs_script=js_source) 198 return [html_source, "html"] 199 else: 200 return [js_source, "javascript"] 201 202 def run(self): 203 env = self.state.document.settings.env 204 205 rst_source = self.state_machine.node.document["source"] 206 rst_filename = basename(rst_source) 207 208 serial_no = env.new_serialno("ccb") 209 target_id = f"{rst_filename}.ccb-{serial_no}" 210 target_id = target_id.replace(".", "-") 211 target_node = nodes.target("", "", ids=[target_id]) 212 213 node = bokehjs_content() 214 node["target_id"] = target_id 215 node["title"] = self.options.get("title", "bokehjs example") 216 node["include_bjs_header"] = False 217 node["disable_codepen"] = self.options.get("disable_codepen", False) 218 node["js_source"] = self.get_js_source() 219 220 source_doc = self.state_machine.node.document 221 if not hasattr(source_doc, "bjs_seen"): 222 # we only want to inject the CODEPEN_INIT on one 223 # bokehjs-content block per page, here we check to see if 224 # bjs_seen exists, if not set it to true, and set 225 # node['include_bjs_header'] to true. This way the 226 # CODEPEN_INIT is only injected once per document (html 227 # page) 228 source_doc.bjs_seen = True 229 node["include_bjs_header"] = True 230 code_content, language = self.get_code_language() 231 cb = self.get_codeblock_node(code_content, language) 232 node.setup_child(cb[0]) 233 node.children.append(cb[0]) 234 return [target_node, node] 235 236 237def html_visit_bokehjs_content(self, node): 238 if node["include_bjs_header"]: 239 # we only want to inject the CODEPEN_INIT on one 240 # bokehjs-content block per page 241 resources = get_sphinx_resources(include_bokehjs_api=True) 242 self.body.append(BJS_CODEPEN_INIT.render(css_files=resources.css_files, js_files=resources.js_files)) 243 244 self.body.append( 245 BJS_PROLOGUE.render( 246 id=node["target_id"], 247 title=node["title"], 248 ) 249 ) 250 251 252def html_depart_bokehjs_content(self, node): 253 self.body.append(BJS_EPILOGUE.render(title=node["title"], enable_codepen=not node["disable_codepen"], js_source=node["js_source"])) 254 255 256def setup(app): 257 """ Required Sphinx extension setup function. """ 258 app.add_node(bokehjs_content, html=(html_visit_bokehjs_content, html_depart_bokehjs_content)) 259 app.add_directive("bokehjs-content", BokehJSContent) 260 261 262# ----------------------------------------------------------------------------- 263# Private API 264# ----------------------------------------------------------------------------- 265 266# ----------------------------------------------------------------------------- 267# Code 268# ----------------------------------------------------------------------------- 269