From 0a4e74c1442218445271d706a859c5199a7d76ed Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Thu, 14 Dec 2017 20:48:03 +0100 Subject: [PATCH] doxygen: ability to add custom CSS classes to the output. --- doc/doxygen.rst | 77 +++++++++++++ doxygen/dox2html5.py | 121 +++++++++++++++++--- doxygen/test/contents_custom/Doxyfile | 15 +++ doxygen/test/contents_custom/index.html | 55 +++++++++ doxygen/test/contents_custom/input.dox | 61 ++++++++++ doxygen/test/contents_custom/math.html | 79 +++++++++++++ doxygen/test/contents_custom/ship-small.jpg | 1 + doxygen/test/test_contents.py | 14 +++ 8 files changed, 405 insertions(+), 18 deletions(-) create mode 100644 doxygen/test/contents_custom/Doxyfile create mode 100644 doxygen/test/contents_custom/index.html create mode 100644 doxygen/test/contents_custom/input.dox create mode 100644 doxygen/test/contents_custom/math.html create mode 120000 doxygen/test/contents_custom/ship-small.jpg diff --git a/doc/doxygen.rst b/doc/doxygen.rst index 61877557..0ac78290 100644 --- a/doc/doxygen.rst +++ b/doc/doxygen.rst @@ -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@endxmlonly" \ + "m_enddiv=@xmlonly@endxmlonly" \ + "m_span{1}=@xmlonly@endxmlonly" \ + "m_endspan=@xmlonly@endxmlonly" \ + "m_class{1}=@xmlonly@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:`
` / :html:`` 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`_ =========================== diff --git a/doxygen/dox2html5.py b/doxygen/dox2html5.py index 9feab44a..b6a915a8 100755 --- a/doxygen/dox2html5.py +++ b/doxygen/dox2html5.py @@ -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 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

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. # - # - , , # - , + # - # - (if block) # - (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 # 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 += '

' + 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 += ''.format(' class="{}"'.format(add_css_class) if add_css_class else '') out.parsed += parsed.parsed if parsed.write_paragraph_close_tag: out.parsed += '

' + + # 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 + # 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 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 += '
  • {}
  • '.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 += '
    ' + out.parsed += '
    '.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 += '
    Image
    {}
    '.format(name, html.escape(caption)) + out.parsed += '
    Image
    {}
    '.format( + ' ' + add_css_class if add_css_class else '', + name, html.escape(caption)) else: - out.parsed += 'Image'.format(name) + out.parsed += 'Image'.format( + ' ' + add_css_class if add_css_class else '', name) + + # Custom
    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 += '
    {}
    '.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}'.format('pre' if code_block else 'code', class_, highlighted) + out.parsed += '<{0} class="{1}{2}">{3}'.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 += '
    {}
    '.format(m.math._patch(i.text, rendered, '')) + out.parsed += '
    {}
    '.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 += '{}'.format(parse_inline_desc(state, i)) elif i.tag == 'emphasis': - out.parsed += '{}'.format(parse_inline_desc(state, i)) + out.parsed += '{}'.format( + ' class="{}"'.format(add_inline_css_class) if add_inline_css_class else '', + parse_inline_desc(state, i)) elif i.tag == 'bold': - out.parsed += '{}'.format(parse_inline_desc(state, i)) + out.parsed += '{}'.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 += '{}'.format(html.escape(i.attrib['url']), add_wbr(parse_inline_desc(state, i))) + out.parsed += '{}'.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))) + + # with custom CSS classes + elif i.tag == '{http://mcss.mosra.cz/doxygen/}span': + out.parsed += '{}'.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 += '–' @@ -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 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 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 index 00000000..a902bd3f --- /dev/null +++ b/doxygen/test/contents_custom/Doxyfile @@ -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@endxmlonly" \ + "m_enddiv=@xmlonly@endxmlonly" \ + "m_span{1}=@xmlonly@endxmlonly" \ + "m_endspan=@xmlonly@endxmlonly" \ + "m_class{1}=@xmlonly@endxmlonly" diff --git a/doxygen/test/contents_custom/index.html b/doxygen/test/contents_custom/index.html new file mode 100644 index 00000000..f21a915d --- /dev/null +++ b/doxygen/test/contents_custom/index.html @@ -0,0 +1,55 @@ + + + + + My Project + + + + + + +
    +
    +
    +
    +
    +

    + My Project +

    +
    This paragraph is rendered in a dim note, centered.

    This paragraph contains a red text in a normal text flow and then small strong italics, blue italics and https://mcss.mosra.cz (big-ass link).

    A paragraph that is not affected by the inline classes from above.

    Bold, non-indented paragraph.

    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:

    Image
    Image
    A flat figure

    A fullwidth table:

    Table headerAnother
    CellAnother cell
    Next rowLast.

    An unstyled list:

    • First item without a dot
    • Second item without a dot
    +
    + + + + + + diff --git a/doxygen/test/contents_custom/input.dox b/doxygen/test/contents_custom/input.dox new file mode 100644 index 00000000..d05777b3 --- /dev/null +++ b/doxygen/test/contents_custom/input.dox @@ -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 index 00000000..7dcbc588 --- /dev/null +++ b/doxygen/test/contents_custom/math.html @@ -0,0 +1,79 @@ + + + + + Math | My Project + + + + + + +
    +
    +
    +
    +
    +

    + Math +

    +

    A green formula:

    +LaTeX Math + +\[ \pi^2 \] + + + + + + + + + +

    A yellow +LaTeX Math + +$ \Sigma $ + + + + + + + + inline formula.

    +
    +
    +
    +
    + + + \ 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 index 00000000..dbc21c6b --- /dev/null +++ b/doxygen/test/contents_custom/ship-small.jpg @@ -0,0 +1 @@ +../../../doc/static/ship-small.jpg \ No newline at end of file diff --git a/doxygen/test/test_contents.py b/doxygen/test/test_contents.py index 03fe0f9a..704b295f 100644 --- a/doxygen/test/test_contents.py +++ b/doxygen/test/test_contents.py @@ -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')) -- 2.30.2