From: Vladimír Vondruš Date: Thu, 18 Jan 2018 12:48:04 +0000 (+0100) Subject: doxygen: new \m_examplenavigation command. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~cjwatson/git?a=commitdiff_plain;h=b8bf073eaa1c1c93e80e6bbc7c1a6b780a53698b;p=blog.git doxygen: new \m_examplenavigation command. This tests the example page for the first time. So I took the opportunity and also renamed the title suffix to "source" instead of "example". --- diff --git a/doc/doxygen.rst b/doc/doxygen.rst index 6e7bbc25..ee6440a7 100644 --- a/doc/doxygen.rst +++ b/doc/doxygen.rst @@ -615,7 +615,8 @@ following to your ``Doxyfile-mcss``: "m_span{1}=@xmlonly@endxmlonly" \ "m_endspan=@xmlonly@endxmlonly" \ "m_class{1}=@xmlonly@endxmlonly" \ - "m_footernavigation=@xmlonly@endxmlonly" + "m_footernavigation=@xmlonly@endxmlonly" \ + "m_examplenavigation{2}=@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 @@ -629,7 +630,8 @@ aliases however you want to fit your naming scheme. "m_span{1}=" \ "m_endspan=" \ "m_class{1}=" \ - "m_footernavigation=" + "m_footernavigation=" \ + "m_examplenavigation{2}" 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. @@ -686,6 +688,28 @@ present in page detailed description, it will cause the footer of the rendered page to contain a link to previous, parent and next page according to defined page order. +The ``@m_examplenavigation`` command is able to put breadcrumb navigation to +parent page(s) of ``@example`` listings in order to make it easier for users to +return back from example source code to a tutorial page, for example. When used +in combination with ``@m_footernavigation``, navigation to parent page and to +prev/next file of the same example is put at the bottom of the page. The +``@m_examplenavigation`` command takes two arguments, first is the parent page +for this example (used to build the breadcrumb and footer navigation), second +is example path prefix (which is then stripped from page title and is also used +to discover which example files belong together). Example usage --- the +``@m_examplenavigation`` and ``@m_footernavigation`` commands are simply +appended the an existing ``@example`` command. + +.. code-figure:: + + .. code:: c++ + + /** + @example helloworld/CMakeLists.txt @m_examplenavigation{example,helloworld/} @m_footernavigation + @example helloworld/configure.h.cmake @m_examplenavigation{example,helloworld/} @m_footernavigation + @example helloworld/main.cpp @m_examplenavigation{example,helloworld/} @m_footernavigation + */ + `Customizing the template`_ =========================== diff --git a/doxygen/dox2html5.py b/doxygen/dox2html5.py index 7d9d0a61..7fce3351 100755 --- a/doxygen/dox2html5.py +++ b/doxygen/dox2html5.py @@ -58,6 +58,7 @@ class State: def __init__(self): self.basedir = '' self.compounds: Dict[str, Any] = {} + self.examples: List[Any] = [] self.doxyfile: Dict[str, str] = {} self.images: List[str] = [] self.current = '' @@ -144,6 +145,7 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET. out.return_value = None out.add_css_class = None out.footer_navigation = False + out.example_navigation = None # DOXYGEN PATCHING 1/4 # @@ -415,8 +417,9 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET. # resetting here explicitly. add_css_class = parsed.add_css_class - # Bubble up also the footer navigation + # Bubble up also footer / example navigation if parsed.footer_navigation: out.footer_navigation = True + if parsed.example_navigation: out.example_navigation = parsed.example_navigation # Assert we didn't miss anything important assert not parsed.section @@ -611,6 +614,11 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET. elif i.tag == '{http://mcss.mosra.cz/doxygen/}footernavigation': out.footer_navigation = True + # Enabling navigation for an example + elif i.tag == '{http://mcss.mosra.cz/doxygen/}examplenavigation': + out.example_navigation = (i.attrib['{http://mcss.mosra.cz/doxygen/}page'], + i.attrib['{http://mcss.mosra.cz/doxygen/}prefix']) + # Either block or inline elif i.tag == 'programlisting': assert element.tag == 'para' # is inside a paragraph :/ @@ -868,7 +876,7 @@ def parse_toplevel_desc(state: State, element: ET.Element): assert not parsed.return_value if parsed.params: logging.warning("{}: use @tparam instead of @param for documenting class templates, @param is ignored".format(state.current)) - return (parsed.parsed, parsed.templates, parsed.section[2] if parsed.section else '', parsed.footer_navigation) + return (parsed.parsed, parsed.templates, parsed.section[2] if parsed.section else '', parsed.footer_navigation, parsed.example_navigation) def parse_typedef_desc(state: State, element: ET.Element): # Verify that we didn't ignore any important info by accident @@ -1129,15 +1137,24 @@ def parse_define(state: State, element: ET.Element): return define if define.brief or define.has_details else None def extract_metadata(state: State, xml): - # index.xml will be parsed later in parse_index_xml() - if os.path.basename(xml) == 'index.xml': return - logging.debug("Extracting metadata from {}".format(os.path.basename(xml))) tree = ET.parse(xml) root = tree.getroot() + # We need just list of all example files in correct order, nothing else + if os.path.basename(xml) == 'index.xml': + for i in root: + if i.attrib['kind'] == 'example': + compound = Empty() + compound.id = i.attrib['refid'] + compound.url = compound.id + '.html' + compound.name = i.find('name').text + state.examples += [compound] + return + compounddef: ET.Element = root.find('compounddef') + if compounddef.attrib['kind'] not in ['namespace', 'class', 'struct', 'union', 'dir', 'file', 'page']: logging.debug("No useful info in {}, skipping".format(os.path.basename(xml))) return @@ -1274,7 +1291,8 @@ def parse_xml(state: State, xml: str): compound.has_template_details = False compound.templates = None compound.brief = parse_desc(state, compounddef.find('briefdescription')) - compound.description, templates, compound.sections, footer_navigation = parse_toplevel_desc(state, compounddef.find('detaileddescription')) + compound.description, templates, compound.sections, footer_navigation, example_navigation = parse_toplevel_desc(state, compounddef.find('detaileddescription')) + compound.example_navigation = None compound.footer_navigation = None compound.dirs = [] compound.files = [] @@ -1305,7 +1323,8 @@ def parse_xml(state: State, xml: str): compound.has_var_details = False compound.has_define_details = False - # Build breadcrumb + # Build breadcrumb. Breadcrumb for example pages is built after everything + # is parsed. if compound.kind in ['namespace', 'struct', 'class', 'union', 'file', 'dir', 'page']: # Gather parent compounds path_reverse = [compound.id] @@ -1326,7 +1345,7 @@ def parse_xml(state: State, xml: str): if footer_navigation: up = state.compounds[compound.id].parent - # Go through all parent children and + # Go through all parent children and find previous and next if up: up = state.compounds[up] @@ -1668,6 +1687,53 @@ def parse_xml(state: State, xml: str): compound.prefix_wbr += '::' + # Example pages + if compound.kind == 'example': + # Build breadcrumb navigation + if example_navigation: + if not compound.name.startswith(example_navigation[1]): + logging.critical("{}: example filename is not prefixed with {}".format(state.current, example_navigation[1])) + assert False + + prefix_length = len(example_navigation[1]) + + path_reverse = [example_navigation[0]] + while path_reverse[-1] in state.compounds and state.compounds[path_reverse[-1]].parent: + path_reverse += [state.compounds[path_reverse[-1]].parent] + + # Fill breadcrumb with leaf names and URLs + compound.breadcrumb = [] + for i in reversed(path_reverse): + compound.breadcrumb += [(state.compounds[i].leaf_name, state.compounds[i].url)] + + # Add example filename as leaf item + compound.breadcrumb += [(compound.name[prefix_length:], compound.id + '.html')] + + # Enable footer navigation, if requested + if footer_navigation: + up = state.compounds[example_navigation[0]] + + prev = None + next = None + prev_child = None + for example in state.examples: + if example.id == compound.id: + if prev_child: prev = prev_child + elif prev_child and prev_child.id == compound.id: + if example.name.startswith(example_navigation[1]): + next = example + break + + if example.name.startswith(example_navigation[1]): + prev_child = example + + compound.footer_navigation = ((prev.url, prev.name[prefix_length:]) if prev else None, + (up.url, up.name), + (next.url, next.name[prefix_length:]) if next else None) + + else: + compound.breadcrumb = [(compound.name, compound.id + '.html')] + parsed = Empty() parsed.version = root.attrib['version'] diff --git a/doxygen/templates/example.html b/doxygen/templates/example.html index e594db25..53927929 100644 --- a/doxygen/templates/example.html +++ b/doxygen/templates/example.html @@ -1,13 +1,30 @@ {% extends 'base.html' %} -{% block title %}{{ compound.name }} example | {{ super() }}{% endblock %} +{% block title %}{% set j = joiner(' » ') %}{% for name, _ in compound.breadcrumb %}{{ j() }}{{ name }}{% endfor %} source | {{ super() }}{% endblock %} + +{% block header_links %} +{% if compound.footer_navigation and compound.footer_navigation[0] %} + +{% endif %} +{% if compound.footer_navigation and compound.footer_navigation[2] %} + +{% endif %} +{% endblock %} {% block main %} -

{{ compound.name }} example

- {% if compound.brief %} -

{{ compound.brief }}

- {% endif %} -{% if compound.description %} +

+ {% for name, target in compound.breadcrumb[:-1] %} + {{ name }} » + {% endfor %} + {{ compound.breadcrumb[-1][0] }} source +

+ {% if compound.brief %} +

{{ compound.brief }}

+ {% endif %} + {% if compound.description %} {{ compound.description }} -{% endif %} + {% endif %} + {% if compound.footer_navigation %} +
{% if compound.footer_navigation[0] %}« {{ compound.footer_navigation[0][1] }} | {% endif %}{{ compound.footer_navigation[1][1] }}{% if compound.footer_navigation[2] %} | {{ compound.footer_navigation[2][1] }} »{% endif %}
+ {% endif %} {% endblock %} diff --git a/doxygen/test/example/Doxyfile b/doxygen/test/example/Doxyfile new file mode 100644 index 00000000..9bb62610 --- /dev/null +++ b/doxygen/test/example/Doxyfile @@ -0,0 +1,14 @@ +INPUT = input.dox +QUIET = YES +GENERATE_HTML = NO +GENERATE_LATEX = NO +GENERATE_XML = YES +EXAMPLE_PATH = . +ALIASES = \ + "m_footernavigation=@xmlonly@endxmlonly" \ + "m_examplenavigation{2}=@xmlonly@endxmlonly" + +M_PAGE_FINE_PRINT = +M_THEME_COLOR = +M_LINKS_NAVBAR1 = +M_LINKS_NAVBAR2 = diff --git a/doxygen/test/example/input.dox b/doxygen/test/example/input.dox new file mode 100644 index 00000000..507035d5 --- /dev/null +++ b/doxygen/test/example/input.dox @@ -0,0 +1,17 @@ +/** @page page A page + +Go here: @subpage example +*/ + +/** @page example The Example + +See here: + +- @ref path-prefix/CMakeLists.txt +- @ref path-prefix/configure.h.cmake +- @ref path-prefix/main.cpp + +@example path-prefix/CMakeLists.txt @m_examplenavigation{example,path-prefix/} @m_footernavigation +@example path-prefix/configure.h.cmake @m_examplenavigation{example,path-prefix/} @m_footernavigation +@example path-prefix/main.cpp @m_examplenavigation{example,path-prefix/} @m_footernavigation +*/ diff --git a/doxygen/test/example/path-prefix/CMakeLists.txt b/doxygen/test/example/path-prefix/CMakeLists.txt new file mode 100644 index 00000000..2828f304 --- /dev/null +++ b/doxygen/test/example/path-prefix/CMakeLists.txt @@ -0,0 +1,5 @@ +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h) + +add_executable(app main.cpp) +target_include_directories(app ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/doxygen/test/example/path-prefix/configure.h.cmake b/doxygen/test/example/path-prefix/configure.h.cmake new file mode 100644 index 00000000..8f399642 --- /dev/null +++ b/doxygen/test/example/path-prefix/configure.h.cmake @@ -0,0 +1 @@ +#cmakedefine SAY_HELLO diff --git a/doxygen/test/example/path-prefix/main.cpp b/doxygen/test/example/path-prefix/main.cpp new file mode 100644 index 00000000..7ff42a07 --- /dev/null +++ b/doxygen/test/example/path-prefix/main.cpp @@ -0,0 +1,9 @@ +#include + +#include "configure.h" + +int main() { + #ifdef SAY_HELLO + std::cout << "hello?" << std::endl; + #endif +} diff --git a/doxygen/test/example/path-prefix_2CMakeLists_8txt-example.html b/doxygen/test/example/path-prefix_2CMakeLists_8txt-example.html new file mode 100644 index 00000000..9fbf300b --- /dev/null +++ b/doxygen/test/example/path-prefix_2CMakeLists_8txt-example.html @@ -0,0 +1,39 @@ + + + + + A page » The Example » CMakeLists.txt source | My Project + + + + + + +
+
+
+
+
+

+ A page » + The Example » + CMakeLists.txt source +

+
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake
+               ${CMAKE_CURRENT_BINARY_DIR}/configure.h)
+
+add_executable(app main.cpp)
+target_include_directories(app ${CMAKE_CURRENT_BINARY_DIR})
+ +
+
+
+
+ + diff --git a/doxygen/test/example/path-prefix_2configure_8h_8cmake-example.html b/doxygen/test/example/path-prefix_2configure_8h_8cmake-example.html new file mode 100644 index 00000000..59313c0a --- /dev/null +++ b/doxygen/test/example/path-prefix_2configure_8h_8cmake-example.html @@ -0,0 +1,36 @@ + + + + + A page » The Example » configure.h.cmake source | My Project + + + + + + + +
+
+ + diff --git a/doxygen/test/example/path-prefix_2main_8cpp-example.html b/doxygen/test/example/path-prefix_2main_8cpp-example.html new file mode 100644 index 00000000..6ff176b8 --- /dev/null +++ b/doxygen/test/example/path-prefix_2main_8cpp-example.html @@ -0,0 +1,43 @@ + + + + + A page » The Example » main.cpp source | My Project + + + + + + +
+
+ + diff --git a/doxygen/test/test_example.py b/doxygen/test/test_example.py new file mode 100644 index 00000000..3e8bfb07 --- /dev/null +++ b/doxygen/test/test_example.py @@ -0,0 +1,39 @@ +# +# This file is part of m.css. +# +# Copyright © 2017, 2018 Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import unittest + +from distutils.version import LooseVersion + +from test import IntegrationTestCase, doxygen_version + +class Example(IntegrationTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, '', *args, **kwargs) + + def test(self): + self.run_dox2html5(index_pages=[], wildcard='*.xml') + self.assertEqual(*self.actual_expected_contents('path-prefix_2CMakeLists_8txt-example.html')) + self.assertEqual(*self.actual_expected_contents('path-prefix_2configure_8h_8cmake-example.html')) + self.assertEqual(*self.actual_expected_contents('path-prefix_2main_8cpp-example.html'))