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)