chiark / gitweb /
doxygen: ability to add custom CSS classes to the output.
authorVladimír Vondruš <mosra@centrum.cz>
Thu, 14 Dec 2017 19:48:03 +0000 (20:48 +0100)
committerVladimír Vondruš <mosra@centrum.cz>
Thu, 14 Dec 2017 19:59:22 +0000 (20:59 +0100)
doc/doxygen.rst
doxygen/dox2html5.py
doxygen/test/contents_custom/Doxyfile [new file with mode: 0644]
doxygen/test/contents_custom/index.html [new file with mode: 0644]
doxygen/test/contents_custom/input.dox [new file with mode: 0644]
doxygen/test/contents_custom/math.html [new file with mode: 0644]
doxygen/test/contents_custom/ship-small.jpg [new symlink]
doxygen/test/test_contents.py

index 61877557f6a2c002e4af23b9bdb9b3840fd52e81..0ac782909a51d58a3ae220ac63a7b6849b4333ac 100644 (file)
@@ -568,6 +568,83 @@ aliases in the original ``Doxyfile``:
     Doxygen with :gh:`doxygen/doxygen#623` applied, otherwise the codes will be
     present in the rendered output in their raw form.
 
+`Custom styling`_
+-----------------
+
+It's possible to insert custom m.css classes into the Doxygen output. Add the
+following to your ``Doxyfile-mcss``:
+
+.. code:: ini
+
+    ALIASES += \
+        "m_div{1}=@xmlonly<mcss:div xmlns:mcss=\"http://mcss.mosra.cz/doxygen/\" mcss:class=\"\1\">@endxmlonly" \
+        "m_enddiv=@xmlonly</mcss:div>@endxmlonly" \
+        "m_span{1}=@xmlonly<mcss:span xmlns:mcss=\"http://mcss.mosra.cz/doxygen/\" mcss:class=\"\1\">@endxmlonly" \
+        "m_endspan=@xmlonly</mcss:span>@endxmlonly" \
+        "m_class{1}=@xmlonly<mcss:class xmlns:mcss=\"http://mcss.mosra.cz/doxygen/\" mcss:class=\"\1\" />@endxmlonly"
+
+If you need backwards compatibility with stock Doxygen HTML output, just make
+the aliases empty in your original ``Doxyfile``. Note that you can rename the
+aliases however you want to fit your naming scheme.
+
+.. code:: ini
+
+    ALIASES += \
+        "m_div{1}=" \
+        "m_enddiv=" \
+        "m_span{1}=" \
+        "m_endspan=" \
+        "m_class{1}="
+
+With ``@m_div`` and ``@m_span`` it's possible to wrap individual paragraphs or
+inline text in :html:`<div>` / :html:`<span>` and add CSS classes to them.
+Example usage and corresponding rendered HTML output:
+
+.. code-figure::
+
+    .. code:: c++
+
+        /**
+        @div{m-note m-dim m-text-center} This paragraph is rendered in a dim
+        note, centered. @enddiv
+
+        This text contains a @span{m-text m-success} green @endspan word.
+        */
+
+    .. note-dim::
+        :class: m-text-center
+
+        This paragraph is rendered in a dim note, centered.
+
+    .. role:: success
+        :class: m-text m-success
+
+    This text contains a :success:`green` word.
+
+.. note-warning::
+
+    Note that due to Doxygen XML output limitations it's not possible to wrap
+    multiple paragraphs this way, attempt to do that will result in an invalid
+    XML file that can't be processed. Similarly, if you forget a closing
+    ``@enddiv`` / ``@endspan`` or misplace them, the result will be an invalid
+    XML file.
+
+With ``@m_class`` it's possible to add CSS classes to the immediately following
+paragraph, image, table, list or math formula block. When used inline, it
+affects the immediately following emphasis, strong text, link or inline math
+formula. Example usage:
+
+.. code-figure::
+
+    .. code:: c++
+
+        /** See the red @m_class{m-danger} @f$ \Sigma @f$ character. */
+
+    .. role:: math-danger(math)
+        :class: m-danger
+
+    See the red :math-danger:`\Sigma` character.
+
 `Customizing the template`_
 ===========================
 
index 9feab44a8f028f88379b7102f663009d5e6e982d..b6a915a828c40744d5bcfcebe0728396bb4c92a7 100755 (executable)
@@ -134,12 +134,13 @@ def parse_type(state: State, type: ET.Element) -> str:
     # Remove spacing inside <> and before & and *
     return fix_type_spacing(out)
 
-def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.Element = None, trim = True):
+def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.Element = None, trim = True, add_css_class = None):
     out = Empty()
     out.section = None
     out.templates = {}
     out.params = {}
     out.return_value = None
+    out.add_css_class = None
 
     # DOXYGEN <PARA> PATCHING 1/4
     #
@@ -160,6 +161,10 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
     if element.text:
         out.parsed = html.escape(element.text.strip() if trim else element.text)
 
+        # There's some inline text at the start, *do not* add any CSS class to
+        # the first child element
+        add_css_class = None
+
     # Needed later for deciding whether we can strip the surrounding <p> from
     # the content
     paragraph_count = 0
@@ -169,6 +174,9 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
     # kind), set only if there is no i.tail, reset in the next iteration.
     previous_section = None
 
+    # A CSS class to be added inline (not propagated outside of the paragraph)
+    add_inline_css_class = None
+
     i: ET.Element
     for index, i in enumerate(element):
         # State used later
@@ -200,6 +208,7 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
         # - <verbatim>
         # - <variablelist>, <itemizedlist>, <orderedlist>
         # - <image>, <table>
+        # - <mcss:div>
         # - <formula> (if block)
         # - <programlisting> (if block)
         #
@@ -220,7 +229,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', 'xrefsect', 'variablelist', 'verbatim', 'itemizedlist', 'orderedlist', 'image', 'table']:
+            if i.tag in ['heading', 'blockquote', 'xrefsect', 'variablelist', 'verbatim', 'itemizedlist', 'orderedlist', 'image', 'table', '{http://mcss.mosra.cz/doxygen/}div']:
                 end_previous_paragraph = True
 
             # <simplesect> describing return type is cut out of text flow, so
@@ -362,21 +371,28 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
             # itself. Also, some paragraphs are actually block content and we
             # might not want to write the start/closing tag.
             #
-            # Also, to make things even funnier, parameter and return value
-            # description come from inside of some paragraph, so bubble them up
-            # and assume they are not scattered all over the place (ugh).
-            #
             # There's also the patching of nested lists that results in the
             # immediate_parent variable in the section 2/4 -- we pass the
             # parent only if this is the first paragraph inside it.
-            parsed = parse_desc_internal(state, i, element if paragraph_count == 1 and not has_block_elements else None, False)
+            parsed = parse_desc_internal(state, i,
+                immediate_parent=element if paragraph_count == 1 and not has_block_elements else None,
+                trim=False,
+                add_css_class=add_css_class)
             parsed.parsed = parsed.parsed.strip()
             if not parsed.is_reasonable_paragraph:
                 has_block_elements = True
             if parsed.parsed:
-                if parsed.write_paragraph_start_tag: out.parsed += '<p>'
+                if parsed.write_paragraph_start_tag:
+                    # If there is some inline content at the beginning, assume
+                    # the CSS class was meant to be added to the paragraph
+                    # itself, not into a nested (block) element.
+                    out.parsed += '<p{}>'.format(' class="{}"'.format(add_css_class) if add_css_class else '')
                 out.parsed += parsed.parsed
                 if parsed.write_paragraph_close_tag: out.parsed += '</p>'
+
+            # Also, to make things even funnier, parameter and return value
+            # description come from inside of some paragraph, so bubble them up
+            # and assume they are not scattered all over the place (ugh).
             if parsed.templates:
                 assert not out.templates
                 out.templates = parsed.templates
@@ -387,6 +403,15 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
                 assert not out.return_value
                 out.return_value = parsed.return_value
 
+            # The same is (of course) with bubbling up the <mcss:class>
+            # element. Reset the current value with the value coming from
+            # inside -- it's either reset back to None or scheduled to be used
+            # in the next iteration. In order to make this work, the resetting
+            # code at the end of the loop iteration resets it to None only if
+            # this is not a paragraph or the <mcss:class> element -- so we are
+            # resetting here explicitly.
+            add_css_class = parsed.add_css_class
+
             # Assert we didn't miss anything important
             assert not parsed.section
 
@@ -399,7 +424,8 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
             assert element.tag == 'para' # is inside a paragraph :/
             has_block_elements = True
             tag = 'ul' if i.tag == 'itemizedlist' else 'ol'
-            out.parsed += '<{}>'.format(tag)
+            out.parsed += '<{}{}>'.format(tag,
+                ' class="{}"'.format(add_css_class) if add_css_class else '')
             for li in i:
                 assert li.tag == 'listitem'
                 out.parsed += '<li>{}</li>'.format(parse_desc(state, li))
@@ -408,7 +434,8 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
         elif i.tag == 'table':
             assert element.tag == 'para' # is inside a paragraph :/
             has_block_elements = True
-            out.parsed += '<table class="m-table">'
+            out.parsed += '<table class="m-table{}">'.format(
+                ' ' + add_css_class if add_css_class else '')
             inside_tbody = False
 
             row: ET.Element
@@ -545,9 +572,34 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
 
                 caption = i.text
                 if caption:
-                    out.parsed += '<figure class="m-figure"><img src="{}" alt="Image" /><figcaption>{}</figcaption></figure>'.format(name, html.escape(caption))
+                    out.parsed += '<figure class="m-figure{}"><img src="{}" alt="Image" /><figcaption>{}</figcaption></figure>'.format(
+                        ' ' + add_css_class if add_css_class else '',
+                        name, html.escape(caption))
                 else:
-                    out.parsed += '<img class="m-image" src="{}" alt="Image" />'.format(name)
+                    out.parsed += '<img class="m-image{}" src="{}" alt="Image" />'.format(
+                        ' ' + add_css_class if add_css_class else '', name)
+
+        # Custom <div> with CSS classes (for making dim notes etc)
+        elif i.tag == '{http://mcss.mosra.cz/doxygen/}div':
+            assert element.tag == 'para' # is inside a paragraph :/
+            has_block_elements = True
+
+            out.parsed += '<div class="{}">{}</div>'.format(i.attrib['{http://mcss.mosra.cz/doxygen/}class'], parse_desc(state, i))
+
+        # Adding a custom CSS class to the immediately following block/inline
+        # element
+        elif i.tag == '{http://mcss.mosra.cz/doxygen/}class':
+            assert element.tag == 'para' # is inside a paragraph :/
+
+            # Bubble up in case we are alone in a paragraph, as that's meant to
+            # affect the next paragraph content.
+            if len([listing for listing in element]) == 1:
+                out.add_css_class = i.attrib['{http://mcss.mosra.cz/doxygen/}class']
+
+            # Otherwise this is meant to only affect inline elements in this
+            # paragraph:
+            else:
+                add_inline_css_class = i.attrib['{http://mcss.mosra.cz/doxygen/}class']
 
         # Either block or inline
         elif i.tag == 'programlisting':
@@ -643,7 +695,11 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
             # Strip whitespace around if inline code, strip only trailing
             # whitespace if a block
             highlighted = highlighted.rstrip() if code_block else highlighted.strip()
-            out.parsed += '<{0} class="{1}">{2}</{0}>'.format('pre' if code_block else 'code', class_, highlighted)
+            out.parsed += '<{0} class="{1}{2}">{3}</{0}>'.format(
+                'pre' if code_block else 'code',
+                class_,
+                ' ' + add_css_class if code_block and add_css_class else '',
+                highlighted)
 
         # Either block or inline
         elif i.tag == 'formula':
@@ -654,14 +710,18 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
             if formula_block:
                 has_block_elements = True
                 rendered = latex2svg.latex2svg('$${}$$'.format(i.text[3:-3]), params=m.math.latex2svg_params)
-                out.parsed += '<div class="m-math">{}</div>'.format(m.math._patch(i.text, rendered, ''))
+                out.parsed += '<div class="m-math{}">{}</div>'.format(
+                    ' ' + add_css_class if add_css_class else '',
+                    m.math._patch(i.text, rendered, ''))
             else:
                 rendered = latex2svg.latex2svg('${}$'.format(i.text[2:-2]), params=m.math.latex2svg_params)
 
                 # CSS classes and styling for proper vertical alignment. Depth is relative
                 # to font size, describes how below the line the text is. Scaling it back
                 # to 12pt font, scaled by 125% as set above in the config.
-                attribs = ' class="m-math" style="vertical-align: -{:.1f}pt;"'.format(rendered['depth']*12*1.25)
+                attribs = ' class="m-math{}" style="vertical-align: -{:.1f}pt;"'.format(
+                    ' ' + add_inline_css_class if add_inline_css_class else '',
+                    rendered['depth']*12*1.25)
                 out.parsed += m.math._patch(i.text, rendered, attribs)
 
         # Inline elements
@@ -676,16 +736,27 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
             out.parsed += '<code>{}</code>'.format(parse_inline_desc(state, i))
 
         elif i.tag == 'emphasis':
-            out.parsed += '<em>{}</em>'.format(parse_inline_desc(state, i))
+            out.parsed += '<em{}>{}</em>'.format(
+                ' class="{}"'.format(add_inline_css_class) if add_inline_css_class else '',
+                parse_inline_desc(state, i))
 
         elif i.tag == 'bold':
-            out.parsed += '<strong>{}</strong>'.format(parse_inline_desc(state, i))
+            out.parsed += '<strong{}>{}</strong>'.format(
+                ' class="{}"'.format(add_inline_css_class) if add_inline_css_class else '',
+                parse_inline_desc(state, i))
 
         elif i.tag == 'ref':
             out.parsed += parse_ref(state, i)
 
         elif i.tag == 'ulink':
-            out.parsed += '<a href="{}">{}</a>'.format(html.escape(i.attrib['url']), add_wbr(parse_inline_desc(state, i)))
+            out.parsed += '<a href="{}"{}>{}</a>'.format(
+                html.escape(i.attrib['url']),
+                ' class="{}"'.format(add_inline_css_class) if add_inline_css_class else '',
+                add_wbr(parse_inline_desc(state, i)))
+
+        # <span> with custom CSS classes
+        elif i.tag == '{http://mcss.mosra.cz/doxygen/}span':
+            out.parsed += '<span class="{}">{}</span>'.format(i.attrib['{http://mcss.mosra.cz/doxygen/}class'], parse_inline_desc(state, i))
 
         # WHAT THE HELL WHY IS THIS NOT AN XML ENTITY
         elif i.tag == 'ndash': out.parsed += '&ndash;'
@@ -701,6 +772,20 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
         if i.tag != 'simplesect' and previous_section:
             previous_section = None
 
+        # A custom inline CSS class was used (or was meant to be used) in this
+        # iteration, reset it so it's not added again in the next iteration. If
+        # this is a <mcss:class> element, it was added just now, don't reset
+        # it.
+        if i.tag != '{http://mcss.mosra.cz/doxygen/}class' and add_inline_css_class:
+            add_inline_css_class = None
+
+        # A custom block CSS class was used (or was meant to be used) in this
+        # iteration, reset it so it's not added again in the next iteration. If
+        # this is a paragraph, it might be added just now from within the
+        # nested content, don't reset it.
+        if i.tag != 'para' and add_css_class:
+            add_css_class = None
+
         # DOXYGEN <PARA> PATCHING 4/4
         #
         # Besides putting notes and blockquotes and shit inside paragraphs,
diff --git a/doxygen/test/contents_custom/Doxyfile b/doxygen/test/contents_custom/Doxyfile
new file mode 100644 (file)
index 0000000..a902bd3
--- /dev/null
@@ -0,0 +1,15 @@
+INPUT                   = input.dox
+IMAGE_PATH              = .
+QUIET                   = YES
+GENERATE_HTML           = NO
+GENERATE_LATEX          = NO
+GENERATE_XML            = YES
+
+M_SHOW_DOXYGEN_VERSION  = NO
+
+ALIASES = \
+    "m_div{1}=@xmlonly<mcss:div xmlns:mcss=\"http://mcss.mosra.cz/doxygen/\" mcss:class=\"\1\">@endxmlonly" \
+    "m_enddiv=@xmlonly</mcss:div>@endxmlonly" \
+    "m_span{1}=@xmlonly<mcss:span xmlns:mcss=\"http://mcss.mosra.cz/doxygen/\" mcss:class=\"\1\">@endxmlonly" \
+    "m_endspan=@xmlonly</mcss:span>@endxmlonly" \
+    "m_class{1}=@xmlonly<mcss:class xmlns:mcss=\"http://mcss.mosra.cz/doxygen/\" mcss:class=\"\1\" />@endxmlonly"
diff --git a/doxygen/test/contents_custom/index.html b/doxygen/test/contents_custom/index.html
new file mode 100644 (file)
index 0000000..f21a915
--- /dev/null
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>My Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+doxygen.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <meta name="theme-color" content="#22272e" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
+      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
+        <div class="m-row">
+          <ol class="m-col-t-6 m-col-m-none">
+            <li><a href="pages.html" id="m-navbar-current">Pages</a></li>
+            <li><a href="namespaces.html">Namespaces</a></li>
+          </ol>
+          <ol class="m-col-t-6 m-col-m-none" start="3">
+            <li><a href="annotated.html">Classes</a></li>
+            <li><a href="files.html">Files</a></li>
+          </ol>
+        </div>
+      </div>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          My Project
+        </h1>
+<div class="m-note m-dim m-text-center">This paragraph is rendered in a dim note, centered.</div><p>This paragraph contains a <span class="m-text m-danger">red text</span> in a normal text flow and then  <strong class="m-text m-em m-small">small strong italics</strong>,   <em class="m-text m-info">blue italics</em> and  <a href="https://mcss.mosra.cz" class="m-text m-big">https:/<wbr />/<wbr />mcss.mosra.cz</a> (big-ass link).</p><p>A paragraph that is not affected by the inline classes from above.</p><p class="m-text m-strong m-noindent">Bold, non-indented paragraph.</p><p class="m-text m-primary"><strong>Bold text that should not have</strong> the same class as the paragraph.</p><p>A paragraph that doesn&#x27;t have any class applied. Next, a full-width image:</p><img class="m-image m-fullwidth" src="ship-small.jpg" alt="Image" /><figure class="m-figure m-flat"><img src="ship-small.jpg" alt="Image" /><figcaption>A flat figure</figcaption></figure><p>A fullwidth table:</p><table class="m-table m-fullwidth"><thead><tr><th>Table header</th><th>Another</th></tr></thead><tbody><tr><td>Cell</td><td>Another cell</td></tr><tr><td>Next row</td><td>Last.</td></tr></tbody></table><p>An unstyled list:</p><ul class="m-unstyled"><li>First item without a dot</li><li>Second item without a dot</li></ul>
+      </div>
+    </div>
+  </div>
+</article></main>
+<footer><nav>
+  <div class="m-container">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <p>My Project. Created by <a href="http://doxygen.org/">Doxygen</a> and <a href="http://mcss.mosra.cz/">m.css</a>.</p>
+      </div>
+    </div>
+  </div>
+</nav></footer>
+</body>
+</html>
diff --git a/doxygen/test/contents_custom/input.dox b/doxygen/test/contents_custom/input.dox
new file mode 100644 (file)
index 0000000..d05777b
--- /dev/null
@@ -0,0 +1,61 @@
+/** @mainpage
+
+@m_div{m-note m-dim m-text-center} This paragraph is rendered in a dim note,
+centered. @m_enddiv
+
+This paragraph contains a @m_span{m-text m-danger} red text @m_endspan in
+a normal text flow and then @m_class{m-text m-em m-small} **small strong italics**,
+@m_class{m-text m-info} *blue italics* and @m_class{m-text m-big} https://mcss.mosra.cz
+(big-ass link).
+
+A paragraph that is not affected by the inline classes from above.
+
+@m_class{m-text m-strong m-noindent}
+
+Bold, non-indented paragraph.
+
+@m_class{m-text m-primary}
+
+__Bold text that should not have__ the same class as the paragraph.
+
+A paragraph that doesn't have any class applied. Next, a full-width image:
+
+@m_class{m-fullwidth}
+
+@image html ship-small.jpg
+
+@m_class{m-flat}
+
+@image html ship-small.jpg A flat figure
+
+A fullwidth table:
+
+@m_class{m-fullwidth}
+
+Table header    | Another
+--------------- | -------
+Cell            | Another cell
+Next row        | Last.
+
+An unstyled list:
+
+@m_class{m-unstyled}
+
+-   First item without a dot
+-   Second item without a dot
+
+*/
+
+/** @page math Math
+
+A green formula:
+
+@m_class{m-success}
+
+@f[
+    \pi^2
+@f]
+
+A yellow @m_class{m-warning} @f$ \Sigma @f$ inline formula.
+
+*/
diff --git a/doxygen/test/contents_custom/math.html b/doxygen/test/contents_custom/math.html
new file mode 100644 (file)
index 0000000..7dcbc58
--- /dev/null
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>Math | My Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+doxygen.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <meta name="theme-color" content="#22272e" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
+      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
+        <div class="m-row">
+          <ol class="m-col-t-6 m-col-m-none">
+            <li><a href="pages.html" id="m-navbar-current">Pages</a></li>
+            <li><a href="namespaces.html">Namespaces</a></li>
+          </ol>
+          <ol class="m-col-t-6 m-col-m-none" start="3">
+            <li><a href="annotated.html">Classes</a></li>
+            <li><a href="files.html">Files</a></li>
+          </ol>
+        </div>
+      </div>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          Math
+        </h1>
+<p>A green formula:</p><div class="m-math m-success"><svg height='12.5906pt' version='1.1' viewBox='188.371 -10.0725 11.3035 10.0725' width='14.1293pt'>
+<title>LaTeX Math</title>
+<desc>
+\[ \pi^2 \]
+</desc>
+<defs>
+<path d='M3.09639 -4.5071H4.44732C4.12453 -3.16812 3.9213 -2.29539 3.9213 -1.33898C3.9213 -1.17161 3.9213 0.119552 4.41146 0.119552C4.66252 0.119552 4.87771 -0.107597 4.87771 -0.310834C4.87771 -0.37061 4.87771 -0.394521 4.79402 -0.573848C4.47123 -1.39875 4.47123 -2.4269 4.47123 -2.51059C4.47123 -2.58232 4.47123 -3.43113 4.72229 -4.5071H6.06127C6.21669 -4.5071 6.61121 -4.5071 6.61121 -4.88966C6.61121 -5.15268 6.38406 -5.15268 6.16887 -5.15268H2.23562C1.96065 -5.15268 1.55417 -5.15268 1.00423 -4.56687C0.6934 -4.22017 0.310834 -3.58655 0.310834 -3.51482S0.37061 -3.41918 0.442341 -3.41918C0.526027 -3.41918 0.537983 -3.45504 0.597758 -3.52677C1.21943 -4.5071 1.8411 -4.5071 2.13998 -4.5071H2.82142C2.55841 -3.61046 2.25953 -2.57036 1.2792 -0.478207C1.18356 -0.286924 1.18356 -0.263014 1.18356 -0.191283C1.18356 0.0597758 1.39875 0.119552 1.50635 0.119552C1.85305 0.119552 1.94869 -0.191283 2.09215 -0.6934C2.28344 -1.30311 2.28344 -1.32702 2.40299 -1.80523L3.09639 -4.5071Z' id='eq1-g0-25'/>
+<path d='M2.24757 -1.6259C2.37509 -1.74545 2.70984 -2.00847 2.83736 -2.12005C3.33151 -2.57435 3.80174 -3.0127 3.80174 -3.73798C3.80174 -4.68643 3.00473 -5.30012 2.00847 -5.30012C1.05205 -5.30012 0.422416 -4.57484 0.422416 -3.8655C0.422416 -3.47497 0.73325 -3.41918 0.844832 -3.41918C1.0122 -3.41918 1.25928 -3.53873 1.25928 -3.84159C1.25928 -4.25604 0.860772 -4.25604 0.765131 -4.25604C0.996264 -4.83786 1.53026 -5.03711 1.9208 -5.03711C2.66202 -5.03711 3.04458 -4.40747 3.04458 -3.73798C3.04458 -2.90909 2.46276 -2.30336 1.52229 -1.33898L0.518057 -0.302864C0.422416 -0.215193 0.422416 -0.199253 0.422416 0H3.57061L3.80174 -1.42665H3.55467C3.53076 -1.26725 3.467 -0.868742 3.37136 -0.71731C3.32354 -0.653549 2.71781 -0.653549 2.59029 -0.653549H1.17161L2.24757 -1.6259Z' id='eq1-g1-50'/>
+</defs>
+<g id='eq1-page1'>
+<use x='188.371' xlink:href='#eq1-g0-25' y='0'/>
+<use x='195.44' xlink:href='#eq1-g1-50' y='-4.93619'/>
+</g>
+</svg></div><p>A yellow  <svg class="m-math m-warning" style="vertical-align: -0.0pt;" height='10.2117pt' version='1.1' viewBox='0 -8.16937 8.45432 8.16937' width='10.5679pt'>
+<title>LaTeX Math</title>
+<desc>
+$ \Sigma $
+</desc>
+<defs>
+<path d='M4.23213 -3.88543C4.32777 -4.00498 4.35168 -4.02889 4.35168 -4.08867C4.35168 -4.11258 4.35168 -4.13649 4.27995 -4.23213L1.86501 -7.81868H4.68643C6.69489 -7.81868 7.30461 -7.38829 7.53176 -5.49938H7.79477L7.48394 -8.16538H0.944458C0.657534 -8.16538 0.645579 -8.16538 0.645579 -7.89041L3.55068 -3.58655L0.777086 -0.310834C0.681445 -0.203238 0.657534 -0.167372 0.657534 -0.119552C0.657534 0 0.753176 0 0.944458 0H7.48394L7.79477 -2.78555H7.53176C7.31656 -0.812951 6.56339 -0.466252 4.65056 -0.466252H1.33898L4.23213 -3.88543Z' id='eq2-g0-6'/>
+</defs>
+<g id='eq2-page1'>
+<use x='0' xlink:href='#eq2-g0-6' y='0'/>
+</g>
+</svg> inline formula.</p>
+      </div>
+    </div>
+  </div>
+</article></main>
+<footer><nav>
+  <div class="m-container">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <p>My Project. Created by <a href="http://doxygen.org/">Doxygen</a> and <a href="http://mcss.mosra.cz/">m.css</a>.</p>
+      </div>
+    </div>
+  </div>
+</nav></footer>
+</body>
+</html>
\ No newline at end of file
diff --git a/doxygen/test/contents_custom/ship-small.jpg b/doxygen/test/contents_custom/ship-small.jpg
new file mode 120000 (symlink)
index 0000000..dbc21c6
--- /dev/null
@@ -0,0 +1 @@
+../../../doc/static/ship-small.jpg
\ No newline at end of file
index 03fe0f9a662791f14ee12bacb89fda92a891a1fd..704b295fa1fa97383f491a079dd9bbe936c037a3 100644 (file)
@@ -125,3 +125,17 @@ class Tagfile(IntegrationTestCase):
     def test(self):
         self.run_dox2html5(wildcard='indexpage.xml')
         self.assertEqual(*self.actual_expected_contents('index.html'))
+
+class Custom(IntegrationTestCase):
+    def __init__(self, *args, **kwargs):
+        super().__init__(__file__, 'custom', *args, **kwargs)
+
+    def test(self):
+        self.run_dox2html5(wildcard='indexpage.xml')
+        self.assertEqual(*self.actual_expected_contents('index.html'))
+
+    @unittest.skipUnless(shutil.which('latex'),
+                         "Math rendering requires LaTeX installed")
+    def test_math(self):
+        self.run_dox2html5(wildcard='math.xml')
+        self.assertEqual(*self.actual_expected_contents('math.html'))