chiark / gitweb /
doxygen: rework of the <para> patching number 67.
authorVladimír Vondruš <mosra@centrum.cz>
Wed, 6 Dec 2017 19:17:01 +0000 (20:17 +0100)
committerVladimír Vondruš <mosra@centrum.cz>
Thu, 7 Dec 2017 01:23:41 +0000 (02:23 +0100)
Argh!

 * No longer having <p> around markdown headings (what!), <ul>, <ol>
   and <image> elements.
 * If ba list item, parameter description or return value description
   has multiple paragraphs, they are preserved. Brief description is
   still strictly single paragraph.
 * A special casing of simple nested lists, where item containing a
   sublist shouldn't be wrapped in a <p>.
 * Documented list behavior, explained how to make inflated lists even
   out of single-paragraph items.

doc/doxygen.rst
doxygen/dox2html5.py
doxygen/test/contents_blocks/index.html
doxygen/test/contents_blocks/input.dox
doxygen/test/contents_image/index.html
doxygen/test/contents_image/warnings.html
doxygen/test/contents_typography/index.html
doxygen/test/contents_typography/warnings.html

index d8f5f5309d4de551144414db49c0dca90c7c9b20..2a697f1318a2e44c1a71c3d186bd6ce545e0e89e 100644 (file)
@@ -306,6 +306,53 @@ modifications:
     added after ``::`` and ``_`` in long symbols in link titles and after ``/``
     in URLs.
 
+Single-paragraph list items, function parameter description and return value
+documentation is stripped from the enclosing :html:`<p>` tag to make the output
+more compact. If multiple paragraphs are present, nothing is stripped. In case
+of lists, they are then rendered in an inflated form. However, in order to
+achieve even spacing also with single-paragraph items, it's needed use some
+explicit markup. Adding :html:`<p></p>` to a single-paragraph item will make
+sure the enclosing :html:`<p>` is not stripped.
+
+.. code-figure::
+
+    .. code:: c++
+
+        /**
+        -   A list
+
+            of multiple
+
+            paragraphs.
+
+        -   Another item
+
+            <p></p>
+
+            -   A sub list
+
+                Another paragraph
+        */
+
+    .. raw:: html
+
+        <ul>
+          <li>
+            <p>A list</p>
+            <p>of multiple</p>
+            <p>paragraphs.</p>
+          </li>
+          <li>
+            <p>Another item</p>
+            <ul>
+              <li>
+                <p>A sub list</p>
+                <p>Another paragraph</p>
+              </li>
+            </ul>
+          </li>
+        </ul>
+
 `Pages, sections and table of contents`_
 ========================================
 
index 464a951d4d4e5f1c996a70f0c315fa4910ab79e3..0b975d2745c8170ffd5126ab77763c8cafa4149f 100755 (executable)
@@ -131,44 +131,89 @@ 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, trim = True):
+def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.Element = None, trim = True):
     out = Empty()
-    out.write_start_tag = True
-    out.write_close_tag = True
     out.section = None
     out.templates = {}
     out.params = {}
     out.return_value = None
 
+    # DOXYGEN <PARA> PATCHING 1/5
+    #
+    # In the optimistic case, when parsing the <para> element, the parsed
+    # content is treated as single reasonable paragraph and the caller is told
+    # to write both <p> and </p> enclosing tag.
+    #
+    # Unfortunately Doxygen puts some *block* elements inside a <para> element
+    # instead of closing it before and opening it again after. That is making
+    # me raging mad. Nested paragraphs are no way valid HTML and they are ugly
+    # and problematic in all ways you can imagine, so it's needed to be
+    # patched. See the long ranty comments below for more parts of the story.
+    out.write_paragraph_start_tag = element.tag == 'para'
+    out.write_paragraph_close_tag = element.tag == 'para'
+    out.is_reasonable_paragraph = element.tag == 'para'
+
     out.parsed: str = ''
     if element.text:
         out.parsed = html.escape(element.text.strip() if trim else element.text)
 
+    # Needed later for deciding whether we can strip the surrounding <p> from
+    # the content
+    paragraph_count = 0
+    has_block_elements = False
+
     i: ET.Element
     for i in element:
-        # Doxygen puts the following *block* elements inside a <para> element
-        # instead of closing it before and then opening it again after. Nested
-        # paragraphs are ugly and also not valid HTML, so we have to patch that
-        # up. If there was any content before, we close the paragraph. If there
+        # DOXYGEN <PARA> PATCHING 2/5
+        #
+        # Upon encountering a block element nested in <para>, we need to act.
+        # If there was any content before, we close the paragraph. If there
         # wasn't, we tell the caller to not even open the paragraph. After
-        # processing the following tag, there won't be any paragraph open, so
-        # we also tell the caller that there's no need to close anything.
+        # processing the following tag, there probably won't be any paragraph
+        # open, so we also tell the caller that there's no need to close
+        # anything (but it's not that simple, see for more patching at the end
+        # of the cycle iteration).
+        #
+        # Those elements are:
+        # - <heading>
+        # - <blockquote>
+        # - <simplesect> and <xrefsect>
+        # - <verbatim>
+        # - <variablelist>, <itemizedlist>, <orderedlist>
+        # - <image>
+        # - block <formula>
+        # - <programlisting> (complex block/inline autodetection involved, so
+        #   the check is deferred to later in the loop)
         #
         # Note that <parameterlist> and <simplesect kind="return"> are
         # extracted out of the text flow, so these are removed from this check.
         #
-        # It's not that simple, see for more patching at the end of the cycle
-        # iteration.
-        if (i.tag in ['blockquote', 'xrefsect', 'variablelist', 'verbatim'] or (i.tag == 'simplesect' and i.attrib['kind'] != 'return') or (i.tag == 'formula' and i.text.startswith('\[ ') and i.text.endswith(' \]'))) and element.tag == 'para' and out.write_close_tag:
+        # In addition, there's special handling to achieve things like this:
+        #   <ul>
+        #     <li>A paragraph
+        #       <ul>
+        #         <li>A nested list item</li>
+        #       </ul>
+        #     </li>
+        # I.e., not wrapping "A paragraph" in a <p>, but only if it's
+        # immediately followed by another and it's the first paragraph in a
+        # list item. We check that using the immediate_parent variable.
+        if (i.tag in ['heading', 'blockquote', 'xrefsect', 'variablelist', 'verbatim', 'itemizedlist', 'orderedlist', 'image'] or (i.tag == 'simplesect' and i.attrib['kind'] != 'return') or (i.tag == 'formula' and i.text.startswith('\[ ') and i.text.endswith(' \]'))) and element.tag == 'para' and out.write_paragraph_close_tag:
+            out.is_reasonable_paragraph = False
             out.parsed = out.parsed.rstrip()
             if not out.parsed:
-                out.write_start_tag = False
+                out.write_paragraph_start_tag = False
+            elif immediate_parent and immediate_parent.tag == 'listitem' and i.tag in ['itemizedlist', 'orderedlist']:
+                out.write_paragraph_start_tag = False
             else:
                 out.parsed += '</p>'
-            out.write_close_tag = False
+            out.write_paragraph_close_tag = False
 
         # Block elements
         if i.tag in ['sect1', 'sect2', 'sect3']:
+            assert element.tag != 'para' # should be top-level block element
+            has_block_elements = True
+
             parsed = parse_desc_internal(state, i)
             assert parsed.section
             assert not parsed.templates and not parsed.params and not parsed.return_value
@@ -179,6 +224,9 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
             out.parsed += '<section id="{}">{}</section>'.format(extract_id(i), parsed.parsed)
 
         elif i.tag == 'title':
+            assert element.tag != 'para' # should be top-level block element
+            has_block_elements = True
+
             if element.tag == 'sect1':
                 tag = 'h2'
             elif element.tag == 'sect2':
@@ -196,6 +244,8 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
             out.parsed += '<{0}><a href="#{1}">{2}</a></{0}>'.format(tag, id, title)
 
         elif i.tag == 'heading':
+            has_block_elements = True
+
             if i.attrib['level'] == '1':
                 tag = 'h2'
             elif i.attrib['level'] == '2':
@@ -208,15 +258,31 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
             out.parsed += '<{0}>{1}</{0}>'.format(tag, html.escape(i.text))
 
         elif i.tag == 'para':
+            assert element.tag != 'para' # should be top-level block element
+            paragraph_count += 1
+
+            # DOXYGEN <PARA> PATCHING 3/5
+            #
             # Parse contents of the paragraph, don't trim whitespace around
             # nested elements but trim it at the begin and end of the paragraph
             # itself. Also, some paragraphs are actually block content and we
             # might not want to write the start/closing tag.
-            parsed = parse_desc_internal(state, i, False)
+            #
+            # 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/5 -- 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.parsed = parsed.parsed.strip()
-
-            # Inherit parameter and return value description, assume it's not
-            # scattered all over the place (ugh)
+            if not parsed.is_reasonable_paragraph:
+                has_block_elements = True
+            if parsed.parsed:
+                if parsed.write_paragraph_start_tag: out.parsed += '<p>'
+                out.parsed += parsed.parsed
+                if parsed.write_paragraph_close_tag: out.parsed += '</p>'
             if parsed.templates:
                 assert not out.templates
                 out.templates = parsed.templates
@@ -230,29 +296,20 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
             # Assert we didn't miss anything important
             assert not parsed.section
 
-            # Omit superfluous <p> for simple elments (list items, brief,
-            # parameter and return value description)
-            if element.tag in ['listitem', 'briefdescription', 'parameterdescription'] or (element.tag == 'simplesect' and element.attrib['kind'] == 'return'):
-                # Not expecting any funny thing from there (this will bite back
-                # in the future)
-                assert parsed.write_start_tag and parsed.write_close_tag
-                out.parsed += parsed.parsed
-            # Otherwise behave like requested
-            elif parsed.parsed:
-                if parsed.write_start_tag: out.parsed += '<p>'
-                out.parsed += parsed.parsed
-                if parsed.write_close_tag: out.parsed += '</p>'
-
         elif i.tag == 'blockquote':
+            has_block_elements = True
             out.parsed += '<blockquote>{}</blockquote>'.format(parse_desc(state, i))
 
         elif i.tag == 'itemizedlist':
+            has_block_elements = True
             out.parsed += '<ul>{}</ul>'.format(parse_desc(state, i))
 
         elif i.tag == 'orderedlist':
+            has_block_elements = True
             out.parsed += '<ol>{}</ol>'.format(parse_desc(state, i))
 
         elif i.tag == 'listitem':
+            has_block_elements = True
             out.parsed += '<li>{}</li>'.format(parse_desc(state, i))
 
         elif i.tag == 'simplesect':
@@ -261,6 +318,7 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
                 assert not out.return_value
                 out.return_value = parse_desc(state, i)
             else:
+                has_block_elements = True
                 if i.attrib['kind'] == 'see':
                     out.parsed += '<aside class="m-note m-default"><h4>See also</h4>'
                 elif i.attrib['kind'] == 'note':
@@ -274,6 +332,8 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
                 out.parsed += '</aside>'
 
         elif i.tag == 'xrefsect':
+            has_block_elements = True
+
             id = i.attrib['id']
             match = xref_id_rx.match(id)
             file = match.group(1)
@@ -287,6 +347,8 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
                 color, file, match.group(2), i.find('xreftitle').text, parse_desc(state, i.find('xrefdescription')))
 
         elif i.tag == 'parameterlist':
+            has_block_elements = True
+
             out.param_kind = i.attrib['kind']
             assert out.param_kind in ['param', 'templateparam']
             for param in i:
@@ -304,6 +366,7 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
                     out.templates[name.text] = description
 
         elif i.tag == 'variablelist':
+            has_block_elements = True
             out.parsed += '<dl class="m-dox">'
 
             for var in i:
@@ -315,11 +378,12 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
             out.parsed += '</dl>'
 
         elif i.tag == 'verbatim':
+            has_block_elements = True
             out.parsed += '<pre class="m-code">{}</pre>'.format(html.escape(i.text))
 
         elif i.tag == 'programlisting':
-            # Seems to be a standalone code paragraph, don't wrap it in <p>
-            # and use <pre>:
+            # If it seems to be a standalone code paragraph, don't wrap it in
+            # <p> and use <pre>:
             # - is either alone in the paragraph, with no text or other
             #   elements around
             # - or is a code snippet (filename instead of just .ext). Doxygen
@@ -345,14 +409,17 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
                 if out.parsed and not out.parsed[-1].isspace() and not out.parsed[-1] in '([{':
                     out.parsed += ' '
 
-            # Specialization of similar paragraph cleanup code above
+            # DOXYGEN <PARA> PATCHING 4/5
+            #
+            # Specialization of similar paragraph cleanup code above.
             if code_block:
+                has_block_elements = True
                 out.parsed = out.parsed.rstrip()
                 if not out.parsed:
-                    out.write_start_tag = False
+                    out.write_paragraph_start_tag = False
                 else:
                     out.parsed += '</p>'
-                out.write_close_tag = False
+                out.write_paragraph_close_tag = False
 
             # Hammer unhighlighted code out of the block
             # TODO: preserve links
@@ -438,6 +505,7 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
             out.parsed += '<{0} class="{1}">{2}</{0}>'.format('pre' if code_block else 'code', class_, highlighted)
 
         elif i.tag == 'image':
+            has_block_elements = True
             name = i.attrib['name']
             path = None
 
@@ -471,14 +539,12 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
                 out.parsed += m.math._patch(i.text, rendered, attribs)
 
             # Block formula
-            elif i.text.startswith('\[ ') and i.text.endswith(' \]'):
+            else:
+                assert i.text.startswith('\[ ') and i.text.endswith(' \]')
+                has_block_elements = True
                 rendered = m.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, ''))
 
-            # Shouldn't happen
-            else: # pragma: no cover
-                assert False
-
         # Inline elements
         elif i.tag == 'anchor':
             out.parsed += '<a name="{}"></a>'.format(extract_id(i))
@@ -506,13 +572,15 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
         else: # pragma: no cover
             logging.warning("Ignoring <{}> in desc".format(i.tag))
 
+        # DOXYGEN <PARA> PATCHING 5/5
+        #
         # Besides putting notes and blockquotes and shit inside paragraphs,
         # Doxygen also doesn't attempt to open a new <para> for the ACTUAL NEW
         # PARAGRAPH after they end. So I do it myself and give a hint to the
         # caller that they should close the <p> again.
-        if element.tag == 'para' and not out.write_close_tag and i.tail and i.tail.strip():
+        if element.tag == 'para' and not out.write_paragraph_close_tag and i.tail and i.tail.strip():
             out.parsed += '<p>'
-            out.write_close_tag = True
+            out.write_paragraph_close_tag = True
             # There is usually some whitespace in between, get rid of it as
             # this is a start of a new paragraph. Stripping of the whole thing
             # is done by the caller.
@@ -522,6 +590,20 @@ def parse_desc_internal(state: State, element: ET.Element, trim = True):
         elif i.tail:
             out.parsed += html.escape(i.tail.strip() if trim else i.tail)
 
+    # Brief description always needs to be single paragraph because we're
+    # sending it out without enclosing <p>.
+    if element.tag == 'briefdescription':
+        assert not has_block_elements and paragraph_count <= 1
+        if paragraph_count == 1:
+            assert out.parsed.startswith('<p>') and out.parsed.endswith('</p>')
+            out.parsed = out.parsed[3:-4]
+
+    # Strip superfluous <p> for simple elments (list items, parameter and
+    # return value description), but only if there is just a single paragraph
+    elif (element.tag in ['listitem', 'parameterdescription'] or (element.tag == 'simplesect' and element.attrib['kind'] == 'return')) and not has_block_elements and paragraph_count == 1:
+        assert out.parsed.startswith('<p>') and out.parsed.endswith('</p>')
+        out.parsed = out.parsed[3:-4]
+
     return out
 
 def parse_desc(state: State, element: ET.Element) -> str:
index d8f8b5afe3b64b0c22582a91c9f3124ec25acae7..262d626581fe8da2c2de692eb8d4b20899e6e92c 100644 (file)
@@ -39,7 +39,7 @@
         </h1>
 <p>First paragraph containing some content.</p><aside class="m-note m-warning"><h4>Attention</h4><p>An attention section.</p></aside>
 <aside class="m-note m-default"><h4>See also</h4><p>Other section.</p></aside><p>Paragraph following the sections.</p><aside class="m-note m-info"><h4>Note</h4><p>A note.</p></aside>
-<aside class="m-note m-danger"><h4><a href="bug.html#_bug000001" class="m-dox">Bug</a></h4><p>This is a bug.</p></aside><aside class="m-note m-dim"><h4><a href="todo.html#_todo000001" class="m-dox">Todo</a></h4><p>Or a TODO.</p></aside><aside class="m-note m-danger"><h4><a href="deprecated.html#_deprecated000001" class="m-dox">Deprecated</a></h4><p>Which is deprecated.</p></aside><aside class="m-note m-default"><h4><a href="old.html#_old000001" class="m-dox">Old stuff</a></h4><p>This is old.</p></aside><blockquote><p>A blockquote</p></blockquote><p>Text right after that blockquote should be a new paragraph.</p>
+<aside class="m-note m-danger"><h4><a href="bug.html#_bug000001" class="m-dox">Bug</a></h4><p>This is a bug.</p></aside><aside class="m-note m-dim"><h4><a href="todo.html#_todo000001" class="m-dox">Todo</a></h4><p>Or a TODO.</p></aside><aside class="m-note m-danger"><h4><a href="deprecated.html#_deprecated000001" class="m-dox">Deprecated</a></h4><p>Which is deprecated.</p></aside><aside class="m-note m-default"><h4><a href="old.html#_old000001" class="m-dox">Old stuff</a></h4><p>This is old.</p></aside><blockquote><p>A blockquote</p></blockquote><p>Text right after that blockquote should be a new paragraph.</p><ul><li>A simple</li><li>List<ol><li>With one line</li><li>for each</li></ol></li><li>item, so paragraphs are removed</li></ul><ul><li>A simple</li><li>List<ol><li>With the sublist delimited</li><li>by blank lines</li></ol></li><li>should behave the same as above</li></ul><ul><li><p>A new list</p><p>of multiple</p><p>paragraphs.</p></li><li><p>Another item</p><ul><li><p>A sub list</p><p>Another paragraph</p></li></ul></li></ul><p>A paragraph after that list.</p>
       </div>
     </div>
   </div>
index 3c0fe3cc1c60588dec4c7a17a49d09416a3b80c6..04902cafe553bd6245c67350fe11c221117f7ea6 100644 (file)
@@ -19,6 +19,40 @@ Paragraph following the sections.
 > A blockquote
 Text right after that blockquote should be a new paragraph.
 
+-   A simple
+-   List
+    -#  With one line
+    -#  for each
+-   item, so paragraphs are removed
+
+.
+
+-   A simple
+-   List
+
+    -#  With the sublist delimited
+    -#  by blank lines
+
+-   should behave the same as above
+
+.
+
+-   A new list
+
+    of multiple
+
+    paragraphs.
+
+-   Another item
+
+    <p></p>
+
+    -   A sub list
+
+        Another paragraph
+
+A paragraph after that list.
+
 */
 
 /** @page other Other page
index 795bc183367884259275f844f8800c53c7c61ced..3a0965ec23194e42028bcfb51977034fd12c8612 100644 (file)
@@ -37,7 +37,7 @@
         <h1>
           My Project
         </h1>
-<p><img class="m-image" src="tiny.png" alt="Alt text" /></p>
+<img class="m-image" src="tiny.png" alt="Alt text" />
       </div>
     </div>
   </div>
index 66925e2b8531c14de2ed297d20388dc74d92173c..769ce8031c0e454646fdd8058c9c38ea9650e0c0 100644 (file)
@@ -37,7 +37,7 @@
         <h1>
           Images that produce warnings
         </h1>
-<p><img class="m-image" src="nonexistent.png" alt="Image that doesn&#x27;t exist." /></p><p>Image without alt text:</p><p><img class="m-image" src="tiny.png" alt="Image" /></p>
+<img class="m-image" src="nonexistent.png" alt="Image that doesn&#x27;t exist." /><p>Image without alt text:</p><img class="m-image" src="tiny.png" alt="Image" />
       </div>
     </div>
   </div>
index cc90fdf90f45ba68ea3d1d459847fe12ff7848b5..eef3e86ae32566bcfd5504001cbd9b396960021f 100644 (file)
@@ -38,7 +38,7 @@
           My Project
         </h1>
 <section id="section"><h2><a href="#section">Page section</a></h2><blockquote><p>A blockquote.</p></blockquote><pre class="m-code">Preformatted text.
-</pre><section id="subsection"><h3><a href="#subsection">Page subsection</a></h3><p><ul><li>Unordered</li><li>list</li><li>of<ul><li>nested</li><li>items</li></ul></li><li>and back</li></ul></p><section id="subsubsection"><h4><a href="#subsubsection">Sub-sub section</a></h4><p><ol><li>Ordered</li><li>list</li><li>of<ol><li>nested</li><li>items</li></ol></li><li>and back</li></ol></p><p><a name="an-anchor"></a> This is a <code>typewriter text</code>, <em>emphasis</em> and <strong>bold</strong>. <a href="http://google.com">http:/<wbr/>/<wbr/>google.com</a> and <a href="http://google.com">URL</a>. En-dash &ndash; and em-dash &mdash;. Reference to a <a href="index.html#subsection" class="m-dox">Page subsection</a>.</p></section></section></section>
+</pre><section id="subsection"><h3><a href="#subsection">Page subsection</a></h3><ul><li>Unordered</li><li>list</li><li>of<ul><li>nested</li><li>items</li></ul></li><li>and back</li></ul><section id="subsubsection"><h4><a href="#subsubsection">Sub-sub section</a></h4><ol><li>Ordered</li><li>list</li><li>of<ol><li>nested</li><li>items</li></ol></li><li>and back</li></ol><p><a name="an-anchor"></a> This is a <code>typewriter text</code>, <em>emphasis</em> and <strong>bold</strong>. <a href="http://google.com">http:/<wbr/>/<wbr/>google.com</a> and <a href="http://google.com">URL</a>. En-dash &ndash; and em-dash &mdash;. Reference to a <a href="index.html#subsection" class="m-dox">Page subsection</a>.</p></section></section></section>
       </div>
     </div>
   </div>
index 454f87e29e972e5d198e28ef7f400e6166361662..1bb044b32f545310c0edeae3d2e905e47bb0bf2e 100644 (file)
@@ -37,7 +37,7 @@
         <h1>
           Content that produces warnings
         </h1>
-<p><h2>Markdown heading 1</h2></p><p><h3>Markdown heading 2</h3></p><p><h4>Markdown heading 3</h4></p>
+<h2>Markdown heading 1</h2><h3>Markdown heading 2</h3><h4>Markdown heading 3</h4>
       </div>
     </div>
   </div>