Commit 5785602d authored by Ingo Heimbach's avatar Ingo Heimbach

Restructered the project and added a `docutils-extended` Python package

parent beeb7a7e
......@@ -6,13 +6,28 @@ This repository is a collection of styles and patched scripts for the Python doc
documents from reStructuredText files.
## Files
## Python package
This repository contains a Python package `docutils_extended` that can be installed with `pip`:
```bash
pip install git+https://iffgit.fz-juelich.de/doc-utils/doc-utils
```
- `rst2latex-listings.py`: Patched version of `rst2latex` using the Listings package consistently for code
highlighting
After installing you can use the extended rst writers `rst2latex-extended` and `rst2html-extended` that are compatible
with the standard versions but offer additional features:
- `rst2html_githublike_fzj.css`: Stylesheet for `rst2html` with fzj blue for strong emphasized text; Using
`--syntax-highlight=short` option with `rst2html` is recommended but long class names are also included in the this
CSS file
- Code blocks are formatted with the `listings` package (LaTeX only)
- Images can be embedded into the generated document (`embedded-image` directive, HTML only; for LaTeX this is already
the default behavior)
- Support for TikZ images by `tikz` and `tikz-figure` directives. TikZ code can be loaded from an external file or
written as embedded source.
## Files
- `rst2latex_fzj.sty`: Style with fzj colors and a custom title page
- `styles/rst2html_githublike_fzj.css`: Stylesheet for `rst2html` with fzj blue for strong emphasized text; Using
`--syntax-highlight=short` option with `rst2html` is recommended but long class names are also included in the this
CSS file
- `styles/rst2latex_fzj.sty`: Style with fzj colors and a custom title page
- `utils/Makefile`: Example Makefile for generating PDFs and HTML pages from Markdown and reStructuredText documents
from .writers import rst2html_extended_main, rst2latex_extended_main
__all__ = ('rst2html_extended_main', 'rst2latex_extended_main')
__version_info__ = (0, 1, 0)
__version__ = '.'.join(map(str, __version_info__))
from .code_directive import CodeBlock
from .embedded_image_directive import EmbeddedImage
from .tikz_directive import Tikz, TikzFigure, set_output_mode
__all__ = ("CodeBlock", "EmbeddedImage", "Tikz", "TikzFigure", "set_output_mode")
from docutils import nodes
from docutils.parsers.rst import Directive
class CodeBlock(Directive):
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
option_spec = {}
has_content = True
rstlang_to_listingslang = {"pytb": "{}", "text": "{}"}
def run(self):
language = self.rstlang_to_listingslang.get(self.arguments[0], self.arguments[0])
content = u"\n".join(self.content)
latex = u"\\begin{{lstlisting}}[language={}]\n{}\n\\end{{lstlisting}}".format(language, content)
return [nodes.raw("", latex, format="latex")]
import base64
import sys
from PIL import Image
from docutils import nodes
from docutils.parsers.rst import Directive
from docutils.parsers.rst import directives, states
from docutils.nodes import fully_normalize_name, whitespace_normalize_name
from docutils.parsers.rst.roles import set_classes
def convert_png_to_html_base64(png_filepath):
with open(png_filepath.encode(sys.getfilesystemencoding()), "rb") as image_file:
encoded_string = base64.b64encode(image_file.read())
return "data:image/png;base64," + encoded_string
class EmbeddedImage(Directive):
align_values = ("left", "center", "right")
required_arguments = 1
optional_arguments = 0
option_spec = {
"alt": directives.unchanged,
"height": directives.length_or_unitless,
"width": directives.length_or_percentage_or_unitless,
"scale": directives.percentage,
"align": lambda argument: directives.choice(argument, EmbeddedImage.align_values),
"name": directives.unchanged,
"target": directives.unchanged_required,
"class": directives.class_option,
}
has_content = True
def run(self):
image_filepath = self.arguments[0]
if "align" in self.options and self.options["align"] not in self.align_values:
raise self.error(
'Error in "%s" directive: "%s" is not a valid value for '
'the "align" option. Valid values for "align" are: "%s".'
% (self.name, self.options["align"], '", "'.join(self.align_values))
)
messages = []
reference = directives.uri(convert_png_to_html_base64(image_filepath))
self.options["uri"] = reference
if not any(attr in self.options for attr in ("height", "width", "scale")):
try:
image = Image.open(image_filepath.encode(sys.getfilesystemencoding()))
except (IOError, UnicodeEncodeError):
pass # TODO: warn?
else:
self.options["width"] = "{}px".format(image.size[0])
reference_node = None
if "target" in self.options:
block = states.escape2null(self.options["target"]).splitlines()
block = [line for line in block]
target_type, data = self.state.parse_target(block, self.block_text, self.lineno)
if target_type == "refuri":
reference_node = nodes.reference(refuri=data)
elif target_type == "refname":
reference_node = nodes.reference(
refname=fully_normalize_name(data), name=whitespace_normalize_name(data)
)
reference_node.indirect_reference_name = data
self.state.document.note_refname(reference_node)
else: # malformed target
messages.append(data) # data is a system message
del self.options["target"]
set_classes(self.options)
image_node = nodes.image(self.block_text, **self.options)
self.add_name(image_node)
if reference_node:
reference_node += image_node
return messages + [reference_node]
else:
return messages + [image_node]
# -*- coding: utf-8 -*-
import base64
import codecs
import jinja2
import os
import tempfile
import shutil
import subprocess
import sys
from PIL import Image
from docutils import nodes
from docutils.parsers.rst import Directive
from docutils.parsers.rst import directives, states
from docutils.nodes import fully_normalize_name, whitespace_normalize_name
from docutils.parsers.rst.roles import set_classes
TEMPLATE_LATEX = u"""
\\documentclass{%- if document_options -%}[{{ document_options }}]{%- endif -%}{standalone}
{% if not do_not_use_default_packages -%}
\\usepackage[utf8]{inputenc}
\\usepackage[T1]{fontenc}
\\usepackage{lmodern}
\\usepackage{babel}
\\usepackage[usenames, dvipsnames, svgnames, table]{xcolor}
\\usepackage[pdftex]{graphicx}
{% endif -%}
{% for extra_package in extra_packages -%}
\\usepackage{{ extra_package }}
{% endfor -%}
\\usepackage{tikz}
{% for tikz_lib in tikz_libs -%}
\\usetikzlibrary{ {{ tikz_lib }} }
{% endfor -%}
\\begin{document}
\\begin{tikzpicture}{%- if tikzpicture_options -%}[{{ tikzpicture_options }}]{%- endif %}
{{ tikz_code }}
\\end{tikzpicture}
\\end{document}
""".strip()
TMP_LATEX_FILENAME = "tikz_picture.tex"
TMP_PDF_FILENAME = "tikz_picture.pdf"
OUT_PDF_FILENAME = "_tikz_rendered{:04d}.pdf"
TMP_PNG_FILENAME = "tikz_picture.png"
OUT_PNG_FILENAME = "_tikz_rendered{:04d}.png"
VALID_OUTPUT_MODES = ("latex", "html")
_output_mode = "latex"
class InvalidOutputModeError(Exception):
pass
def set_output_mode(output_mode):
global _output_mode
if output_mode in VALID_OUTPUT_MODES:
_output_mode = output_mode
else:
raise InvalidOutputModeError("{} is not valid output mode".format(output_mode))
class TemporaryDirectory(object):
def __init__(self):
self.tmp_dir = tempfile.mkdtemp()
def __enter__(self):
return self.tmp_dir
def __exit__(self, exception_type, value, traceback):
shutil.rmtree(self.tmp_dir)
self.tmp_dir = None
def render_tikz(tikz_code, options, output_png=False):
self = render_tikz
if not hasattr(self, "image_number"):
self.image_number = 1
if "do_not_use_default_packages" in options:
options["do_not_use_default_packages"] = True
if "extra_packages" in options:
options["extra_packages"] = [package.strip() for package in options["extra_packages"].split(";")]
if "tikz_libs" in options:
options["tikz_libs"] = [tikz_lib.strip() for tikz_lib in options["tikz_libs"].split(",")]
template = jinja2.Template(TEMPLATE_LATEX)
latex_output = template.render(tikz_code=tikz_code, **options)
out_pdf_filename = OUT_PDF_FILENAME.format(self.image_number)
out_png_filename = OUT_PNG_FILENAME.format(self.image_number)
with TemporaryDirectory() as tmp_dir:
tmp_latex_path = os.path.join(tmp_dir, TMP_LATEX_FILENAME)
tmp_pdf_path = os.path.join(tmp_dir, TMP_PDF_FILENAME)
tmp_png_path = os.path.join(tmp_dir, TMP_PNG_FILENAME)
with codecs.open(tmp_latex_path, "w", "utf-8") as f:
f.write(latex_output)
with open(os.devnull, "w") as devnull:
try:
subprocess.check_call(
["latexmk", "-pdf", "-halt-on-error", TMP_LATEX_FILENAME],
cwd=tmp_dir,
stdout=devnull,
stderr=devnull,
)
except subprocess.CalledProcessError:
with codecs.open(
os.path.join(tmp_dir, "{}.log".format(os.path.splitext(TMP_LATEX_FILENAME)[0])), "r", "utf-8"
) as f:
sys.stderr.write(f.read())
raise
if output_png:
subprocess.check_call(
["mudraw", "-r", "360", "-o", TMP_PNG_FILENAME, TMP_PDF_FILENAME],
cwd=tmp_dir,
stdout=devnull,
stderr=devnull,
)
if output_png:
shutil.move(tmp_png_path, out_png_filename)
else:
shutil.move(tmp_pdf_path, out_pdf_filename)
return out_png_filename if output_png else out_pdf_filename
def convert_png_to_html_base64(png_filepath):
with open(png_filepath.encode(sys.getfilesystemencoding()), "rb") as image_file:
encoded_string = base64.b64encode(image_file.read())
return "data:image/png;base64," + encoded_string
class Tikz(Directive):
align_values = ("left", "center", "right")
required_arguments = 0
optional_arguments = 1
option_spec = {
"alt": directives.unchanged,
"height": directives.length_or_unitless,
"width": directives.length_or_percentage_or_unitless,
"scale": directives.percentage,
"align": lambda argument: directives.choice(argument, Tikz.align_values),
"name": directives.unchanged,
"target": directives.unchanged_required,
"class": directives.class_option,
"document_options": directives.unchanged,
"do_not_use_default_packages": directives.flag,
"extra_packages": directives.unchanged,
"tikz_libs": directives.unchanged,
"tikzpicture_options": directives.unchanged,
}
has_content = True
def run(self):
if len(self.arguments) > 0:
read_from_file = True
tikz_source_filename = self.arguments[0]
else:
read_from_file = False
if "align" in self.options and self.options["align"] not in self.align_values:
raise self.error(
'Error in "%s" directive: "%s" is not a valid value for '
'the "align" option. Valid values for "align" are: "%s".'
% (self.name, self.options["align"], '", "'.join(self.align_values))
)
if "document_options" not in self.options:
self.options["document_options"] = "12pt"
is_output_mode_html = _output_mode == "html"
messages = []
if read_from_file:
with codecs.open(tikz_source_filename, "r", "utf-8") as f:
content = f.read()
else:
content = self.content
rendered_tikz_filename = render_tikz("\n".join(content), self.options, output_png=is_output_mode_html)
if is_output_mode_html:
reference = directives.uri(convert_png_to_html_base64(rendered_tikz_filename))
else:
reference = directives.uri(rendered_tikz_filename)
self.options["uri"] = reference
if is_output_mode_html and not any(attr in self.options for attr in ("height", "width", "scale")):
try:
image = Image.open(rendered_tikz_filename.encode(sys.getfilesystemencoding()))
except (IOError, UnicodeEncodeError):
pass # TODO: warn?
else:
self.options["width"] = "{}pt".format(image.size[0] / 5)
reference_node = None
if "target" in self.options:
block = states.escape2null(self.options["target"]).splitlines()
block = [line for line in block]
target_type, data = self.state.parse_target(block, self.block_text, self.lineno)
if target_type == "refuri":
reference_node = nodes.reference(refuri=data)
elif target_type == "refname":
reference_node = nodes.reference(
refname=fully_normalize_name(data), name=whitespace_normalize_name(data)
)
reference_node.indirect_reference_name = data
self.state.document.note_refname(reference_node)
else: # malformed target
messages.append(data) # data is a system message
del self.options["target"]
set_classes(self.options)
image_node = nodes.image(self.block_text, **self.options)
self.add_name(image_node)
if reference_node:
reference_node += image_node
return messages + [reference_node]
else:
return messages + [image_node]
class TikzFigure(Tikz):
option_spec = Tikz.option_spec.copy()
option_spec["figwidth"] = lambda argument: directives.length_or_percentage_or_unitless(argument, "px")
option_spec["figclass"] = directives.class_option
option_spec["align"] = lambda argument: directives.choice(argument, TikzFigure.align_values)
option_spec["caption"] = directives.unchanged
has_content = True
def run(self):
figwidth = self.options.pop("figwidth", None)
figclasses = self.options.pop("figclass", None)
align = self.options.pop("align", None)
caption = self.options.pop("caption", None)
(image_node,) = Tikz.run(self)
if isinstance(image_node, nodes.system_message):
return [image_node]
figure_node = nodes.figure("", image_node)
if figwidth is not None:
figure_node["width"] = figwidth
if figclasses:
figure_node["classes"] += figclasses
if align:
figure_node["align"] = align
if caption:
caption = nodes.caption("", text=caption)
figure_node += caption
return [figure_node]
from .rst2html_extended import main as rst2html_extended_main
from .rst2latex_extended import main as rst2latex_extended_main
__all__ = ('rst2html_extended_main', 'rst2latex_extended_main')
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# $Id: rst2html.py 4564 2006-05-21 20:44:42Z wiemann $
# Author: David Goodger <goodger@python.org>
# Copyright: This module has been placed in the public domain.
"""
A customized front end to the Docutils Publisher, producing HTML with support for embedded images and Tikz figures.
"""
try:
import locale
locale.setlocale(locale.LC_ALL, "")
except:
pass
from docutils.core import publish_cmdline, default_description
from docutils.parsers.rst import directives
from ..directives import EmbeddedImage, Tikz, TikzFigure, set_output_mode
description = "Generates (X)HTML documents from standalone reStructuredText " "sources. " + default_description
def main():
set_output_mode("html")
directives.register_directive("embedded-image", EmbeddedImage)
directives.register_directive("tikz", Tikz)
directives.register_directive("tikz-figure", TikzFigure)
publish_cmdline(writer_name="html", description=description)
if __name__ == '__main__':
main()
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# $Id: rst2latex.py 5905 2009-04-16 12:04:49Z milde $
# Author: David Goodger <goodger@python.org>
# Copyright: This module has been placed in the public domain.
"""
A customized front end to the Docutils Publisher, producing LaTeX with valid codeblocks using the listings package and
support for Tikz images.
"""
try:
import locale
locale.setlocale(locale.LC_ALL, "")
except:
pass
from docutils.core import publish_cmdline
from docutils.parsers.rst import directives
from ..directives import CodeBlock, Tikz, TikzFigure, set_output_mode
description = (
"Generates LaTeX documents from standalone reStructuredText "
"sources. "
"Reads from <source> (default is stdin) and writes to "
"<destination> (default is stdout). See "
"<http://docutils.sourceforge.net/docs/user/latex.html> for "
"the full reference."
)
def main():
set_output_mode("latex")
for directive_name in ("code", "code-block"):
directives.register_directive(directive_name, CodeBlock)
directives.register_directive("tikz", Tikz)
directives.register_directive("tikz-figure", TikzFigure)
publish_cmdline(writer_name="latex", description=description)
if __name__ == '__main__':
main()
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
"""
A customized front end to the Docutils Publisher, producing LaTeX with valid codeblocks using the listings package.
"""
try:
import locale
locale.setlocale(locale.LC_ALL, '')
except:
pass
from docutils.core import publish_cmdline
from docutils.parsers.rst import directives, Directive
from docutils import nodes
class CodeBlock(Directive):
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
option_spec = {}
has_content = True
rstlang_to_listingslang = {
'pytb': '{}',
'text': '{}'
}
def run(self):
language = self.rstlang_to_listingslang.get(self.arguments[0], self.arguments[0])
content = '\n'.join(self.content)
latex = '\\begin{{lstlisting}}[language={}]\n{}\n\\end{{lstlisting}}'.format(language, content)
return [nodes.raw('', latex, format='latex')]
description = ('Generates LaTeX documents from standalone reStructuredText '
'sources. '
'Reads from <source> (default is stdin) and writes to '
'<destination> (default is stdout). See '
'<http://docutils.sourceforge.net/docs/user/latex.html> for '
'the full reference.')
for directive_name in ('code', 'code-block'):
directives.register_directive(directive_name, CodeBlock)
publish_cmdline(writer_name='latex', description=description)
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import codecs
import os
import runpy
import subprocess
from setuptools import setup, find_packages
def get_version_from_pyfile(version_file="docutils_extended/_version.py"):
file_globals = runpy.run_path(version_file)
return file_globals["__version__"]
def get_install_requires_from_requirements(requirements_filename="requirements.txt"):
try:
with codecs.open(requirements_filename, "r", "utf-8") as requirements_file:
requirements = requirements_file.readlines()
except OSError:
import logging
logging.warning("Could not read the requirements file.")
return requirements
def get_long_description_from_readme(readme_filename="README.md"):
rst_filename = "{}.rst".format(os.path.splitext(os.path.basename(readme_filename))[0])
created_tmp_rst = False
if not os.path.isfile(rst_filename):
try:
subprocess.check_call(["pandoc", readme_filename, "-t", "rst", "-o", rst_filename])
created_tmp_rst = True
except (OSError, subprocess.CalledProcessError):
import logging
logging.warning("Could not convert the readme file to rst.")
long_description = None
if os.path.isfile(rst_filename):
with codecs.open(rst_filename, "r", "utf-8") as readme_file:
long_description = readme_file.read()
if created_tmp_rst:
os.remove(rst_filename)
return long_description
version = get_version_from_pyfile()
long_description = get_long_description_from_readme()
install_requires = get_install_requires_from_requirements()
setup(
name="docutils-extended",
version=version,
packages=find_packages(),
python_requires=">=2.7, <3",
install_requires=install_requires,
entry_points={
"console_scripts": [
"rst2latex-extended = docutils_extended:rst2latex_extended_main",
"rst2html-extended = docutils_extended:rst2html_extended_main",
]
},
author="Ingo Heimbach",
author_email="i.heimbach@fz-juelich.de",
description="HTML and LaTeX rst writer with support for extra directives",
long_description=long_description,
license="MIT",
url="https://iffgit.fz-juelich.de/doc-utils/doc-utils",
keywords=["docutils", "rst", "reStructuredText", "TikZ"],
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS",
"Operating System :: Unix",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2 :: Only",
"Topic :: Documentation",
"Topic :: Software Development :: Documentation",
],
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment