source/ext/extref.py (202 lines of code) (raw):

# # Copyright (c) 2021. JetBrains s.r.o. # Use of this source code is governed by the MIT license that can be found in the LICENSE file. # """A sphinx extension for inserting references in two formats - image or text. The ``extref`` directive do the all job. For example: .. code-block:: rst .. extref:: name :type: image :url: https://example.com :image: default :width: 400 It approximately translates to the following html: .. code-block:: html <a class="reference external image-reference" href="https://example.com"> <img alt="default-text" title="default-text" src="default-image" width="400"> </a> Here ``alt``, ``title`` and ``src`` values comes from the JSON configuration file, and they implicitly specified by the ``name`` value. The first one is given by default, the second is defined through the ``:image:`` option. Configuration ------------- In your configuration file add ``extref`` to your extensions list, e.g.: .. code-block:: python extensions = [ ... 'extref', ... ] The extension provides the following configuration values: - ``extref_conf`` : str Path to the JSON configuration file with parameters variety for each reference. The structure is the following: {"name1": options, "name2": options, ...}. Here for each named reference there is a standard bunch of options that is described below in the "JSON Options" section. The name of the reference connects directive with this options. - ``extref_images_dir`` : str Path to the output directory with images inside the documentation site. - ``extref_logo_images`` : dict For each ``:ref:`` value that is used to be a logo it should be specified the path to the corresponding logo image. - ``extref_default_type`` : {'image', 'logo', 'text'} Default ``:type:`` value if it is not specified. - ``extref_default_ref`` : str Default ``:ref:`` value if it is not specified. - ``extref_default_image`` : str Default ``:image:`` value if it is not specified. - ``extref_class`` : str Additional custom class for the link tag. JSON Options ------------ - ``ref`` : Dictionary of pairs ``"ref_type": "url"``. ``:ref: ref_type`` in the reStructuredText means ``href="url"`` in html. By default, if ``:ref:`` option and ``extref_default_ref`` config value are not specified, used the first reference among all. - ``image`` : Dictionary of pairs ``"img_type": "path"``. ``:image: img_type`` in the reStructuredText means ``src="path"`` in html, if reference type is image. By default, if ``:image:`` option and ``extref_default_image`` config value are not specified, used the first image among all. - ``title`` : String with default title value, if reference type is image. - ``text`` : String with default text value. In html it means ``<a ...>text</a>`` when reference type is text and ``alt="text"`` in other cases. Used when ``:text:`` option is not specified. Directive Options ----------------- - ``type`` : Should be the one of the three types of references: text, image, logo. - ``ref`` : Reference type from the JSON configuration file. - ``url`` : Explicit url for the reference, that used instead of the ``:ref:`` option. - ``image`` : Image type from the JSON configuration file. - ``text`` : Explicit text for the reference. - ``title`` : Explicit title for the reference. - ``width`` : Width of the image if the ``:type:`` value is image or logo. - ``height`` : Height of the image if the ``:type:`` value is image or logo. Examples -------- Suppose that ``extref_logo_images`` configuration value is the following: .. code-block:: python extref_logo_images = { ... 'kaggle': "_static/images/kaggle.svg", ... } Suppose that JSON configuration file is the following: .. code-block:: javascript { ... "example1": { "ref": { "nbviewer": "https://nbviewer.jupyter.org/github/Example/example/blob/master/example/example.ipynb", "kaggle": "https://www.kaggle.com/example/example" }, "image": { "default": "_static/images/example1.png" }, "text": "My Example" } ... } Then .. code-block:: rst .. extref:: example1 gives the following html: .. code-block:: html <a class="reference external image-reference" href="https://nbviewer.jupyter.org/github/Example/example/blob/master/example/example.ipynb"> <img alt="My Example" title="My Example" src="_static/images/example1.png"> </a> The code .. code-block:: rst .. extref:: example1 :type: logo :ref: kaggle gives the following html: .. code-block:: html <a class="reference external image-reference" href="https://www.kaggle.com/example/example"> <img alt="My Example" title="My Example" src="_static/images/kaggle.svg"> </a> The code .. code-block:: rst .. extref:: example1 :type: text :url: https://example.com gives the following html: .. code-block:: html <a class="reference external" href="https://example.com"> My Example </a> """ import os import json import shutil from argparse import ArgumentParser from urllib.parse import urlparse from docutils import nodes from docutils.parsers.rst import Directive, directives REF_TYPES = ('image', 'logo', 'text') IMAGES_DIR = "_extref_images" LOGO_DIR = "logo" AVAILABLE_UTILS = ["check_using_notebook_names"] class ExtRefDirective(Directive): has_content = True option_spec = { 'type': lambda t: directives.choice(t, REF_TYPES), 'ref': directives.unchanged, 'url': directives.uri, 'image': directives.unchanged, 'title': directives.unchanged, 'text': directives.unchanged, 'width': directives.unchanged, 'height': directives.unchanged, } def run(self): return [nodes.raw( "", '<a class="{0}" href="{1}">{2}</a>'.format(self._class(), self._href(), self._content()), format='html' )] def _env(self): return self.state.document.settings.env def _conf(self): return self._env().config['extref_conf'][self.content[0]] def _type(self): if 'type' in self.options.keys(): return self.options['type'] return self._env().config['extref_default_type'] def _href(self): if 'url' in self.options.keys(): return self.options['url'] return self._ref() def _ref(self): return self._conf()['ref'][self._ref_type()] def _ref_type(self): if 'ref' in self.options.keys(): return self.options['ref'] if 'extref_default_ref' in self._env().config and \ self._env().config['extref_default_ref'] in self._conf()['ref']: return self._env().config['extref_default_ref'] return list(self._conf()['ref'])[0] def _class(self): class_text = "reference {0}".format(self._url_type()) if self._type() == 'image': class_text += " image-reference preview" if self._type() == 'logo': class_text += " image-reference logo" if self._custom_class() is not None: class_text += " {0}".format(self._custom_class()) return class_text def _custom_class(self): if 'extref_class' in self._env().config: return self._env().config['extref_class'] else: return None def _url_type(self): return "external" if urlparse(self._href()).netloc else "internal" def _content(self): if self._type() == 'image': return self._image() if self._type() == 'logo': return self._logo() return self._text() def _image(self): image_src_path = self._image_src_path() image_src_fullpath = os.path.join(self._env().app.srcdir, image_src_path) image_doc_path = os.path.join(self._env().config['extref_images_dir'], os.path.basename(image_src_path)) image_doc_fullpath = os.path.join(self._env().app.outdir, image_doc_path) if not os.path.isfile(image_doc_fullpath): shutil.copy(image_src_fullpath, image_doc_fullpath) return self._image_tag(image_doc_path) def _image_src_path(self): conf_image = self._conf()['image'] if 'image' in self.options.keys(): return conf_image[self.options['image']] if 'extref_default_image' in self._env().config and \ self._env().config['extref_default_image'] in conf_image: return conf_image[self._env().config['extref_default_image']] return conf_image[list(conf_image)[0]] def _alt(self): return self._text() def _title(self): if 'title' in self.options.keys(): return self.options['title'] if 'title' in self._conf(): return self._conf()['title'] if 'text' in self.options.keys(): return self.options['text'] if 'text' in self._conf(): return self._conf()['text'] return "" def _logo(self): logo_fullpath = next((path for name, path in self._env().config['extref_logo_images'].items() \ if name == self._ref_type()), None) if not logo_fullpath: raise ValueError("There is no appropriate logo for the reference {0}".format(self._ref_type())) logo_path = logo_fullpath.replace(str(self._env().app.outdir), '')[1:] return self._image_tag(logo_path) def _image_tag(self, image_path): doc_dir = os.path.dirname(self.state.document.attributes['source'].replace(str(self._env().app.srcdir), ''))[1:] return '<img alt="{0}" title="{1}" src="{2}" style="{3}{4}"/>'.format( self._alt(), self._title(), os.path.relpath(image_path, doc_dir), self._width_tag_option(), self._height_tag_option() ) def _width_tag_option(self): if 'width' in self.options.keys(): width = self.options['width'] if width[-1] in "0123456789": width += "px" return "width: {0};".format(width) return "" def _height_tag_option(self): if 'height' in self.options.keys(): height = self.options['height'] if height[-1] in "0123456789": height += "px" return "height: {0};".format(height) return "" def _text(self): if 'text' in self.options.keys(): return self.options['text'] if 'text' in self._conf(): return self._conf()['text'] return self._href() def config_inited_handler(app, config): if config.extref_default_type and not config.extref_default_type in REF_TYPES: raise ValueError("Parameter extref_default_type should be in {0}".format(REF_TYPES)) prepare_conf_json(app, config) prepare_images(app, config) def prepare_conf_json(app, config): if not config.extref_conf: raise ValueError("Parameter extref_conf could not be empty") with open(os.path.join(app.srcdir, config.extref_conf)) as f: try: config.extref_conf = json.loads(f.read()) except json.decoder.JSONDecodeError as e: msg = "Decode error in {0}. {1}".format(config.extref_conf, e.msg) raise json.decoder.JSONDecodeError(msg, e.doc, e.pos) from e def prepare_images(app, config): extref_images_dir = os.path.join(app.outdir, config.extref_images_dir) if not os.path.isdir(extref_images_dir): os.makedirs(extref_images_dir) prepare_logo(app, config) def prepare_logo(app, config): extref_logo_dir = os.path.join(app.outdir, config.extref_images_dir, LOGO_DIR) if not os.path.isdir(extref_logo_dir): os.makedirs(extref_logo_dir) if config.extref_logo_images: extref_logo_images = {} for logo_name, logo_src_path in config.extref_logo_images.items(): logo_src_fullpath = os.path.join(app.srcdir, logo_src_path) logo_doc_fullpath = os.path.join(extref_logo_dir, "{0}{1}".format(logo_name, os.path.splitext(logo_src_path)[1])) extref_logo_images[logo_name] = logo_doc_fullpath if not os.path.isfile(logo_doc_fullpath): shutil.copy(logo_src_fullpath, logo_doc_fullpath) config.extref_logo_images = extref_logo_images def ext_utils(util_name, conf_path, src_dir): globals()[util_name](conf_path, src_dir) def check_using_notebook_names(conf_path, src_dir): """ Calculate count of using for each notebook name from JSON configuration file. """ import json with open(conf_path, 'r') as f: nb_names = json.load(f).keys() nb_counts = {nb_name: 0 for nb_name in nb_names} for root, dirs, files in os.walk(src_dir): for file in files: if os.path.splitext(file)[1] != ".rst": continue with open(os.path.join(root, file), 'r', errors='ignore') as f: data = f.read() for nb_name in nb_names: nb_counts[nb_name] += data.count(" extref:: {0}".format(nb_name)) for nb_name, count in sorted(nb_counts.items(), key=lambda p: p[1], reverse=True): print(nb_name, count) def setup(app): app.add_config_value('extref_conf', None, 'html') app.add_config_value('extref_logo_images', None, 'html') app.add_config_value('extref_images_dir', IMAGES_DIR, 'html') app.add_config_value('extref_default_type', REF_TYPES[0], 'html') app.add_config_value('extref_default_ref', None, 'html') app.add_config_value('extref_default_image', None, 'html') app.add_config_value('extref_class', None, 'html') app.add_directive('extref', ExtRefDirective) app.connect('config-inited', config_inited_handler) return { 'version': '0.3', } if __name__ == '__main__': parser = ArgumentParser() parser.add_argument('-c', '--conf_path', required=True, metavar='CONF_PATH', help="Path to the JSON configuration file.") parser.add_argument('-s', '--src_dir', required=True, metavar='SRC_DIR', help="Path to the source directory.") parser.add_argument('-u', '--util_name', required=True, choices=AVAILABLE_UTILS, metavar='UTIL_NAME', help="Util name to use") args = parser.parse_args() ext_utils(args.util_name, args.conf_path, args.src_dir)