From 6a289d2a5633f7cb77b9a4330bc5618d75b98a64 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 14 Oct 2018 18:26:25 +0200 Subject: [PATCH] doxygen: implement support for \dot and \dotfile. Ugh, this blew up again. Three days to get here. --- doc/doxygen.rst | 19 ++++ doxygen/dox2html5.py | 49 +++++++- doxygen/test/contents_custom/Doxyfile | 4 + doxygen/test/contents_custom/ab.dot | 4 + doxygen/test/contents_custom/dot-236.html | 67 +++++++++++ doxygen/test/contents_custom/dot-238.html | 67 +++++++++++ doxygen/test/contents_custom/dot.html | 67 +++++++++++ doxygen/test/contents_custom/input.dox | 20 ++++ doxygen/test/contents_dot/Doxyfile | 17 +++ doxygen/test/contents_dot/ab.dot | 4 + doxygen/test/contents_dot/colors.dot | 6 + doxygen/test/contents_dot/index-236.html | 132 +++++++++++++++++++++ doxygen/test/contents_dot/index-238.html | 133 ++++++++++++++++++++++ doxygen/test/contents_dot/index.html | 132 +++++++++++++++++++++ doxygen/test/contents_dot/input.dox | 36 ++++++ doxygen/test/contents_dot/warnings.html | 35 ++++++ doxygen/test/test_contents.py | 50 ++++++++ doxygen/test/test_doxyfile.py | 2 + pelican-plugins/dot2svg.py | 19 ++-- 19 files changed, 853 insertions(+), 10 deletions(-) create mode 100644 doxygen/test/contents_custom/ab.dot create mode 100644 doxygen/test/contents_custom/dot-236.html create mode 100644 doxygen/test/contents_custom/dot-238.html create mode 100644 doxygen/test/contents_custom/dot.html create mode 100644 doxygen/test/contents_dot/Doxyfile create mode 100644 doxygen/test/contents_dot/ab.dot create mode 100644 doxygen/test/contents_dot/colors.dot create mode 100644 doxygen/test/contents_dot/index-236.html create mode 100644 doxygen/test/contents_dot/index-238.html create mode 100644 doxygen/test/contents_dot/index.html create mode 100644 doxygen/test/contents_dot/input.dox create mode 100644 doxygen/test/contents_dot/warnings.html diff --git a/doc/doxygen.rst b/doc/doxygen.rst index da3efc4f..121e894d 100644 --- a/doc/doxygen.rst +++ b/doc/doxygen.rst @@ -147,6 +147,9 @@ If you see something unexpected or not see something expected, check the - Math rendered as embedded SVG instead of raster images / MathJax. The supported feature set is equivalent to the `m.math Pelican plugin <{filename}/plugins/math-and-code.rst#math>`_, see its documentation for more information. +- Graphviz / Dot diagrams rendered as embedded SVG. The supported feature set + is equivalent to the `m.dot Pelican plugin <{filename}/plugins/plots-and-graphs.rst#graphs>`_, + see its documentation for more information. - Uses Pygments for better code highlighting. The supported feature set is equivalent to the `m.code Pelican plugin <{filename}/plugins/math-and-code.rst#code>`_, see its documentation for more information. @@ -280,6 +283,15 @@ Variable Description searched relative to the Doxyfile base dir and to the ``dox2html5.py`` script dir as a fallback. +:ini:`DOT_FONTNAME` Font name to use for ``@dot`` and ``@dotfile`` + commands. To ensure consistent look with the + default m.css themes, set it to + ``Source Sans Pro``. Doxygen default is + ``Helvetica``. +:ini:`DOT_FONTSIZE` Font size to use for ``@dot`` and ``@dotfile`` + commands. To ensure consistent look with the + default m.css themes, set it to ``16``. + Doxygen default is ``10``. =============================== =============================================== In addition, the m.css Doxygen theme recognizes the following extra options: @@ -592,6 +604,13 @@ as well: @image image.png width=250px */ +`Dot graphs`_ +------------- + +Grapviz ``dot`` graphs from the ``@dot`` and ``@dotfile`` commands are rendered +as an inline SVG. Graph name and the ``sizespec`` works equivalently to the +`Images and figures`_. + `Pages, sections and table of contents`_ ---------------------------------------- diff --git a/doxygen/dox2html5.py b/doxygen/dox2html5.py index 14933cd4..d4987bef 100755 --- a/doxygen/dox2html5.py +++ b/doxygen/dox2html5.py @@ -50,6 +50,7 @@ from pygments.formatters import HtmlFormatter from pygments.lexers import TextLexer, BashSessionLexer, get_lexer_by_name, find_lexer_class_for_filename sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../pelican-plugins')) +import dot2svg import latex2svg import latex2svgextra import ansilexer @@ -560,7 +561,7 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET. # - , (those are the same thing!) # - (a weird grouping thing that we abuse for
s) # - , , - # - , + # - , , ,
# - # - (if block) # - (if block) @@ -582,7 +583,7 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET. end_previous_paragraph = False # Straightforward elements - if i.tag in ['heading', 'blockquote', 'hruler', 'xrefsect', 'variablelist', 'verbatim', 'parblock', 'preformatted', 'itemizedlist', 'orderedlist', 'image', 'table', '{http://mcss.mosra.cz/doxygen/}div']: + if i.tag in ['heading', 'blockquote', 'hruler', 'xrefsect', 'variablelist', 'verbatim', 'parblock', 'preformatted', 'itemizedlist', 'orderedlist', 'image', 'dot', 'dotfile', 'table', '{http://mcss.mosra.cz/doxygen/}div']: end_previous_paragraph = True # describing return type is cut out of text flow, so @@ -1065,6 +1066,41 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET. out.parsed += 'Image'.format( ' ' + add_css_class if add_css_class else '', name, sizespec) + elif i.tag in ['dot', 'dotfile']: + # can be in but often also in
and other m.css-specific + # elements + has_block_elements = True + + # Why the heck can't it just read the file and paste it into the + # XML?! + caption = None + if i.tag == 'dotfile': + if 'name' in i.attrib: + with open(i.attrib['name'], 'r') as f: + source = f.read() + else: + logging.warning("{}: file passed to @dotfile was not found, rendering an empty graph") + source = 'digraph "" {}' + caption = i.text + else: + source = i.text + if 'caption' in i.attrib: caption = i.attrib['caption'] + + size = None + if 'width' in i.attrib: + size = 'width: {};'.format(i.attrib['width']) + elif 'height' in i.attrib: + size = 'height: {};'.format(i.attrib['height']) + + if caption: + out.parsed += '
{}
{}
'.format(dot2svg.dot2svg( + source, size=size, + attribs=' class="m-graph{}"'.format(' ' + add_css_class if add_css_class else '')), + caption) + else: + out.parsed += '
{}
'.format( + ' ' + add_css_class if add_css_class else '', dot2svg.dot2svg(source, size)) + elif i.tag == 'hruler': assert element.tag == 'para' # is inside a paragraph :/ out.parsed += '
' @@ -2847,6 +2883,8 @@ def parse_doxyfile(state: State, doxyfile, config = None): 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600', '../css/m-dark+doxygen.compiled.css'], 'HTML_EXTRA_FILES': [], + 'DOT_FONTNAME': ['Helvetica'], + 'DOT_FONTSIZE': ['10'], 'M_CLASS_TREE_EXPAND_LEVELS': ['1'], 'M_FILE_TREE_EXPAND_LEVELS': ['1'], @@ -2957,6 +2995,7 @@ list using and 'OUTPUT_DIRECTORY', 'HTML_OUTPUT', 'XML_OUTPUT', + 'DOT_FONTNAME', 'M_MAIN_PROJECT_URL', 'M_HTML_HEADER', 'M_PAGE_HEADER', @@ -2969,7 +3008,8 @@ list using and if i in config: state.doxyfile[i] = '\n'.join(config[i]) # Int values that we want - for i in ['M_CLASS_TREE_EXPAND_LEVELS', + for i in ['DOT_FONTSIZE', + 'M_CLASS_TREE_EXPAND_LEVELS', 'M_FILE_TREE_EXPAND_LEVELS']: if i in config: state.doxyfile[i] = int(' '.join(config[i])) @@ -3017,6 +3057,9 @@ def run(doxyfile, templates=default_templates, wildcard=default_wildcard, index_ else: latex2svgextra.unpickle_cache(None) + # Configure graphviz/dot + dot2svg.configure(state.doxyfile['DOT_FONTNAME'], state.doxyfile['DOT_FONTSIZE']) + if sort_globbed_files: xml_files_metadata.sort() xml_files.sort() diff --git a/doxygen/test/contents_custom/Doxyfile b/doxygen/test/contents_custom/Doxyfile index a2cb09dc..32e461f3 100644 --- a/doxygen/test/contents_custom/Doxyfile +++ b/doxygen/test/contents_custom/Doxyfile @@ -6,6 +6,10 @@ GENERATE_LATEX = NO GENERATE_XML = YES XML_PROGRAMLISTING = NO +DOTFILE_DIRS = . +DOT_FONTNAME = DejaVu Sans +DOT_FONTSIZE = 16 + ##! M_PAGE_FINE_PRINT = ##! M_THEME_COLOR = ##! M_FAVICON = diff --git a/doxygen/test/contents_custom/ab.dot b/doxygen/test/contents_custom/ab.dot new file mode 100644 index 00000000..69d0ddfe --- /dev/null +++ b/doxygen/test/contents_custom/ab.dot @@ -0,0 +1,4 @@ +strict graph "" { + a -- b + a -- b +} diff --git a/doxygen/test/contents_custom/dot-236.html b/doxygen/test/contents_custom/dot-236.html new file mode 100644 index 00000000..b7b246d1 --- /dev/null +++ b/doxygen/test/contents_custom/dot-236.html @@ -0,0 +1,67 @@ + + + + + Dot | My Project + + + + + +
+
+
+
+
+

+ Dot +

+

A red graph:

+ + +a + +a + + +b + +b + + +a--b + + + + +

A blue graph, from a file:

+ + +a + +a + + +b + +b + + +a--b + + + + +
+
+
+
+
+ + diff --git a/doxygen/test/contents_custom/dot-238.html b/doxygen/test/contents_custom/dot-238.html new file mode 100644 index 00000000..295af83c --- /dev/null +++ b/doxygen/test/contents_custom/dot-238.html @@ -0,0 +1,67 @@ + + + + + Dot | My Project + + + + + +
+
+
+
+
+

+ Dot +

+

A red graph:

+ + +a + +a + + +b + +b + + +a--b + + + + +

A blue graph, from a file:

+ + +a + +a + + +b + +b + + +a--b + + + + +
+
+
+
+
+ + diff --git a/doxygen/test/contents_custom/dot.html b/doxygen/test/contents_custom/dot.html new file mode 100644 index 00000000..da5879c2 --- /dev/null +++ b/doxygen/test/contents_custom/dot.html @@ -0,0 +1,67 @@ + + + + + Dot | My Project + + + + + +
+
+
+
+
+

+ Dot +

+

A red graph:

+ + +a + +a + + +b + +b + + +a--b + + + + +

A blue graph, from a file:

+ + +a + +a + + +b + +b + + +a--b + + + + +
+
+
+
+
+ + diff --git a/doxygen/test/contents_custom/input.dox b/doxygen/test/contents_custom/input.dox index a1f542ce..e52258c4 100644 --- a/doxygen/test/contents_custom/input.dox +++ b/doxygen/test/contents_custom/input.dox @@ -80,3 +80,23 @@ A green formula: A yellow @m_class{m-warning} @f$ \Sigma @f$ inline formula. */ + +/** @page dot Dot + +A red graph: + +@m_class{m-danger} + +@dot +strict graph "" { + a -- b + a -- b +} +@enddot + +A blue graph, from a file: + +@m_class{m-info} + +@dotfile ab.dot +*/ diff --git a/doxygen/test/contents_dot/Doxyfile b/doxygen/test/contents_dot/Doxyfile new file mode 100644 index 00000000..9cd0d80c --- /dev/null +++ b/doxygen/test/contents_dot/Doxyfile @@ -0,0 +1,17 @@ +INPUT = input.dox +QUIET = YES +GENERATE_HTML = NO +GENERATE_LATEX = NO +GENERATE_XML = YES +XML_PROGRAMLISTING = NO + +DOTFILE_DIRS = . +DOT_FONTNAME = DejaVu Sans +DOT_FONTSIZE = 16 + +##! M_PAGE_FINE_PRINT = +##! M_THEME_COLOR = +##! M_FAVICON = +##! M_LINKS_NAVBAR1 = +##! M_LINKS_NAVBAR2 = +##! M_SEARCH_DISABLED = YES diff --git a/doxygen/test/contents_dot/ab.dot b/doxygen/test/contents_dot/ab.dot new file mode 100644 index 00000000..69d0ddfe --- /dev/null +++ b/doxygen/test/contents_dot/ab.dot @@ -0,0 +1,4 @@ +strict graph "" { + a -- b + a -- b +} diff --git a/doxygen/test/contents_dot/colors.dot b/doxygen/test/contents_dot/colors.dot new file mode 100644 index 00000000..699de0f9 --- /dev/null +++ b/doxygen/test/contents_dot/colors.dot @@ -0,0 +1,6 @@ +digraph Colors { + a [class="m-success"] + b [style=filled shape=circle class="m-dim"] + a -> b [class="m-warning" label="yes"] + b -> b [class="m-primary" label="no"] +} diff --git a/doxygen/test/contents_dot/index-236.html b/doxygen/test/contents_dot/index-236.html new file mode 100644 index 00000000..7af67227 --- /dev/null +++ b/doxygen/test/contents_dot/index-236.html @@ -0,0 +1,132 @@ + + + + + My Project + + + + + +
+
+
+
+
+

+ My Project +

+

Note: the test uses DejaVu Sans instead of Source Sans Pro in order to have predictable rendering on the CIs.

+ +Basics + +a + +a + + +b + + +b + + +a->b + + + + +c + +c + + +b->c + + +0 + + +c->c + + +1 + + + +
+ +Colors + +a + +a + + +b + +b + + +a->b + + +yes + + +b->b + + +no + + + +
+ + +a + +a + + +b + +b + + +a--b + + + + +
A graph
+ + +a + +a + + +b + +b + + +a--b + + + + +
A graph
+
+
+
+
+ + diff --git a/doxygen/test/contents_dot/index-238.html b/doxygen/test/contents_dot/index-238.html new file mode 100644 index 00000000..3e7bcdf7 --- /dev/null +++ b/doxygen/test/contents_dot/index-238.html @@ -0,0 +1,133 @@ + + + + + My Project + + + + + +
+
+
+
+
+

+ My Project +

+

Note: the test uses DejaVu Sans instead of Source Sans Pro in order to have predictable rendering on the CIs.

+ +Basics + +a + +a + + +b + + +b + + +a->b + + + + +c + +c + + +b->c + + +0 + + +c->c + + +1 + + + +
+ +Colors + +a + +a + + +b + +b + + +a->b + + +yes + + +b->b + + +no + + + +
+
+ + +a + +a + + +b + +b + + +a--b + + + + +
A graph
+ + +a + +a + + +b + +b + + +a--b + + + + +
A graph
+
+
+
+
+ + diff --git a/doxygen/test/contents_dot/index.html b/doxygen/test/contents_dot/index.html new file mode 100644 index 00000000..0247b5ae --- /dev/null +++ b/doxygen/test/contents_dot/index.html @@ -0,0 +1,132 @@ + + + + + My Project + + + + + +
+
+
+
+
+

+ My Project +

+

Note: the test uses DejaVu Sans instead of Source Sans Pro in order to have predictable rendering on the CIs.

+ +Basics + +a + +a + + +b + + +b + + +a->b + + + + +c + +c + + +b->c + + +0 + + +c->c + + +1 + + + +
+ +Colors + +a + +a + + +b + +b + + +a->b + + +yes + + +b->b + + +no + + + +
+ + +a + +a + + +b + +b + + +a--b + + + + +
A graph
+ + +a + +a + + +b + +b + + +a--b + + + + +
A graph
+
+
+
+
+ + diff --git a/doxygen/test/contents_dot/input.dox b/doxygen/test/contents_dot/input.dox new file mode 100644 index 00000000..d2012608 --- /dev/null +++ b/doxygen/test/contents_dot/input.dox @@ -0,0 +1,36 @@ +/** @mainpage + +Note: the test uses DejaVu Sans instead of Source Sans Pro in order to have +predictable rendering on the CIs. + +@dot +digraph Basics { + rankdir=LR + + a [style=filled shape=rect] + b [peripheries=2 shape=circle] + c [shape=ellipse] + a -> b + b -> c [label="0" fontsize=40] + c -> c [label="1"] +} +@enddot + +@dotfile colors.dot + +@dot "A graph" width=5rem +strict graph "" { + a -- b + a -- b +} +@enddot + +@dotfile ab.dot "A graph" height=5rem +*/ + +/** @page warnings + +This file doesn't exist: + +@dotfile nonexistent.dot +*/ diff --git a/doxygen/test/contents_dot/warnings.html b/doxygen/test/contents_dot/warnings.html new file mode 100644 index 00000000..42e2f66d --- /dev/null +++ b/doxygen/test/contents_dot/warnings.html @@ -0,0 +1,35 @@ + + + + + warnings | My Project + + + + + +
+
+
+
+
+

+ warnings +

+

This file doesn't exist:

+ + + +
+
+
+
+
+ + \ No newline at end of file diff --git a/doxygen/test/test_contents.py b/doxygen/test/test_contents.py index 3e1d8433..89956787 100644 --- a/doxygen/test/test_contents.py +++ b/doxygen/test/test_contents.py @@ -24,6 +24,7 @@ import os import pickle +import re import shutil import subprocess import unittest @@ -34,6 +35,9 @@ from distutils.version import LooseVersion from . import BaseTestCase, IntegrationTestCase, doxygen_version +def dot_version(): + return re.match(".*version (?P\d+\.\d+\.\d+).*", subprocess.check_output(['dot', '-V'], stderr=subprocess.STDOUT).decode('utf-8').strip()).group('version') + class Typography(IntegrationTestCase): def __init__(self, *args, **kwargs): super().__init__(__file__, 'typography', *args, **kwargs) @@ -244,6 +248,25 @@ class Custom(IntegrationTestCase): self.run_dox2html5(wildcard='math.xml') self.assertEqual(*self.actual_expected_contents('math.html')) + @unittest.skipUnless(LooseVersion(dot_version()) >= LooseVersion("2.40.1"), + "Dot < 2.40.1 has a completely different output.") + def test_dot(self): + self.run_dox2html5(wildcard='dot.xml') + self.assertEqual(*self.actual_expected_contents('dot.html')) + + @unittest.skipUnless(LooseVersion(dot_version()) < LooseVersion("2.40.1") and + LooseVersion(dot_version()) >= LooseVersion("2.38.0"), + "Dot < 2.38 and dot > 2.38 has a completely different output.") + def test_dot238(self): + self.run_dox2html5(wildcard='dot.xml') + self.assertEqual(*self.actual_expected_contents('dot.html', 'dot-238.html')) + + @unittest.skipUnless(LooseVersion(dot_version()) < LooseVersion("2.38.0"), + "Dot > 2.36 has a completely different output.") + def test_dot236(self): + self.run_dox2html5(wildcard='dot.xml') + self.assertEqual(*self.actual_expected_contents('dot.html', 'dot-236.html')) + class ParseError(BaseTestCase): def __init__(self, *args, **kwargs): super().__init__(__file__, 'parse_error', *args, **kwargs) @@ -328,3 +351,30 @@ class UnexpectedSections(IntegrationTestCase): def test(self): self.run_dox2html5(wildcard='File_8h.xml') self.assertEqual(*self.actual_expected_contents('File_8h.html')) + +class Dot(IntegrationTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, 'dot', *args, **kwargs) + + @unittest.skipUnless(LooseVersion(dot_version()) >= LooseVersion("2.40.1"), + "Dot < 2.40.1 has a completely different output.") + def test(self): + self.run_dox2html5(wildcard='indexpage.xml') + self.assertEqual(*self.actual_expected_contents('index.html')) + + @unittest.skipUnless(LooseVersion(dot_version()) < LooseVersion("2.40.1") and + LooseVersion(dot_version()) >= LooseVersion("2.38.0"), + "Dot < 2.38 and dot > 2.38 has a completely different output.") + def test_238(self): + self.run_dox2html5(wildcard='indexpage.xml') + self.assertEqual(*self.actual_expected_contents('index.html', 'index-238.html')) + + @unittest.skipUnless(LooseVersion(dot_version()) < LooseVersion("2.38.0"), + "Dot > 2.36 has a completely different output.") + def test_236(self): + self.run_dox2html5(wildcard='indexpage.xml') + self.assertEqual(*self.actual_expected_contents('index.html', 'index-236.html')) + + def test_warnings(self): + self.run_dox2html5(wildcard='warnings.xml') + self.assertEqual(*self.actual_expected_contents('warnings.html')) diff --git a/doxygen/test/test_doxyfile.py b/doxygen/test/test_doxyfile.py index a86a0311..36c33eb1 100644 --- a/doxygen/test/test_doxyfile.py +++ b/doxygen/test/test_doxyfile.py @@ -42,6 +42,8 @@ class Doxyfile(unittest.TestCase): state = State() parse_doxyfile(state, 'test/doxyfile/Doxyfile') self.assertEqual(state.doxyfile, { + 'DOT_FONTNAME': 'Helvetica', + 'DOT_FONTSIZE': 10, 'HTML_EXTRA_FILES': ['css', 'another.png', 'hello'], 'HTML_EXTRA_STYLESHEET': ['a.css', 'b.css'], 'HTML_OUTPUT': 'html', diff --git a/pelican-plugins/dot2svg.py b/pelican-plugins/dot2svg.py index 5c224bd8..bece7ad9 100644 --- a/pelican-plugins/dot2svg.py +++ b/pelican-plugins/dot2svg.py @@ -34,6 +34,8 @@ _patch_src = re.compile(r"""<\?xml version="1\.0" encoding="UTF-8" standalone="n _patch_dst = r""" +\n""") @@ -63,7 +65,7 @@ _font_size = 0.0 # converting to rem here def _pt2em(pt): return pt/_font_size -def dot2svg(source, attribs=''): +def dot2svg(source, size=None, attribs=''): try: ret = subprocess.run(['dot', '-Tsvg', '-Gfontname={}'.format(_font), @@ -83,12 +85,15 @@ def dot2svg(source, attribs=''): svg = _comment_src.sub('', ret.stdout.decode('utf-8')) # Remove preamble and fixed size - def patch_repl(match): return _patch_dst.format( - attribs=attribs, - width=_pt2em(float(match.group('width'))), - height=_pt2em(float(match.group('height'))), - viewBox=match.group('viewBox')) - svg = _patch_src.sub(patch_repl, svg) + if size: + svg = _patch_src.sub(_patch_custom_size_dst.format(attribs=attribs, size=size), svg) + else: + def patch_repl(match): return _patch_dst.format( + attribs=attribs, + width=_pt2em(float(match.group('width'))), + height=_pt2em(float(match.group('height'))), + viewBox=match.group('viewBox')) + svg = _patch_src.sub(patch_repl, svg) # Remove unnecessary IDs and attributes, replace classes for elements def element_repl(match): -- 2.30.2