chiark / gitweb /
documentation/doxygen: switch to a config supplied from a Python file.
authorVladimír Vondruš <mosra@centrum.cz>
Sun, 7 Jun 2020 15:50:53 +0000 (17:50 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Mon, 8 Jun 2020 17:42:29 +0000 (19:42 +0200)
Keeping full backwards compatibility with the original Doxyfile names,
except for template files which now have to use the new values.

doc/documentation/doxygen.rst
documentation/doxygen.py
documentation/templates/doxygen/annotated.html
documentation/templates/doxygen/base.html
documentation/templates/doxygen/files.html
documentation/templates/doxygen/opensearch.xml
documentation/test_doxygen/__init__.py
documentation/test_doxygen/doxyfile/Doxyfile
documentation/test_doxygen/doxyfile/Doxyfile-legacy [new file with mode: 0644]
documentation/test_doxygen/test_doxyfile.py

index 411fcfacc72ca2a5eb98dbc9ecaa7d38715c6431..9181f38bc5649ef8d5d09dfb211b278d2ac0dfde 100644 (file)
@@ -167,9 +167,9 @@ In addition to features `shared by all doc generators <{filename}/documentation.
     documentation practices --- having the output consist of an actual
     human-written documentation instead of just autogenerated lists. If you
     *really* want to have them included in the output, you can enable them
-    using a default-to-off :ini:`M_SHOW_UNDOCUMENTED` option, but there are
-    some tradeoffs. See `Showing undocumented symbols and files`_ for
-    more information.
+    using a default-to-off :py:`SHOW_UNDOCUMENTED` option, but there are some
+    tradeoffs. See `Showing undocumented symbols and files`_ for more
+    information.
 -   Table of contents is generated for compound references as well, containing
     all sections of detailed description together with anchors to member
     listings
@@ -253,10 +253,8 @@ amount of generated content for no added value.
 `Configuration`_
 ================
 
-The script takes most of the configuration from the ``Doxyfile`` itself,
-(ab)using the following builtin options. The used options are similar to the
-`Python config <{filename}python.rst#configuration>`_, but with Doxygen-imposed
-naming and constraints.
+The script takes a part of the configuration from the ``Doxyfile`` itself,
+(ab)using the following builtin options:
 
 .. class:: m-table m-fullwidth
 
@@ -277,17 +275,6 @@ Variable                        Description
 :ini:`HTML_OUTPUT`              The output will be written here
 :ini:`TAGFILES`                 Used to discover what base URL to prepend to
                                 external references
-:ini:`HTML_EXTRA_STYLESHEET`    List of CSS files to include. Relative paths
-                                are searched relative to the Doxyfile base dir
-                                and to the ``doxygen.py`` script dir as a
-                                fallback. See `Theme selection`_ for more
-                                information.
-:ini:`HTML_EXTRA_FILES`         List of extra files to copy (for example
-                                additional CSS files that are :css:`@import`\ ed
-                                from the primary one). Relative paths are
-                                searched relative to the Doxyfile base dir and
-                                to the ``doxygen.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
@@ -308,107 +295,143 @@ Variable                        Description
                                 actually useful. Doxygen default is ``YES``.
 =============================== ===============================================
 
-In addition, the m.css Doxygen theme recognizes the following extra options:
+On top of the above, the script can take additional options in a way consistent
+with the `Python documentation generator <{filename}python.rst#configuration>`_.
+The recommended and most flexible way is to create a ``conf.py`` file
+referencing the original Doxyfile:
+
+.. code:: py
+
+    DOXYFILE = 'Doxyfile-mcss'
+
+    # additional options from the table below
+
+and then pass that file to the script, instead of the original Doxyfile:
+
+.. code:: sh
+
+    ./doxygen.py path/to/your/conf.py
 
 .. class:: m-table m-fullwidth
 
 =================================== ===========================================
 Variable                            Description
 =================================== ===========================================
-:ini:`M_THEME_COLOR`                Color for :html:`<meta name="theme-color" />`,
-                                    corresponding to the CSS style. If empty,
+:py:`MAIN_PROJECT_URL: str`         If set and :ini:`PROJECT_BRIEF` is also
+                                    set, then :ini:`PROJECT_NAME` in the top
+                                    navbar will link to this URL and
+                                    :ini:`PROJECT_BRIEF` to the documentation
+                                    main page, similarly as `shown here <{filename}/css/page-layout.rst#link-back-to-main-site-from-a-subsite>`_.
+:py:`THEME_COLOR: str`              Color for :html:`<meta name="theme-color" />`,
+                                    corresponding to the CSS style. If not set,
                                     no :html:`<meta>` tag is rendered. See
                                     `Theme selection`_ for more information.
-:ini:`M_FAVICON`                    Favicon URL, used to populate
+:py:`FAVICON: str`                  Favicon URL, used to populate
                                     :html:`<link rel="icon" />`. If empty, no
                                     :html:`<link>` tag is rendered. Relative
-                                    paths are searched relative to the Doxyfile
+                                    paths are searched relative to the
+                                    :abbr:`config file <Doxyfile or conf.py>`
                                     base dir and to the ``doxygen.py`` script
                                     dir as a fallback. See `Theme selection`_
                                     for more information.
-:ini:`M_LINKS_NAVBAR1`              Left navbar column links. See
-                                    `Navbar links`_ for more information.
-:ini:`M_LINKS_NAVBAR2`              Right navbar column links. See
-                                    `Navbar links`_ for more information.
-:ini:`M_MAIN_PROJECT_URL`           If set and :ini:`PROJECT_BRIEF` is also
-                                    set, then :ini:`PROJECT_NAME` in the top
-                                    navbar will link to this URL and
-                                    :ini:`PROJECT_BRIEF` to the documentation
-                                    main page, similarly as `shown here <{filename}/css/page-layout.rst#link-back-to-main-site-from-a-subsite>`_.
-:ini:`M_HTML_HEADER`                HTML code to put at the end of the
+:py:`STYLESHEETS: List[str]`        List of CSS files to include. Relative
+                                    paths are searched relative to the
+                                    :abbr:`config file <Doxyfile or conf.py>`
+                                    base dir and to the ``doxygen.py`` script
+                                    dir as a fallback. See `Theme selection`_
+                                    for more information.
+:py:`HTML_HEADER: str`              HTML code to put at the end of the
                                     :html:`<head>` element. Useful for linking
                                     arbitrary JavaScript code or, for example,
                                     adding :html:`<link>` CSS stylesheets with
                                     additional properties and IDs that are
                                     otherwise not possible with just
                                     :ini:`HTML_EXTRA_STYLESHEET`
-:ini:`M_PAGE_HEADER`                HTML code to put at the top of every page.
+:py:`EXTRA_FILES: List[str]`        List of extra files to copy (for example
+                                    additional CSS files that are :css:`@import`\ ed
+                                    from the primary one). Relative paths are
+                                    searched relative to the
+                                    :abbr:`config file <Doxyfile or conf.py>`
+                                    base dir and to the ``doxygen.py`` script
+                                    dir as a fallback.
+:py:`LINKS_NAVBAR1: List[Any]`      Left navbar column links. See
+                                    `Navbar links`_ for more information.
+:py:`LINKS_NAVBAR2: List[Any]`      Right navbar column links. See
+                                    `Navbar links`_ for more information.
+:py:`PAGE_HEADER: str`              HTML code to put at the top of every page.
                                     Useful for example to link to different
                                     versions of the same documentation. The
                                     ``{filename}`` placeholder is replaced with
                                     current file name.
-:ini:`M_PAGE_FINE_PRINT`            HTML code to put into the footer. If not
+:py:`FINE_PRINT: str`               HTML code to put into the footer. If not
                                     set, a default generic text is used. If
                                     empty, no footer is rendered at all. The
                                     ``{doxygen_version}`` placeholder is
                                     replaced with Doxygen version that
                                     generated the input XML files.
-:ini:`M_CLASS_TREE_EXPAND_LEVELS`   How many levels of the class tree to
-                                    expand. ``0`` means only the top-level
-                                    symbols are shown. If not set, ``1`` is
+:py:`CLASS_INDEX_EXPAND_LEVELS`     How many levels of the class tree to
+                                    expand. :py:`0` means only the top-level
+                                    symbols are shown. If not set, :py:`1` is
                                     used.
-:ini:`M_FILE_TREE_EXPAND_LEVELS`    How many levels of the file tree to expand.
-                                    ``0`` means only the top-level dirs/files
-                                    are shown. If not set, ``1`` is used.
-:ini:`M_EXPAND_INNER_TYPES`         Whether to expand inner types (e.g. a class
+:py:`CLASS_INDEX_EXPAND_INNER`      Whether to expand inner types (e.g. a class
                                     inside a class) in the symbol tree. If not
-                                    set, ``NO`` is used.
-:ini:`M_MATH_CACHE_FILE`            File to cache rendered math formulas. If
-                                    not set, ``m.math.cache`` file in the
-                                    output directory is used. Old cached output
-                                    is periodically pruned and new formulas
-                                    added to the file. Set it empty to disable
-                                    caching.
-:ini:`M_SEARCH_DISABLED`            Disable search functionality. If this
+                                    set, :py:`False` is used.
+:py:`FILE_INDEX_EXPAND_LEVELS`      How many levels of the file tree to expand.
+                                    :py:`0` means only the top-level dirs/files
+                                    are shown. If not set, :py:`1` is used.
+:py:`SEARCH_DISABLED: bool`         Disable search functionality. If this
                                     option is set, no search data is compiled
                                     and the rendered HTML does not contain any
                                     search-related UI or support. If not set,
-                                    ``NO`` is used.
-:ini:`M_SEARCH_DOWNLOAD_BINARY`     Download search data as a binary to save
+                                    :py:`False` is used.
+:py:`SEARCH_DOWNLOAD_BINARY`        Download search data as a binary to save
                                     bandwidth and initial processing time. If
-                                    not set, ``NO`` is used. See
+                                    not set, :py:`False` is used. See
                                     `Search options`_ for more information.
-:ini:`M_SEARCH_HELP`                HTML code to display as help text on empty
+:py:`SEARCH_HELP: str`              HTML code to display as help text on empty
                                     search popup. If not set, a default message
                                     is used. Has effect only if
-                                    :ini:`M_SEARCH_DISABLED` is not ``YES``.
-:ini:`M_SEARCH_BASE_URL`            Base URL for OpenSearch-based search engine
+                                    :py:`SEARCH_DISABLED` is not :py:`True`.
+:py:`SEARCH_BASE_URL: str`          Base URL for OpenSearch-based search engine
                                     suggestions for web browsers. See
                                     `Search options`_ for more information. Has
-                                    effect only if :ini:`M_SEARCH_DISABLED` is
-                                    not ``YES``.
-:ini:`M_SEARCH_EXTERNAL_URL`        URL for external search. The ``{query}``
+                                    effect only if :py:`SEARCH_DISABLED` is
+                                    not :py:`True`.
+:py:`SEARCH_EXTERNAL_URL: str`      URL for external search. The ``{query}``
                                     placeholder is replaced with urlencoded
                                     search string. If not set, no external
                                     search is offered. See `Search options`_
                                     for more information. Has effect only if
-                                    :ini:`M_SEARCH_DISABLED` is not ``YES``.
-:ini:`M_VERSION_LABELS`             Show the ``@since`` annotation as labels
+                                    :py:`SEARCH_DISABLED` is not :py:`True`.
+:py:`VERSION_LABELS: bool`          Show the ``@since`` annotation as labels
                                     visible in entry listing and detailed docs.
-                                    Defaults to ``NO``, see `Version labels`_
+                                    Defaults to :py:`False`, see `Version labels`_
                                     for more information.
-:ini:`M_SHOW_UNDOCUMENTED`          Include undocumented symbols, files and
+:py:`SHOW_UNDOCUMENTED: bool`       Include undocumented symbols, files and
                                     directories in the output. If not set,
-                                    ``NO`` is used. See `Showing undocumented symbols and files`_
+                                    :py:`False` is used. See `Showing undocumented symbols and files`_
                                     for more information.
+:py:`M_MATH_CACHE_FILE`             File to cache rendered math formulas. If
+                                    not set, ``m.math.cache`` file in the
+                                    output directory is used. Old cached output
+                                    is periodically pruned and new formulas
+                                    added to the file. Set it empty to disable
+                                    caching.
 =================================== ===========================================
 
 Note that namespace, directory and page lists are always fully expanded as
 these are not expected to be excessively large.
 
-.. block-success:: Hiding extra options from Doxygen
+.. block-warning:: Legacy configuration through extra Doxyfile options
 
-    Doxygen complains on unknown options, so it's possible to add them
+    Originally, the above options were parsed from the Doxyfile as well, but
+    the Doxyfile format limited the flexibility quite a lot. These are still
+    supported for backwards compatibility and map to the above options as shown
+    below. Integer and string values are parsed as-is, list items are parsed
+    one item per line with ``\`` for line continuations and boolean values have
+    to be either ``YES`` or ``NO``.
+
+    Doxygen complains on unknown options, so as a workaround one can add them
     prefixed with ``##!``. Line continuations are supported too, using ``##!``
     ensures that the options also survive Doxyfile upgrades using
     ``doxygen -u`` (which is not the case when the options would be specified
@@ -419,6 +442,38 @@ these are not expected to be excessively large.
         ##! M_LINKS_NAVBAR1 = pages \
         ##!                   modules
 
+    .. class:: m-table m-fullwidth
+
+    =================================== =======================================
+    Legacy ``Doxyfile`` variable        Corresponding ``conf.py`` variable
+    =================================== =======================================
+    :ini:`HTML_EXTRA_STYLESHEET`        :py:`STYLESHEETS`
+    :ini:`HTML_EXTRA_FILES`             :py:`EXTRA_FILE`
+    :ini:`M_THEME_COLOR`                :py:`THEME_COLOR`
+    :ini:`M_FAVICON`                    :py:`FAVICON`
+    :ini:`M_LINKS_NAVBAR1`              :py:`LINKS_NAVBAR1`. The syntax is
+                                        different in each case, see
+                                        `Navbar links`_ for more information.
+    :ini:`M_LINKS_NAVBAR2`              :py:`LINKS_NAVBAR2`. The syntax is
+                                        different in each case, see
+                                        `Navbar links`_ for more information.
+    :ini:`M_MAIN_PROJECT_URL`           :py:`MAIN_PROJECT_URL`
+    :ini:`M_HTML_HEADER`                :py:`HTML_HEADER`
+    :ini:`M_PAGE_HEADER`                :py:`PAGE_HEADER`
+    :ini:`M_PAGE_FINE_PRINT`            :py:`FINE_PRINT`
+    :ini:`M_CLASS_TREE_EXPAND_LEVELS`   :py:`CLASS_INDEX_EXPAND_LEVELS`
+    :ini:`M_FILE_TREE_EXPAND_LEVELS`    :py:`FILE_INDEX_EXPAND_LEVELS`
+    :ini:`M_EXPAND_INNER_TYPES`         :py:`CLASS_INDEX_EXPAND_INNER`
+    :ini:`M_MATH_CACHE_FILE`            :py:`M_MATH_CACHE_FILE`
+    :ini:`M_SEARCH_DISABLED`            :py:`SEARCH_DISABLED`
+    :ini:`M_SEARCH_DOWNLOAD_BINARY`     :py:`SEARCH_DOWNLOAD_BINARY`
+    :ini:`M_SEARCH_HELP`                :py:`SEARCH_HELP`
+    :ini:`M_SEARCH_BASE_URL`            :py:`SEARCH_BASE_URL`
+    :ini:`M_SEARCH_EXTERNAL_URL`        :py:`SEARCH_EXTERNAL_URL`
+    :ini:`M_VERSION_LABELS`             :py:`VERSION_LABELS`
+    :ini:`M_SHOW_UNDOCUMENTED`          :py:`SHOW_UNDOCUMENTED`
+    =================================== =======================================
+
 `Theme selection`_
 ------------------
 
@@ -426,42 +481,46 @@ By default, the `dark m.css theme <{filename}/css/themes.rst#dark>`_ together
 with documentation-theme-specific additions is used, which corresponds to the
 following configuration:
 
-.. code:: ini
+.. code:: py
     :class: m-console-wrap
 
-    HTML_EXTRA_STYLESHEET = \
-        https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600 \
-        ../css/m-dark+documentation.compiled.css
-    M_THEME_COLOR = #22272e
-    M_FAVICON = favicon-dark.png
+    STYLESHEETS = [
+        'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600',
+        '../css/m-dark+documentation.compiled.css'
+    ]
+    THEME_COLOR = '#22272e'
+    FAVICON = 'favicon-dark.png'
 
 If you have a site already using the ``m-dark.compiled.css`` file, there's
 another file called ``m-dark.documentation.compiled.css``, which contains just
 the documentation-theme-specific additions so you can reuse the already cached
 ``m-dark.compiled.css`` file from your main site:
 
-.. code:: ini
+.. code:: py
     :class: m-console-wrap
 
-    HTML_EXTRA_STYLESHEET = \
-        https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600 \
-        ../css/m-dark.compiled.css \
-        ../css/m-dark.documentation.compiled.css
-    M_THEME_COLOR = #22272e
+    STYLESHEETS = [
+        'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600',
+        '../css/m-dark.compiled.css',
+        '../css/m-dark.documentation.compiled.css'
+    ]
+    THEME_COLOR = '#22272e'
+    FAVICON = 'favicon-dark.png'
 
 If you prefer the `light m.css theme <{filename}/css/themes.rst#light>`_
 instead, use the following configuration (and, similarly, you can use
 ``m-light.compiled.css`` together with ``m-light.documentation.compiled-css``
 in place of ``m-light+documentation.compiled.css``:
 
-.. code:: ini
+.. code:: py
     :class: m-console-wrap
 
-    HTML_EXTRA_STYLESHEET = \
-        https://fonts.googleapis.com/css?family=Libre+Baskerville:400,400i,700,700i%7CSource+Code+Pro:400,400i,600 \
-        ../css/m-light+documentation.compiled.css
-    M_THEME_COLOR = #cb4b16
-    M_FAVICON = favicon-light.png
+    STYLESHEETS = [
+        'https://fonts.googleapis.com/css?family=Libre+Baskerville:400,400i,700,700i%7CSource+Code+Pro:400,400i,600',
+        '../css/m-light+documentation.compiled.css'
+    ]
+    THEME_COLOR = '#cb4b16'
+    FAVICON = 'favicon-light.png'
 
 See the `CSS files`_ section below for more information about customizing the
 CSS files.
@@ -469,64 +528,129 @@ CSS files.
 `Navbar links`_
 ---------------
 
-The :ini:`M_LINKS_NAVBAR1` and :ini:`M_LINKS_NAVBAR2` options define which
-links are shown on the top navbar, split into left and right column on small
-screen sizes. These options take a whitespace-separated list of compound IDs
-and additionally the special ``pages``, ``modules``, ``namespaces``,
-``annotated``, ``files`` IDs. By default the variables are defined like
-following:
+The :py:`LINKS_NAVBAR1` and :py:`LINKS_NAVBAR2` options define which links are
+shown on the top navbar, split into left and right column on small screen
+sizes. These options take a list of :py:`(title, path, sub)` tuples ---
+``title`` is the link title; ``path`` is either one of the :py:`'pages'`,
+:py:`'modules'`, :py:`'namespaces'`, :py:`'annotated'`, :py:`'files'` IDs or a
+compound ID; and ``sub`` is an optional submenu, containing :py:`(title, path)`
+with ``path`` being interpreted the same way.
 
-.. code:: ini
+By default the variables are defined like following --- two items in the left
+column, two items in the right columns, with no submenus:
 
-    M_LINKS_NAVBAR1 = pages namespaces
-    M_LINKS_NAVBAR2 = annotated files
+.. code:: py
+
+    LINKS_NAVBAR1 = [
+        ("Pages", 'pages', []),
+        ("Namespaces", 'namespaces', [])
+    ]
+    'LINKS_NAVBAR2' = [
+        ("Classes", 'annotated', []),
+        ("Files", 'files', [])
+    ]
 
 .. note-info::
 
     The theme by default assumes that the project is grouping symbols in
     namespaces. If you use modules (``@addtogroup`` and related commands) and
     you want to show their index in the navbar, add ``modules`` to one of
-    the :ini:`M_LINKS_NAVBAR*` options, for example:
+    the :py:`LINKS_NAVBAR*` options, for example:
 
-    .. code:: ini
+    .. code:: py
 
-        M_LINKS_NAVBAR1 = pages modules
-        M_LINKS_NAVBAR2 = annotated files
+        LINKS_NAVBAR1 = [
+            ("Pages", 'pages', []),
+            ("Modules", 'modules', [])
+        ]
+        LINKS_NAVBAR2 = [
+            ("Classes", 'annotated', []),
+            ("Files", 'files', [])
+        ]
 
-Titles for the links are taken implicitly. Empty :ini:`M_LINKS_NAVBAR2` will
-cause the navigation appear in a single column, setting both empty will cause
-the navbar links to not be rendered at all.
+If the title is :py:`None`, it's taken implicitly from the page it links to.
+Empty :py:`LINKS_NAVBAR2` will cause the navigation appear in a single column,
+setting both empty will cause the navbar links to not be rendered at all.
 
 A menu item is higlighted if a compound with the same ID is the current page
 (and similarly for the special ``pages``, ... IDs).
 
-It's possible to specify sub-menu items by enclosing more than one ID in
-quotes. The top-level items then have to be specified each on a single line.
-Example (note the mangled names, corresponding to filenames of given compounds
-generated by Doxygen):
+Alternatively, a link can be a plain HTML instead of the first pair of tuple
+values, in which case it's put into the navbar as-is. It's not limited to just
+a text, but it can contain an image, embedded SVG or anything else. A complex
+example including submenus follows --- the first navbar column will have links
+to namespaces *Foo*, *Bar* and *Utils* as a sub-items of a top-level
+*Namespaces* item and links to two subdirectories as sub-items of the *Files*
+item, with title being :py:`None` to have it automatically filled in by
+Doxygen. The second column has two top-level items, first linking to the page
+index and having a submenu linking to an e-mail address and a ``fine-print``
+page and the second linking to a GitHub project page. Note the mangled names,
+corresponding to filenames of given compounds generated by Doxygen.
+
+.. code:: py
+
+    LINKS_NAVBAR1 = [
+        (None, 'namespaces', [
+            (None, 'namespaceFoo'),
+            (None, 'namespaceBar'),
+            (None, 'namespaceUtils'),
+        ]),
+        (None, 'files', [
+            (None, 'dir_d3b07384d113edec49eaa6238ad5ff00'),
+            (None, 'dir_cbd8f7984c654c25512e3d9241ae569f')
+        ])
+    ]
+    LINKS_NAVBAR2 = [
+        ("Pages", 'pages', [
+            ("<a href=\"mailto:mosra@centrum.cz\">Contact</a>", ),
+            ("Fine print", 'fine-print')
+        ]),
+        ("<a href=\"https://github.com/mosra/m.css\">GitHub</a>", [])
+    ]
+
+.. block-warning:: Legacy navbar link specification in the Doxyfile
+
+    For backwards compatibility, the :ini:`M_LINKS_NAVBAR1` and
+    :ini:`M_LINKS_NAVBAR2` options in the Doxyfile are recognized. Compared to
+    the Python variant above, the encoding is a bit more complicated --- these
+    options take a whitespace-separated list of compound IDs and additionally
+    the special ``pages``, ``modules``, ``namespaces``, ``annotated``,
+    ``files`` IDs. An equivalent to the above default Python config would be
+    the following --- titles for the links are always taken implicitly in this
+    case:
 
-.. code:: ini
+    .. code:: ini
 
-    M_LINKS_NAVBAR1 = \
-        "namespaces namespaceFoo namespaceBar namespaceUtils" \
-        "files dir_d3b07384d113edec49eaa6238ad5ff00 dir_cbd8f7984c654c25512e3d9241ae569f"
+        M_LINKS_NAVBAR1 = pages namespaces
+        M_LINKS_NAVBAR2 = annotated files
+
+    It's possible to specify sub-menu items by enclosing more than one ID in
+    quotes. The top-level items then have to be specified each on a single
+    line. Example (note the mangled names, corresponding to filenames of given
+    compounds generated by Doxygen):
 
-This will put links to namespaces Foo, Bar and Utils as a sub-items of a
-top-level *Namespaces* item and links to two subdirectories as sub-items of the
-*Files* item.
+    .. code:: ini
 
-For custom links in the navbar it's possible to use HTML code directly, both
-for a top-level item or in a submenu. The item is taken as everything from the
-initial :html:`<a` to the first closing :html:`</a>`. In the following snippet,
-there are two top-level items, first linking to the page index and having a
-submenu linking to an e-mail address and a ``fine-print`` page and the second
-linking to a GitHub project page:
+        M_LINKS_NAVBAR1 = \
+            "namespaces namespaceFoo namespaceBar namespaceUtils" \
+            "files dir_d3b07384d113edec49eaa6238ad5ff00 dir_cbd8f7984c654c25512e3d9241ae569f"
 
-.. code:: ini
+    This will put links to namespaces Foo, Bar and Utils as a sub-items of a
+    top-level *Namespaces* item and links to two subdirectories as sub-items of
+    the *Files* item.
+
+    For custom links in the navbar it's possible to use HTML code directly,
+    both for a top-level item or in a submenu. The item is taken as everything
+    from the initial :html:`<a` to the first closing :html:`</a>`. In the
+    following snippet, there are two top-level items, first linking to the page
+    index and having a submenu linking to an e-mail address and a
+    ``fine-print`` page and the second linking to a GitHub project page:
+
+    .. code:: ini
 
-    M_LINKS_NAVBAR2 = \
-        "pages <a href=\"mailto:mosra@centrum.cz\">Contact</a> fine-print" \
-        "<a href=\"https://github.com/mosra/m.css\">GitHub</a>"
+        M_LINKS_NAVBAR2 = \
+            "pages <a href=\"mailto:mosra@centrum.cz\">Contact</a> fine-print" \
+            "<a href=\"https://github.com/mosra/m.css\">GitHub</a>"
 
 `Search options`_
 -----------------
@@ -543,17 +667,17 @@ Base85-encoded representation of the search binary and loading that
 asynchronously as a plain JavaScript file. This results in the search data
 being 25% larger, but since this is for serving from a local filesystem, it's
 not considered a problem. If your docs are accessed through a server (or you
-don't need Chrome support), enable the :ini:`M_SEARCH_DOWNLOAD_BINARY` option.
+don't need Chrome support), enable the :py:`SEARCH_DOWNLOAD_BINARY` option.
 
 The site can provide search engine metadata using the `OpenSearch <http://www.opensearch.org/>`_
 specification. On supported browsers this means you can add the search field to
 search engines and search directly from the address bar. To enable search
-engine metadata, point :ini:`M_SEARCH_BASE_URL` to base URL of your
-documentation, for example:
+engine metadata, point :py:`SEARCH_BASE_URL` to base URL of your documentation,
+for example:
 
-.. code:: ini
+.. code:: py
 
-    M_SEARCH_BASE_URL = "https://doc.magnum.graphics/magnum/"
+    SEARCH_BASE_URL = "https://doc.magnum.graphics/magnum/"
 
 In general, even without the above setting, appending ``?q={query}#search`` to
 the URL will directly open the search popup with results for ``{query}``.
@@ -564,16 +688,16 @@ the URL will directly open the search popup with results for ``{query}``.
     directly in the browser address bar. However that requires a server-side
     search implementation and is not supported at the moment.
 
-If :ini:`M_SEARCH_EXTERNAL_URL` is specified, full-text search using an
-external search engine is offered if nothing is found for given string or if
-the user has JavaScript disabled. It's recommended to restrict the search to
-a particular domain or add additional keywords to the search query to filter
-out irrelevant results. Example, using Google search engine and restricting
-the search to a subdomain:
+If :py:`SEARCH_EXTERNAL_URL` is specified, full-text search using an external
+search engine is offered if nothing is found for given string or if the user
+has JavaScript disabled. It's recommended to restrict the search to a
+particular domain or add additional keywords to the search query to filter out
+irrelevant results. Example, using Google search engine and restricting the
+search to a subdomain:
 
-.. code:: ini
+.. code:: py
 
-    M_SEARCH_EXTERNAL_URL = "https://google.com/search?q=site:doc.magnum.graphics+{query}"
+    SEARCH_EXTERNAL_URL = "https://google.com/search?q=site:doc.magnum.graphics+{query}"
 
 `Showing undocumented symbols and files`_
 -----------------------------------------
@@ -586,21 +710,21 @@ shown.
 In some cases, however, it might be desirable to show undocumented symbols as
 well --- for example when converting an existing project from vanilla Doxygen
 to m.css, not all APIs might be documented yet and thus the output would be
-incomplete. The :ini:`M_SHOW_UNDOCUMENTED` option unconditionally makes all
+incomplete. The :py:`SHOW_UNDOCUMENTED` option unconditionally makes all
 undocumented symbols, files and directories "appear documented". Note, however,
 that Doxygen itself doesn't allow to link to undocumented symbols and so even
 though the undocumented symbols are present in the output, nothing is able to
 reference them, causing very questionable usability of such approach. A
-potential "fix" to this is enabling the :ini:`EXTRACT_ALL` option, but that
-exposes all symbols, including private and file-local ones --- which, most
+potential "fix" to this is enabling the :ini:`EXTRACT_ALL` Doxyfile option, but
+that exposes all symbols, including private and file-local ones --- which, most
 probably, is *not* what you want.
 
 If you have namespaces not documented, Doxygen will by put function docs into
 file pages --- but it doesn't put them into the XML output, meaning all links
 to them will lead nowhere and the functions won't appear in search either.
-To fix this, enable the :ini:`XML_NS_MEMB_FILE_SCOPE` option as described in
-the `Namespace members in file scope`_ section below; if you document all
-namespaces this problem will go away as well.
+To fix this, enable the :ini:`XML_NS_MEMB_FILE_SCOPE` Doxyfile option as
+described in the `Namespace members in file scope`_ section below; if you
+document all namespaces this problem will go away as well.
 
 `Content`_
 ==========
@@ -705,7 +829,7 @@ Table of contents for pages is generated only if they specify
 
 Doxygen by default doesn't render namespace members for file documentation in
 its XML output. To match the behavior of stock HTML output, enable the
-:ini:`XML_NS_MEMB_FILE_SCOPE` option:
+:ini:`XML_NS_MEMB_FILE_SCOPE` Doxyfile option:
 
 .. code:: ini
 
@@ -727,10 +851,10 @@ still need to be patched (or worked around).
 Private virtual functions, if documented, are shown in the output as well, so
 codebases can properly follow (`Virtuality guidelines by Herb Sutter <http://www.gotw.ca/publications/mill18.htm>`_)
 To avoid also undocumented :cpp:`override`\ s showing in the output, you may
-want to disable the :ini:`INHERIT_DOCS` option (which is enabled by default).
-Also, please note that while privates are currently unconditionally exported to
-the XML output, Doxygen doesn't allow linking to them by default and you have
-to enable the :ini:`EXTRACT_PRIV_VIRTUAL` option:
+want to disable the :ini:`INHERIT_DOCS` Doxyfile option (which is enabled by
+default). Also, please note that while privates are currently unconditionally
+exported to the XML output, Doxygen doesn't allow linking to them by default
+and you have to enable the :ini:`EXTRACT_PRIV_VIRTUAL` Doxyfile option:
 
 .. code:: ini
 
@@ -764,8 +888,8 @@ typedefs, variables and :cpp:`#define`\ s. The rules are:
     to some class, these also have the :cpp:`#include` shown in case it's
     different from the class :cpp:`include`.
 
-This feature is enabled by default, disable :ini:`SHOW_INCLUDE_FILES` to hide
-all :cpp:`#include`-related information:
+This feature is enabled by default, disable :ini:`SHOW_INCLUDE_FILES` in the
+Doxyfile to hide all :cpp:`#include`-related information:
 
 .. code:: ini
 
@@ -1224,12 +1348,19 @@ label rendering and  For example (the ``@m_class`` is the same as described in
 
 .. code:: ini
 
-    M_VERSION_LABELS = YES
     ALIASES = \
         "m_class{1}=@xmlonly<mcss:class xmlns:mcss=\"http://mcss.mosra.cz/doxygen/\" mcss:class=\"\1\" />@endxmlonly" \
         "m_since{2}=@since @m_class{m-label m-success m-flat} @ref changelog-\1-\2 \"since v\1.\2\"" \
         "m_deprecated_since{2}=@since deprecated in v\1.\2 @deprecated"
 
+.. class:: m-noindent
+
+in the Doxyfile, and the following in ``conf.py``:
+
+.. code:: py
+
+    VERSION_LABELS = True
+
 With the above configuration, the following markup will render a
 :label-flat-success:`since v1.3` label leading to a page named ``changelog-1-3``
 next to both function entry and detailed docs in the first case, and a
@@ -1264,11 +1395,11 @@ next to both function entry and detailed docs in the first case, and a
                  [--search-no-lookahead-barriers]
                  [--search-no-prefix-merging] [--sort-globbed-files]
                  [--debug]
-                 doxyfile
+                 config
 
 Arguments:
 
--   ``doxyfile`` --- where the Doxyfile is
+-   ``config`` --- where the Doxyfile or conf.py is
 
 Options:
 
@@ -1405,18 +1536,20 @@ is an example configuration corresponding to the dark theme:
 
 .. code:: ini
 
-    HTML_EXTRA_STYLESHEET = \
-        https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600 \
-        ../css/m-dark.css \
-        ../css/m-documentation.css
-    HTML_EXTRA_FILES = \
-        ../css/m-theme-dark.css \
-        ../css/m-grid.css \
-        ../css/m-components.css \
-        ../css/m-layout.css \
-        ../css/pygments-dark.css \
-        ../css/pygments-console.css
-    M_THEME_COLOR = #22272e
+    STYLESHEETS = [
+        'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600',
+        '../css/m-dark.css',
+        '../css/m-documentation.css'
+    ]
+    EXTRA_FILES = [
+        '../css/m-theme-dark.css',
+        '../css/m-grid.css',
+        '../css/m-components.css',
+        '../css/m-layout.css',
+        '../css/pygments-dark.css',
+        '../css/pygments-console.css'
+    ]
+    THEME_COLOR = '#22272e'
 
 After making desired changes to the source files, it's possible to postprocess
 them back to the compiled version using the ``postprocess.py`` utility as
@@ -1461,19 +1594,20 @@ Filename                Use
                         as ``union*.html``
 ======================= =======================================================
 
-Each template is passed a subset of the ``Doxyfile`` configuration values from
-the `Configuration`_ table. Most values are provided as-is depending on their
-type, so either strings, booleans, or lists of strings. The exceptions are:
-
--   The :py:`M_LINKS_NAVBAR1` and :py:`M_LINKS_NAVBAR2` are processed to tuples
-    in a form :py:`(html, title, url, id, sub)` where either :py:`html` is a
-    full HTML code for the link and :py:`title`, :py:`url` :py:`id` is empty;
-    or :py:`html` is :py:`None`, :py:`title` and :py:`url` is a link title and
-    URL and :py:`id` is compound ID (to use for highlighting active menu item).
-    The last item, :py:`sub` is a list optionally containing sub-menu items.
-    The sub-menu items are in a similarly formed tuple,
+Each template is passed a subset of the ``Doxyfile`` and ``conf.py``
+configuration values from the `Configuration`_ tables. Most values are provided
+as-is depending on their type, so either strings, booleans, or lists of
+strings. The exceptions are:
+
+-   The :py:`LINKS_NAVBAR1` and :py:`LINKS_NAVBAR2` are processed to tuples in
+    a form :py:`(html, title, url, id, sub)` where either :py:`html` is a full
+    HTML code for the link and :py:`title`, :py:`url` :py:`id` is empty; or
+    :py:`html` is :py:`None`, :py:`title` and :py:`url` is a link title and URL
+    and :py:`id` is compound ID (to use for highlighting active menu item). The
+    last item, :py:`sub` is a list optionally containing sub-menu items. The
+    sub-menu items are in a similarly formed tuple,
     :py:`(html, title, url, id)`.
--   The :py:`M_FAVICON` is converted to a tuple of :py:`(url, type)` where
+-   The :py:`FAVICON` is converted to a tuple of :py:`(url, type)` where
     :py:`url` is the favicon URL and :py:`type` is favicon MIME type to
     populate the ``type`` attribute of :html:`<link rel="favicon" />`.
 
@@ -1496,7 +1630,7 @@ argument is an absolute URL. It's useful in cases like this:
 
 .. code:: html+jinja
 
-  {% for css in HTML_EXTRA_STYLESHEET %}
+  {% for css in STYLESHEETS %}
   <link rel="stylesheet" href="{{ css|basename_or_url }}" />
   {% endfor %}
 
@@ -2161,10 +2295,10 @@ Filename                Use
 By default it's those five pages, but you can configure any other pages via the
 ``--index-pages`` option as mentioned in the `Command-line options`_ section.
 
-Each template is passed a subset of the ``Doxyfile`` configuration values from
-the above table and in addition the :py:`FILENAME` and :py:`DOXYGEN_VERSION`
-variables as above. The navigation tree is provided in an :py:`index` object,
-which has the following properties:
+Each template is passed a subset of the ``Doxyfile`` and ``conf.py``
+configuration values from the `Configuration`_ tables and in addition the
+:py:`FILENAME` and :py:`DOXYGEN_VERSION` variables as above. The navigation
+tree is provided in an :py:`index` object, which has the following properties:
 
 .. class:: m-table m-fullwidth
 
index 8ac8354a24a0f90adc75c42726c1d0b44d78274c..92a4886fd614f540826e7e7d556358f07afd11f0 100755 (executable)
@@ -31,6 +31,7 @@ import enum
 import sys
 import re
 import html
+import inspect
 import os
 import glob
 import mimetypes
@@ -41,8 +42,8 @@ import logging
 from types import SimpleNamespace as Empty
 from typing import Tuple, Dict, Any, List
 
+from importlib.machinery import SourceFileLoader
 from jinja2 import Environment, FileSystemLoader
-
 from pygments import highlight
 from pygments.formatters import HtmlFormatter
 from pygments.lexers import TextLexer, BashSessionLexer, get_lexer_by_name, find_lexer_class_for_filename
@@ -91,6 +92,56 @@ search_type_map = [
     (CssClass.DEFAULT, "var")
 ]
 
+default_config = {
+    'DOXYFILE': 'Doxyfile',
+
+    'THEME_COLOR': '#22272e',
+    'FAVICON': 'favicon-dark.png',
+    'LINKS_NAVBAR1': [
+        ("Pages", 'pages', []),
+        ("Namespaces", 'namespaces', [])
+    ],
+    'LINKS_NAVBAR2': [
+        ("Classes", 'annotated', []),
+        ("Files", 'files', [])
+    ],
+
+    'STYLESHEETS': [
+        'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600',
+        '../css/m-dark+documentation.compiled.css'],
+    'HTML_HEADER': None,
+    'EXTRA_FILES': [],
+    'PAGE_HEADER': None,
+    'FINE_PRINT': '[default]',
+
+    'CLASS_INDEX_EXPAND_LEVELS': 1,
+    'FILE_INDEX_EXPAND_LEVELS': 1,
+    'CLASS_INDEX_EXPAND_INNER': False,
+
+    'M_MATH_CACHE_FILE': 'm.math.cache',
+
+    'SEARCH_DISABLED': False,
+    'SEARCH_DOWNLOAD_BINARY': False,
+    'SEARCH_HELP':
+"""<p class="m-noindent">Search for symbols, directories, files, pages or
+modules. You can omit any prefix from the symbol or file path; adding a
+<code>:</code> or <code>/</code> suffix lists all members of given symbol or
+directory.</p>
+<p class="m-noindent">Use <span class="m-label m-dim">&darr;</span>
+/ <span class="m-label m-dim">&uarr;</span> to navigate through the list,
+<span class="m-label m-dim">Enter</span> to go.
+<span class="m-label m-dim">Tab</span> autocompletes common prefix, you can
+copy a link to the result using <span class="m-label m-dim">⌘</span>
+<span class="m-label m-dim">L</span> while <span class="m-label m-dim">⌘</span>
+<span class="m-label m-dim">M</span> produces a Markdown link.</p>
+""",
+    'SEARCH_BASE_URL': None,
+    'SEARCH_EXTERNAL_URL': None,
+
+    'SHOW_UNDOCUMENTED': False,
+    'VERSION_LABELS': False
+}
+
 xref_id_rx = re.compile(r"""(.*)_1(_[a-z-]+[0-9]+|@)$""")
 slugify_nonalnum_rx = re.compile(r"""[^\w\s-]""")
 slugify_hyphens_rx = re.compile(r"""[-\s]+""")
@@ -109,13 +160,14 @@ class StateCompound:
         self.parent: str = None
 
 class State:
-    def __init__(self):
+    def __init__(self, config):
         self.basedir = ''
         self.compounds: Dict[str, StateCompound] = {}
         self.includes: Dict[str, str] = {}
         self.search: List[Any] = []
         self.examples: List[Any] = []
-        self.doxyfile: Dict[str, str] = {}
+        self.doxyfile: Dict[str, Any] = {}
+        self.config: Dict[str, Any] = config
         self.images: List[str] = []
         self.current = '' # current file being processed (for logging)
         # Current kind of compound being processed. Affects current_include
@@ -758,7 +810,7 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
 
             # Content of @since tags is put as-is into entry description /
             # details, if enabled.
-            elif i.attrib['kind'] == 'since' and state.doxyfile['M_VERSION_LABELS']:
+            elif i.attrib['kind'] == 'since' and state.config['VERSION_LABELS']:
                 since = parse_inline_desc(state, i).strip()
                 assert since.startswith('<p>') and since.endswith('</p>')
                 out.since = since[3:-4]
@@ -1733,7 +1785,7 @@ def parse_enum(state: State, element: ET.Element):
         value.brief = parse_desc(state, enumvalue.find('briefdescription'))
         value.description, value_search_keywords, value.deprecated, value.since = parse_enum_value_desc(state, enumvalue)
         if value.brief or value.description:
-            if enum.base_url == state.current_compound_url and not state.doxyfile['M_SEARCH_DISABLED']:
+            if enum.base_url == state.current_compound_url and not state.config['SEARCH_DISABLED']:
                 result = Empty()
                 result.flags = ResultFlag.from_type(ResultFlag.DEPRECATED if value.deprecated else ResultFlag(0), EntryType.ENUM_VALUE)
                 result.url = enum.base_url + '#' + value.id
@@ -1745,12 +1797,11 @@ def parse_enum(state: State, element: ET.Element):
                 state.search += [result]
 
             # If either brief or description for this value is present, we want
-            # to show the detailed enum docs. However, in
-            # case M_SHOW_UNDOCUMENTED is enabled, the values might have just
-            # a dummy <span></span> content in order to make them "appear
-            # documented". Then it doesn't make sense to repeat the same list
-            # twice.
-            if not state.doxyfile['M_SHOW_UNDOCUMENTED'] or value.brief != '<span></span>':
+            # to show the detailed enum docs. However, in case
+            # SHOW_UNDOCUMENTED is enabled, the values might have just a dummy
+            # <span></span> content in order to make them "appear documented".
+            # Then it doesn't make sense to repeat the same list twice.
+            if not state.config['SHOW_UNDOCUMENTED'] or value.brief != '<span></span>':
                 enum.has_value_details = True
 
         enum.values += [value]
@@ -1758,7 +1809,7 @@ def parse_enum(state: State, element: ET.Element):
     if enum.base_url == state.current_compound_url and (enum.description or enum.has_value_details):
         enum.has_details = True # has_details might already be True from above
     if enum.brief or enum.has_details or enum.has_value_details:
-        if enum.base_url == state.current_compound_url and not state.doxyfile['M_SEARCH_DISABLED']:
+        if enum.base_url == state.current_compound_url and not state.config['SEARCH_DISABLED']:
             result = Empty()
             result.flags = ResultFlag.from_type(ResultFlag.DEPRECATED if enum.deprecated else ResultFlag(0), EntryType.ENUM)
             result.url = enum.base_url + '#' + enum.id
@@ -1834,7 +1885,7 @@ def parse_typedef(state: State, element: ET.Element):
         typedef.has_details = True # has_details might already be True from above
     if typedef.brief or typedef.has_details:
         # Avoid duplicates in search
-        if typedef.base_url == state.current_compound_url and not state.doxyfile['M_SEARCH_DISABLED']:
+        if typedef.base_url == state.current_compound_url and not state.config['SEARCH_DISABLED']:
             result = Empty()
             result.flags = ResultFlag.from_type(ResultFlag.DEPRECATED if typedef.deprecated else ResultFlag(0), EntryType.TYPEDEF)
             result.url = typedef.base_url + '#' + typedef.id
@@ -1984,7 +2035,7 @@ def parse_func(state: State, element: ET.Element):
         func.has_details = True # has_details might already be True from above
     if func.brief or func.has_details:
         # Avoid duplicates in search
-        if func.base_url == state.current_compound_url and not state.doxyfile['M_SEARCH_DISABLED']:
+        if func.base_url == state.current_compound_url and not state.config['SEARCH_DISABLED']:
             result = Empty()
             result.flags = ResultFlag.from_type((ResultFlag.DEPRECATED if func.deprecated else ResultFlag(0))|(ResultFlag.DELETED if func.is_deleted else ResultFlag(0)), EntryType.FUNC)
             result.url = func.base_url + '#' + func.id
@@ -2020,7 +2071,7 @@ def parse_var(state: State, element: ET.Element):
         var.has_details = True # has_details might already be True from above
     if var.brief or var.has_details:
         # Avoid duplicates in search
-        if var.base_url == state.current_compound_url and not state.doxyfile['M_SEARCH_DISABLED']:
+        if var.base_url == state.current_compound_url and not state.config['SEARCH_DISABLED']:
             result = Empty()
             result.flags = ResultFlag.from_type(ResultFlag.DEPRECATED if var.deprecated else ResultFlag(0), EntryType.VAR)
             result.url = var.base_url + '#' + var.id
@@ -2060,7 +2111,7 @@ def parse_define(state: State, element: ET.Element):
         define.has_details = True # has_details might already be True from above
     if define.brief or define.has_details:
         # Avoid duplicates in search
-        if define.base_url == state.current_compound_url and not state.doxyfile['M_SEARCH_DISABLED']:
+        if define.base_url == state.current_compound_url and not state.config['SEARCH_DISABLED']:
             result = Empty()
             result.flags = ResultFlag.from_type(ResultFlag.DEPRECATED if define.deprecated else ResultFlag(0), EntryType.DEFINE)
             result.url = define.base_url + '#' + define.id
@@ -2128,7 +2179,7 @@ def extract_metadata(state: State, xml):
 
     # In order to show also undocumented members, go through all empty
     # <briefdescription>s and fill them with a generic text.
-    if state.doxyfile['M_SHOW_UNDOCUMENTED']:
+    if state.config['SHOW_UNDOCUMENTED']:
         _document_all_stuff(compounddef)
 
     compound = StateCompound()
@@ -2153,7 +2204,7 @@ def extract_metadata(state: State, xml):
     # @deprecated, treat it as version in which given feature was deprecated
     compound.deprecated = None
     compound.since = None
-    if state.doxyfile['M_VERSION_LABELS']:
+    if state.config['VERSION_LABELS']:
         for i in compounddef.find('detaileddescription').findall('.//simplesect'):
             if i.attrib['kind'] != 'since': continue
             since = parse_inline_desc(state, i).strip()
@@ -2259,64 +2310,21 @@ def postprocess_state(state: State):
 
             state.includes['/'.join(include)] = compound.id
 
-    # Assign names and URLs to menu items. The link can be either a predefined
-    # keyword from the below list, a Doxygen symbol, or a HTML code. The
-    # template then gets a tuple of (HTML code, title, URL) and either puts
-    # in the HTML code verbatim (if it's not empty) or creates a link from the
-    # title and URL.
-    predefined = {
-        'pages': (None, "Pages", 'pages.html'),
-        'namespaces': (None, "Namespaces", 'namespaces.html'),
-        'modules': (None, "Modules", 'modules.html'),
-        'annotated': (None, "Classes", 'annotated.html'),
-        'files': (None, "Files", 'files.html')
-    }
-    def extract_link(link):
-        # If this is a HTML code, return it verbatim
-        if link.startswith('<a'):
-            return link, None, None
-
-        # If predefined, return those
-        if link in predefined:
-            return predefined[link]
-
-        # Otherwise search in symbols
-        found = state.compounds[link]
-        return None, found.name, found.url
-    i: str
-    for var in 'M_LINKS_NAVBAR1', 'M_LINKS_NAVBAR2':
-        navbar_links = []
-        for i in state.doxyfile[var]:
-            # Split the line into links. It's either single-word keywords or
-            # HTML <a> elements. If it looks like a HTML, take everything until
-            # the closing </a>, otherwise take everything until the next
-            # whitespace.
-            links = []
-            while i:
-                if i.startswith('<a'):
-                    end = i.index('</a>') + 4
-                    links += [i[0:end]]
-                    i = i[end:].lstrip()
-                else:
-                    firstAndRest = i.split(None, 1)
-                    if len(firstAndRest):
-                        links += [firstAndRest[0]]
-                        if len(firstAndRest) == 1:
-                            break;
-                    i = firstAndRest[1]
-
+    # Resolve navbar links that are just an ID
+    def resolve_link(html, title, url, id):
+        if not html and not title and not url:
+            found = state.compounds[id]
+            title, url = found.name, found.url
+        return html, title, url, id
+    for var in 'LINKS_NAVBAR1', 'LINKS_NAVBAR2':
+        links = []
+        for html, title, url, id, sub in state.config[var]:
+            html, title, url, id = resolve_link(html, title, url, id)
             sublinks = []
-            for sublink in links[1:]:
-                html, title, url = extract_link(sublink)
-                sublinks += [(html, title, url, sublink)]
-            html, title, url = extract_link(links[0])
-            navbar_links += [(html, title, url, links[0], sublinks)]
-
-        state.doxyfile[var] = navbar_links
-
-    # Guess MIME type of the favicon
-    if state.doxyfile['M_FAVICON']:
-        state.doxyfile['M_FAVICON'] = (state.doxyfile['M_FAVICON'], mimetypes.guess_type(state.doxyfile['M_FAVICON'])[0])
+            for i in sub:
+                sublinks += [resolve_link(*i)]
+            links += [(html, title, url, id, sublinks)]
+        state.config[var] = links
 
 def build_search_data(state: State, merge_subtrees=True, add_lookahead_barriers=True, merge_prefixes=True) -> bytearray:
     trie = Trie()
@@ -2438,7 +2446,7 @@ def parse_xml(state: State, xml: str):
 
     # In order to show also undocumented members, go through all empty
     # <briefdescription>s and fill them with a generic text.
-    if state.doxyfile['M_SHOW_UNDOCUMENTED']:
+    if state.config['SHOW_UNDOCUMENTED']:
         _document_all_stuff(compounddef)
 
     # Ignoring compounds w/o any description, except for groups,
@@ -3152,7 +3160,7 @@ def parse_xml(state: State, xml: str):
 
     # Add the compound to search data, if it's documented
     # TODO: add example sources there? how?
-    if not state.doxyfile['M_SEARCH_DISABLED'] and not compound.kind == 'example' and (compound.kind == 'group' or compound.brief or compounddef.find('detaileddescription')):
+    if not state.config['SEARCH_DISABLED'] and not compound.kind == 'example' and (compound.kind == 'group' or compound.brief or compounddef.find('detaileddescription')):
         if compound.kind == 'namespace':
             kind = EntryType.NAMESPACE
         elif compound.kind == 'struct':
@@ -3303,7 +3311,7 @@ def parse_index_xml(state: State, xml):
 
     return parsed
 
-def parse_doxyfile(state: State, doxyfile, config = None):
+def parse_doxyfile(state: State, doxyfile, values = None):
     state.basedir = os.path.dirname(doxyfile)
 
     logging.debug("Parsing configuration from {}".format(doxyfile))
@@ -3313,55 +3321,30 @@ def parse_doxyfile(state: State, doxyfile, config = None):
     variable_continuation_re = re.compile(r"""^\s*(##!\s*)?(?P<key>[A-Z_]+)\s*\+=\s*(?P<quote>['"]?)(?P<value>.*)(?P=quote)\s*(?P<backslash>\\?)$""")
     continuation_re = re.compile(r"""^\s*(##!\s*)?(?P<quote>['"]?)(?P<value>.*)(?P=quote)\s*(?P<backslash>\\?)$""")
 
-    default_config = {
+    default_values = {
         'PROJECT_NAME': ['My Project'],
         'PROJECT_LOGO': [''],
         'OUTPUT_DIRECTORY': [''],
         'XML_OUTPUT': ['xml'],
         'HTML_OUTPUT': ['html'],
-        'HTML_EXTRA_STYLESHEET': [
-            'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600',
-            '../css/m-dark+documentation.compiled.css'],
-        'HTML_EXTRA_FILES': [],
         'DOT_FONTNAME': ['Helvetica'],
         'DOT_FONTSIZE': ['10'],
-        'SHOW_INCLUDE_FILES': ['YES'],
-
-        'M_CLASS_TREE_EXPAND_LEVELS': ['1'],
-        'M_FILE_TREE_EXPAND_LEVELS': ['1'],
-        'M_EXPAND_INNER_TYPES': ['NO'],
-        'M_THEME_COLOR': ['#22272e'],
-        'M_FAVICON': ['favicon-dark.png'],
-        'M_LINKS_NAVBAR1': ['pages', 'namespaces'],
-        'M_LINKS_NAVBAR2': ['annotated', 'files'],
-        'M_MATH_CACHE_FILE': ['m.math.cache'],
-        'M_PAGE_FINE_PRINT': ['[default]'],
-        'M_SEARCH_DISABLED': ['NO'],
-        'M_SEARCH_DOWNLOAD_BINARY': ['NO'],
-        'M_SEARCH_HELP': [
-"""<p class="m-noindent">Search for symbols, directories, files, pages or
-modules. You can omit any prefix from the symbol or file path; adding a
-<code>:</code> or <code>/</code> suffix lists all members of given symbol or
-directory.</p>
-<p class="m-noindent">Use <span class="m-label m-dim">&darr;</span>
-/ <span class="m-label m-dim">&uarr;</span> to navigate through the list,
-<span class="m-label m-dim">Enter</span> to go.
-<span class="m-label m-dim">Tab</span> autocompletes common prefix, you can
-copy a link to the result using <span class="m-label m-dim">⌘</span>
-<span class="m-label m-dim">L</span> while <span class="m-label m-dim">⌘</span>
-<span class="m-label m-dim">M</span> produces a Markdown link.</p>
-"""],
-        'M_SEARCH_BASE_URL': [''],
-        'M_SEARCH_EXTERNAL_URL': [''],
-        'M_SHOW_UNDOCUMENTED': ['NO'],
-        'M_VERSION_LABELS': ['NO']
+        'SHOW_INCLUDE_FILES': ['YES']
     }
 
     # Defaults so we don't fail with minimal Doxyfiles and also that the
     # user-provided Doxygen can append to them. They are later converted to
     # string or kept as a list based on type, so all have to be a list of
     # strings now.
-    if not config: config = copy.deepcopy(default_config)
+    #
+    # If there are no `values`, it means this is a top-level call (not recursed
+    # from Doxyfile @INCLUDEs). In that case (and only in that case) we
+    # finalize the config values (such as expanding FAVICON or LINKS_NAVBAR).
+    if not values:
+        finalize = True
+        values = copy.deepcopy(default_values)
+    else:
+        finalize = False
 
     def parse_value(var):
         if var.group('quote') in ['"', '\'']:
@@ -3390,7 +3373,7 @@ copy a link to the result using <span class="m-label m-dim">⌘</span>
             if continued_line:
                 var = continuation_re.match(line)
                 value, backslash = parse_value(var)
-                config[continued_line] += value
+                values[continued_line] += value
                 if not backslash: continued_line = None
                 continue
 
@@ -3402,10 +3385,10 @@ copy a link to the result using <span class="m-label m-dim">⌘</span>
 
                 # Another file included, parse it
                 if key == '@INCLUDE':
-                    parse_doxyfile(state, os.path.join(os.path.dirname(doxyfile), ' '.join(value)), config)
+                    parse_doxyfile(state, os.path.join(os.path.dirname(doxyfile), ' '.join(value)), values)
                     assert not backslash
                 else:
-                    config[key] = value
+                    values[key] = value
 
                 if backslash: continued_line = key
                 continue
@@ -3414,9 +3397,9 @@ copy a link to the result using <span class="m-label m-dim">⌘</span>
             var = variable_continuation_re.match(line)
             if var:
                 key = var.group('key')
-                if not key in config: config[key] = []
+                if not key in values: values[key] = []
                 value, backslash = parse_value(var)
-                config[key] += value
+                values[key] += value
                 if backslash: continued_line = key
 
                 # only because coverage.py can't handle continue
@@ -3432,61 +3415,172 @@ copy a link to the result using <span class="m-label m-dim">⌘</span>
             logging.warning("{}: unmatchable line {}".format(doxyfile, line)) # pragma: no cover
 
     # Some values are set to empty in the default-generated Doxyfile but they
-    # shouldn't be empty. Revert them to our defaults.
-    # TODO: this may behave strange in corner cases where multiple @INCLUDEd
-    # files set or append to the same thing
+    # shouldn't be empty. Delete the variable ín that case so it doesn't
+    # override our defaults.
     for i in ['HTML_EXTRA_STYLESHEET']:
-        if i in config and not config[i]:
-            config[i] = default_config[i]
-
-    # String values that we want
-    for i in ['PROJECT_NAME',
-              'PROJECT_BRIEF',
-              'PROJECT_LOGO',
-              'OUTPUT_DIRECTORY',
-              'HTML_OUTPUT',
-              'XML_OUTPUT',
-              'DOT_FONTNAME',
-              'M_MAIN_PROJECT_URL',
-              'M_HTML_HEADER',
-              'M_PAGE_HEADER',
-              'M_PAGE_FINE_PRINT',
-              'M_THEME_COLOR',
-              'M_FAVICON',
-              'M_MATH_CACHE_FILE',
-              'M_SEARCH_HELP',
-              'M_SEARCH_EXTERNAL_URL',
-              'M_SEARCH_BASE_URL']:
-        if i in config: state.doxyfile[i] = '\n'.join(config[i])
-
-    # Int values that we want
-    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]))
-
-    # Boolean values that we want
-    for i in ['CREATE_SUBDIRS',
-              'JAVADOC_AUTOBRIEF',
-              'QT_AUTOBRIEF',
-              'INTERNAL_DOCS',
-              'SHOW_INCLUDE_FILES',
-              'M_EXPAND_INNER_TYPES',
-              'M_SEARCH_DISABLED',
-              'M_SEARCH_DOWNLOAD_BINARY',
-              'M_SHOW_UNDOCUMENTED',
-              'M_VERSION_LABELS']:
-        if i in config: state.doxyfile[i] = ' '.join(config[i]) == 'YES'
-
-    # List values that we want. Drop empty lines.
-    for i in ['TAGFILES',
-              'HTML_EXTRA_STYLESHEET',
-              'HTML_EXTRA_FILES',
-              'M_LINKS_NAVBAR1',
-              'M_LINKS_NAVBAR2']:
-        if i in config:
-            state.doxyfile[i] = [line for line in config[i] if line]
+        if i in values and not values[i]: del values[i]
+
+    # Parse recognized Doxyfile values with desired type. The second tuple
+    # value denotes that the Doxyfile value is an alias to a value in conf.py,
+    # in which case we'll save it there instead.
+    for key, alias, type_ in [
+        # Order roughly the same as in python.py default_config to keep those
+        # two consistent
+        ('PROJECT_NAME', None, str),
+        ('PROJECT_BRIEF', None, str),
+        ('PROJECT_LOGO', None, str),
+        ('M_MAIN_PROJECT_URL', 'MAIN_PROJECT_URL', str),
+
+        ('OUTPUT_DIRECTORY', None, str),
+        ('HTML_OUTPUT', None, str),
+        ('XML_OUTPUT', None, str),
+
+        ('DOT_FONTNAME', None, str),
+        ('DOT_FONTSIZE', None, int),
+        ('CREATE_SUBDIRS', None, bool), # processing fails below if this is set
+        ('JAVADOC_AUTOBRIEF', None, bool),
+        ('QT_AUTOBRIEF', None, bool),
+        ('INTERNAL_DOCS', None, bool),
+        ('SHOW_INCLUDE_FILES', None, bool),
+        ('TAGFILES', None, list),
+
+        ('M_THEME_COLOR', 'THEME_COLOR', str),
+        ('M_FAVICON', 'FAVICON', str), # plus special handling below
+        ('HTML_EXTRA_STYLESHEET', 'STYLESHEETS', list),
+        ('HTML_EXTRA_FILES', 'EXTRA_FILES', list),
+        # M_LINKS_NAVBAR1 and M_LINKS_NAVBAR2 have special handling below
+
+        ('M_HTML_HEADER', 'HTML_HEADER', str),
+        ('M_PAGE_HEADER', 'PAGE_HEADER', str),
+        ('M_PAGE_FINE_PRINT', 'FINE_PRINT', str),
+
+        ('M_CLASS_TREE_EXPAND_LEVELS', 'CLASS_INDEX_EXPAND_LEVELS', int),
+        ('M_EXPAND_INNER_TYPES', 'CLASS_INDEX_EXPAND_INNER', bool),
+        ('M_FILE_TREE_EXPAND_LEVELS', 'FILE_INDEX_EXPAND_LEVELS', int),
+
+        ('M_SEARCH_DISABLED', 'SEARCH_DISABLED', bool),
+        ('M_SEARCH_DOWNLOAD_BINARY', 'SEARCH_DOWNLOAD_BINARY', bool),
+        ('M_SEARCH_HELP', 'SEARCH_HELP', str),
+        ('M_SEARCH_BASE_URL', 'SEARCH_BASE_URL', str),
+        ('M_SEARCH_EXTERNAL_URL', 'SEARCH_EXTERNAL_URL', str),
+
+        ('M_SHOW_UNDOCUMENTED', 'SHOW_UNDOCUMENTED', bool),
+        ('M_VERSION_LABELS', 'VERSION_LABELS', bool),
+
+        ('M_MATH_CACHE_FILE', 'M_MATH_CACHE_FILE', str),
+    ]:
+        if key not in values: continue
+
+        if type_ is str:
+            value = '\n'.join(values[key])
+        elif type_ is int:
+            value = int(' '.join(values[key]))
+        elif type_ is bool:
+            value = ' '.join(values[key]) == 'YES'
+        elif type_ is list:
+            value = [line for line in values[key] if line] # Drop empty lines
+        else: # pragma: no cover
+            assert False
 
+        if alias:
+            state.config[alias] = value
+        else:
+            state.doxyfile[key] = value
+
+    # Process M_LINKS_NAVBAR[12] into either (HTML, sublinks) or
+    # (title, URL, ID, sublinks), with sublinks being either
+    # (HTML) or (title, URL, ID). Those are then saved into LINKS_NAVBAR[12]
+    # and processed further.
+    predefined = {
+        'pages': ("Pages", 'pages.html'),
+        'namespaces': ("Namespaces", 'namespaces.html'),
+        'modules': ("Modules", 'modules.html'),
+        'annotated': ("Classes", 'annotated.html'),
+        'files': ("Files", 'files.html')
+    }
+    def extract_link(link):
+        # If this is a HTML code, return it as a one-item tuple
+        if link.startswith('<a'):
+            return (link, )
+
+        # If predefined, return those
+        if link in predefined:
+            return (predefined[link][0], link)
+
+        # Otherwise keep the ID, which will be resolved later
+        return None, link
+    for key, alias in [
+        ('M_LINKS_NAVBAR1', 'LINKS_NAVBAR1'),
+        ('M_LINKS_NAVBAR2', 'LINKS_NAVBAR2')
+    ]:
+        if key not in values: continue
+
+        navbar_links = []
+        # Drop empty lines
+        for i in [line for line in values[key] if line]:
+            # Split the line into links. It's either single-word keywords or
+            # HTML <a> elements. If it looks like a HTML, take everything until
+            # the closing </a>, otherwise take everything until the next
+            # whitespace.
+            links = []
+            while i:
+                if i.startswith('<a'):
+                    end = i.index('</a>') + 4
+                    links += [i[0:end]]
+                    i = i[end:].lstrip()
+                else:
+                    firstAndRest = i.split(None, 1)
+                    if len(firstAndRest):
+                        links += [firstAndRest[0]]
+                        if len(firstAndRest) == 1:
+                            break;
+                    i = firstAndRest[1]
+
+            sublinks = []
+            for sublink in links[1:]:
+                sublinks += [extract_link(sublink)]
+            navbar_links += [extract_link(links[0]) + (sublinks, )]
+
+        state.config[alias] = navbar_links
+
+    # Below we finalize the config values, converting them to formats that are
+    # easy to understand by the code / templates (but not easy to write from
+    # the user PoV). If this is not a top-level call (but a recursed one from
+    # @INCLUDE), we exit, to avoid finalizing everything multiple times.
+    if not finalize: return
+
+    # Convert the links from either (html, ) or (title, id) to a
+    # (html, title, url, id) tuple so it's easier to process by the template.
+    # The url is not known at this point and will be filled later in
+    # postprocess_state().
+    def expand_link(link):
+        if len(link) == 1:
+            return (link[0], None, None, None)
+        else:
+            assert len(link) == 2
+            if link[1] in predefined:
+                url = predefined[link[1]][1]
+                if not link[0]: title = predefined[link[1]][0]
+                else: title = link[0]
+            else:
+                title = None
+                url = None
+            return (None, title, url, link[1])
+    for key in ('LINKS_NAVBAR1', 'LINKS_NAVBAR2'):
+        links = []
+        for i in state.config[key]:
+            sublinks = []
+            for subi in i[-1]:
+                sublinks += [expand_link(subi)]
+            links += [expand_link(i[:-1]) + (sublinks, )]
+        state.config[key] = links
+
+    # Guess MIME type of the favicon. It's supplied explicitly when coming from
+    # a conf.py
+    if 'FAVICON' in state.config and state.config['FAVICON']:
+        state.config['FAVICON'] = (state.config['FAVICON'], mimetypes.guess_type(state.config['FAVICON'])[0])
+
+    # Fail if this option is set
     if state.doxyfile.get('CREATE_SUBDIRS', False):
         logging.fatal("{}: CREATE_SUBDIRS is not supported, sorry. Disable it and try again.".format(doxyfile))
         raise NotImplementedError
@@ -3504,8 +3598,8 @@ def run(state: State, *, templates=default_templates, wildcard=default_wildcard,
     # If math rendering cache is not disabled, load the previous version. If
     # there is no cache, reset the cache to an empty state to avoid
     # order-dependent issues when testing
-    math_cache_file = os.path.join(state.basedir, state.doxyfile['OUTPUT_DIRECTORY'], state.doxyfile['M_MATH_CACHE_FILE'])
-    if state.doxyfile['M_MATH_CACHE_FILE'] and os.path.exists(math_cache_file):
+    math_cache_file = os.path.join(state.basedir, state.doxyfile['OUTPUT_DIRECTORY'], state.config['M_MATH_CACHE_FILE'])
+    if state.config['M_MATH_CACHE_FILE'] and os.path.exists(math_cache_file):
         latex2svgextra.unpickle_cache(math_cache_file)
     else:
         latex2svgextra.unpickle_cache(None)
@@ -3561,7 +3655,8 @@ def run(state: State, *, templates=default_templates, wildcard=default_wildcard,
                     DOXYGEN_VERSION=parsed.version,
                     FILENAME=file,
                     SEARCHDATA_FORMAT_VERSION=searchdata_format_version,
-                    **state.doxyfile)
+                    # TODO: whitelist only what matters from doxyfile
+                    **state.doxyfile, **state.config)
 
                 output = os.path.join(html_output, file)
                 with open(output, 'wb') as f:
@@ -3580,7 +3675,8 @@ def run(state: State, *, templates=default_templates, wildcard=default_wildcard,
                 DOXYGEN_VERSION=parsed.version,
                 FILENAME=parsed.compound.url,
                 SEARCHDATA_FORMAT_VERSION=searchdata_format_version,
-                **state.doxyfile)
+                # TODO: whitelist only what matters from doxyfile
+                **state.doxyfile, **state.config)
 
             output = os.path.join(html_output, parsed.compound.url)
             with open(output, 'wb') as f:
@@ -3607,7 +3703,8 @@ def run(state: State, *, templates=default_templates, wildcard=default_wildcard,
             DOXYGEN_VERSION=None,
             FILENAME='index.html',
             SEARCHDATA_FORMAT_VERSION=searchdata_format_version,
-            **state.doxyfile)
+            # TODO: whitelist only what matters from doxyfile
+            **state.doxyfile, **state.config)
         output = os.path.join(html_output, 'index.html')
         with open(output, 'wb') as f:
             f.write(rendered.encode('utf-8'))
@@ -3617,12 +3714,12 @@ def run(state: State, *, templates=default_templates, wildcard=default_wildcard,
             # also for nested templates :(
             f.write(b'\n')
 
-    if not state.doxyfile['M_SEARCH_DISABLED']:
+    if not state.config['SEARCH_DISABLED']:
         logging.debug("building search data for {} symbols".format(len(state.search)))
 
         data = build_search_data(state, add_lookahead_barriers=search_add_lookahead_barriers, merge_subtrees=search_merge_subtrees, merge_prefixes=search_merge_prefixes)
 
-        if state.doxyfile['M_SEARCH_DOWNLOAD_BINARY']:
+        if state.config['SEARCH_DOWNLOAD_BINARY']:
             with open(os.path.join(html_output, searchdata_filename), 'wb') as f:
                 f.write(data)
         else:
@@ -3630,11 +3727,12 @@ def run(state: State, *, templates=default_templates, wildcard=default_wildcard,
                 f.write(base85encode_search_data(data))
 
         # OpenSearch metadata, in case we have the base URL
-        if state.doxyfile['M_SEARCH_BASE_URL']:
+        if state.config['SEARCH_BASE_URL']:
             logging.debug("writing OpenSearch metadata file")
 
             template = env.get_template('opensearch.xml')
-            rendered = template.render(**state.doxyfile)
+            # TODO: whitelist only what matters from doxyfile
+            rendered = template.render(**state.doxyfile, **state.config)
             output = os.path.join(html_output, 'opensearch.xml')
             with open(output, 'wb') as f:
                 f.write(rendered.encode('utf-8'))
@@ -3645,7 +3743,7 @@ def run(state: State, *, templates=default_templates, wildcard=default_wildcard,
                 f.write(b'\n')
 
     # Copy all referenced files
-    for i in state.images + state.doxyfile['HTML_EXTRA_STYLESHEET'] + state.doxyfile['HTML_EXTRA_FILES'] + ([state.doxyfile['PROJECT_LOGO']] if state.doxyfile['PROJECT_LOGO'] else []) + ([state.doxyfile['M_FAVICON'][0]] if state.doxyfile['M_FAVICON'] else []) + ([] if state.doxyfile['M_SEARCH_DISABLED'] else ['search.js']):
+    for i in state.images + state.config['STYLESHEETS'] + state.config['EXTRA_FILES'] + ([state.doxyfile['PROJECT_LOGO']] if state.doxyfile['PROJECT_LOGO'] else []) + ([state.config['FAVICON'][0]] if state.config['FAVICON'] else []) + ([] if state.config['SEARCH_DISABLED'] else ['search.js']):
         # Skip absolute URLs
         if urllib.parse.urlparse(i).netloc: continue
 
@@ -3665,12 +3763,12 @@ def run(state: State, *, templates=default_templates, wildcard=default_wildcard,
         shutil.copy(i, os.path.join(html_output, os.path.basename(file_out)))
 
     # Save updated math cache file
-    if state.doxyfile['M_MATH_CACHE_FILE']:
+    if state.config['M_MATH_CACHE_FILE']:
         latex2svgextra.pickle_cache(math_cache_file)
 
 if __name__ == '__main__': # pragma: no cover
     parser = argparse.ArgumentParser()
-    parser.add_argument('doxyfile', help="where the Doxyfile is")
+    parser.add_argument('config', help="where the Doxyfile or conf.py is")
     parser.add_argument('--templates', help="template directory", default=default_templates)
     parser.add_argument('--wildcard', help="only process files matching the wildcard", default=default_wildcard)
     parser.add_argument('--index-pages', nargs='+', help="index page templates", default=default_index_pages)
@@ -3687,10 +3785,19 @@ if __name__ == '__main__': # pragma: no cover
     else:
         logging.basicConfig(level=logging.INFO)
 
-    # Make the Doxyfile path absolute, otherwise everything gets messed up
-    doxyfile = os.path.abspath(args.doxyfile)
+    config = copy.deepcopy(default_config)
+
+    if args.config.endswith('.py'):
+        name, _ = os.path.splitext(os.path.basename(args.config))
+        module = SourceFileLoader(name, args.config).load_module()
+        if module is not None:
+            config.update((k, v) for k, v in inspect.getmembers(module) if k.isupper())
+        doxyfile = os.path.join(os.path.dirname(os.path.abspath(args.config)), config['DOXYFILE'])
+    else:
+        # Make the Doxyfile path absolute, otherwise everything gets messed up
+        doxyfile = os.path.abspath(args.doxyfile)
 
-    state = State()
+    state = State(config)
     parse_doxyfile(state, doxyfile)
 
     # Doxygen is stupid and can't create nested directories, create the input
@@ -3698,7 +3805,7 @@ if __name__ == '__main__': # pragma: no cover
     os.makedirs(state.doxyfile['OUTPUT_DIRECTORY'], exist_ok=True)
 
     if not args.no_doxygen:
-        logging.debug("running Doxygen on {}".format(args.doxyfile))
+        logging.debug("running Doxygen on {}".format(doxyfile))
         subprocess.run(["doxygen", doxyfile], cwd=os.path.dirname(doxyfile), check=True)
 
     run(state, templates=os.path.abspath(args.templates), wildcard=args.wildcard, index_pages=args.index_pages, search_merge_subtrees=not args.search_no_subtree_merging, search_add_lookahead_barriers=not args.search_no_lookahead_barriers, search_merge_prefixes=not args.search_no_prefix_merging)
index 338ad88116dfd524b9432c71fa61840495ca53ec..132f221d2ef6679292807f0fe43af67a60f67f6b 100644 (file)
@@ -6,7 +6,7 @@
         <ul class="m-doc">
           {% for i in index.symbols recursive %}
           {% if i.children %}
-          <li class="m-doc-collapsible{% if loop.depth > M_CLASS_TREE_EXPAND_LEVELS or (i.kind != 'namespace' and not M_EXPAND_INNER_TYPES) %} collapsed{% endif %}">
+          <li class="m-doc-collapsible{% if loop.depth > CLASS_INDEX_EXPAND_LEVELS or (i.kind != 'namespace' and not CLASS_INDEX_EXPAND_INNER) %} collapsed{% endif %}">
             <a href="#" onclick="return toggle(this)">{{ i.kind }}</a> <a href="{{ i.url }}" class="m-doc">{{ i.name }}</a>{% if i.deprecated %} <span class="m-label m-danger">{{ i.deprecated }}</span>{% endif %}{% if i.since %} {{ i.since }}{% endif %} <span class="m-doc">{{ i.brief }}</span>
             <ul class="m-doc">
 {{ loop(i.children)|rtrim|indent(4, true) }}
index 30477a99d468284c684073d489e75fb26bf2475e..f212976493cc71e6f23550fd8238c69747b6708d 100644 (file)
@@ -3,39 +3,39 @@
 <head>
   <meta charset="UTF-8" />
   <title>{% block title %}{{ PROJECT_NAME }}{% if PROJECT_BRIEF %} {{ PROJECT_BRIEF }}{% endif %}{% endblock %}</title>
-  {% for css in HTML_EXTRA_STYLESHEET %}
+  {% for css in STYLESHEETS %}
   <link rel="stylesheet" href="{{ css|basename_or_url|e }}" />
   {% endfor %}
-  {% if M_FAVICON %}
-  <link rel="icon" href="{{ M_FAVICON[0]|basename_or_url|e }}" type="{{ M_FAVICON[1] }}" />
+  {% if FAVICON %}
+  <link rel="icon" href="{{ FAVICON[0]|basename_or_url|e }}" type="{{ FAVICON[1] }}" />
   {% endif %}
-  {% if not M_SEARCH_DISABLED and M_SEARCH_BASE_URL %}
+  {% if not SEARCH_DISABLED and SEARCH_BASE_URL %}
   <link rel="search" type="application/opensearchdescription+xml" href="opensearch.xml" title="Search {{ PROJECT_NAME }} documentation" />
   {% endif %}
   {% block header_links %}
   {% endblock %}
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-  {% if M_THEME_COLOR %}
-  <meta name="theme-color" content="{{ M_THEME_COLOR }}" />
+  {% if THEME_COLOR %}
+  <meta name="theme-color" content="{{ THEME_COLOR }}" />
   {% endif %}
-  {% if M_HTML_HEADER %}
-  {{ M_HTML_HEADER|rtrim|indent(2) }}
+  {% if HTML_HEADER %}
+  {{ HTML_HEADER|rtrim|indent(2) }}
   {% endif %}
 </head>
 <body>
 <header><nav id="navigation">
   <div class="m-container">
     <div class="m-row">
-      {% if M_MAIN_PROJECT_URL and PROJECT_BRIEF %}
+      {% if MAIN_PROJECT_URL and PROJECT_BRIEF %}
       <span id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">
-        <a href="{{ M_MAIN_PROJECT_URL }}">{% if PROJECT_LOGO %}<img src="{{ PROJECT_LOGO|basename_or_url|e }}" alt="" />{% endif %}{{ PROJECT_NAME }}</a> <span class="m-breadcrumb">|</span> <a href="index.html" class="m-thin">{{ PROJECT_BRIEF }}</a>
+        <a href="{{ MAIN_PROJECT_URL }}">{% if PROJECT_LOGO %}<img src="{{ PROJECT_LOGO|basename_or_url|e }}" alt="" />{% endif %}{{ PROJECT_NAME }}</a> <span class="m-breadcrumb">|</span> <a href="index.html" class="m-thin">{{ PROJECT_BRIEF }}</a>
       </span>
       {% else %}
       <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">{% if PROJECT_LOGO %}<img src="{{ PROJECT_LOGO|basename_or_url|e }}" alt="" />{% endif %}{{ PROJECT_NAME }}{% if PROJECT_BRIEF %} <span class="m-thin">{{ PROJECT_BRIEF }}</span>{% endif %}</a>
       {% endif %}
-      {% if M_LINKS_NAVBAR1 or M_LINKS_NAVBAR2 or not M_SEARCH_DISABLED %}
+      {% if LINKS_NAVBAR1 or LINKS_NAVBAR2 or not SEARCH_DISABLED %}
       <div class="m-col-t-4 m-hide-m m-text-right m-nopadr">
-        {% if not M_SEARCH_DISABLED %}
+        {% if not SEARCH_DISABLED %}
         <a href="#search" class="m-doc-search-icon" title="Search" onclick="return showSearch()"><svg style="height: 0.9rem;" viewBox="0 0 16 16">
           <path id="m-doc-search-icon-path" d="m6 0c-3.31 0-6 2.69-6 6 0 3.31 2.69 6 6 6 1.49 0 2.85-0.541 3.89-1.44-0.0164 0.338 0.147 0.759 0.5 1.15l3.22 3.79c0.552 0.614 1.45 0.665 2 0.115 0.55-0.55 0.499-1.45-0.115-2l-3.79-3.22c-0.392-0.353-0.812-0.515-1.15-0.5 0.895-1.05 1.44-2.41 1.44-3.89 0-3.31-2.69-6-6-6zm0 1.56a4.44 4.44 0 0 1 4.44 4.44 4.44 4.44 0 0 1-4.44 4.44 4.44 4.44 0 0 1-4.44-4.44 4.44 4.44 0 0 1 4.44-4.44z"/>
         </svg></a>
@@ -45,8 +45,8 @@
       </div>
       <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="{% if M_LINKS_NAVBAR2 %}m-col-t-6{% else %}m-col-t-12{% endif %} m-col-m-none">
-            {% for html, title, link, id, sub in M_LINKS_NAVBAR1 %}
+          <ol class="{% if LINKS_NAVBAR2 %}m-col-t-6{% else %}m-col-t-12{% endif %} m-col-m-none">
+            {% for html, title, link, id, sub in LINKS_NAVBAR1 %}
             {% if not sub %}
             <li>{% if html %}{{ html }}{% else %}<a href="{{ link }}"{% if (compound and compound.id == id) or navbar_current == id %} id="m-navbar-current"{% endif %}>{{ title }}</a>{% endif %}</li>
             {% else %}
             {% endif %}
             {% endfor %}
           </ol>
-          {% if M_LINKS_NAVBAR2 or not M_SEARCH_DISABLED %}
-          {% set start = M_LINKS_NAVBAR1|length + 1 %}
+          {% if LINKS_NAVBAR2 or not SEARCH_DISABLED %}
+          {% set start = LINKS_NAVBAR1|length + 1 %}
           <ol class="m-col-t-6 m-col-m-none" start="{{ start }}">
-            {% for html, title, link, id, sub in M_LINKS_NAVBAR2 %}
+            {% for html, title, link, id, sub in LINKS_NAVBAR2 %}
             {% if not sub %}
             <li>{% if html %}{{ html }}{% else %}<a href="{{ link }}"{% if (compound and compound.id == id) or navbar_current == id %} id="m-navbar-current"{% endif %}>{{ title }}</a>{% endif %}</li>
             {% else %}
@@ -86,7 +86,7 @@
             </li>
             {% endif %}
             {% endfor %}
-            {% if not M_SEARCH_DISABLED %}
+            {% if not SEARCH_DISABLED %}
             <li class="m-show-m"><a href="#search" class="m-doc-search-icon" title="Search" onclick="return showSearch()"><svg style="height: 0.9rem;" viewBox="0 0 16 16">
               <use href="#m-doc-search-icon-path" />
             </svg></a></li>
   <div class="m-container m-container-inflatable">
     <div class="m-row">
       <div class="m-col-l-10 m-push-l-1">
-        {% if M_PAGE_HEADER %}
-        {{ M_PAGE_HEADER|replace('{filename}', FILENAME) }}
+        {% if PAGE_HEADER %}
+        {{ PAGE_HEADER|replace('{filename}', FILENAME) }}
         {% endif %}
 {% block main %}
 {% endblock %}
     </div>
   </div>
 </article></main>
-{% if not M_SEARCH_DISABLED %}
+{% if not SEARCH_DISABLED %}
 <div class="m-doc-search" id="search">
   <a href="#!" onclick="return hideSearch()"></a>
   <div class="m-container">
           <div id="search-symbolcount">&hellip;</div>
         </div>
         <div class="m-doc-search-content">
-          <form{% if M_SEARCH_BASE_URL %} action="{{ M_SEARCH_BASE_URL }}#search"{% endif %}>
+          <form{% if SEARCH_BASE_URL %} action="{{ SEARCH_BASE_URL }}#search"{% endif %}>
             <input type="search" name="q" id="search-input" placeholder="Loading &hellip;" disabled="disabled" autofocus="autofocus" autocomplete="off" spellcheck="false" />
           </form>
-          <noscript class="m-text m-danger m-text-center">Unlike everything else in the docs, the search functionality <em>requires</em> JavaScript.{% if M_SEARCH_EXTERNAL_URL %} Enable it or <a href="{{ M_SEARCH_EXTERNAL_URL|replace('{query}', '') }}">use an external search engine</a>.{% endif %}</noscript>
+          <noscript class="m-text m-danger m-text-center">Unlike everything else in the docs, the search functionality <em>requires</em> JavaScript.{% if SEARCH_EXTERNAL_URL %} Enable it or <a href="{{ SEARCH_EXTERNAL_URL|replace('{query}', '') }}">use an external search engine</a>.{% endif %}</noscript>
           <div id="search-help" class="m-text m-dim m-text-center">
-            {{ M_SEARCH_HELP|rtrim|indent(12) }}
+            {{ SEARCH_HELP|rtrim|indent(12) }}
           </div>
-          <div id="search-notfound" class="m-text m-warning m-text-center">Sorry, nothing was found.{% if M_SEARCH_EXTERNAL_URL %}<br />Maybe try a full-text <a href="#" id="search-external" data-search-engine="{{ M_SEARCH_EXTERNAL_URL }}">search with external engine</a>?{% endif %}</div>
+          <div id="search-notfound" class="m-text m-warning m-text-center">Sorry, nothing was found.{% if SEARCH_EXTERNAL_URL %}<br />Maybe try a full-text <a href="#" id="search-external" data-search-engine="{{ SEARCH_EXTERNAL_URL }}">search with external engine</a>?{% endif %}</div>
           <ul id="search-results"></ul>
         </div>
       </div>
   </div>
 </div>
 <script src="search-v{{ SEARCHDATA_FORMAT_VERSION }}.js"></script>
-{% if M_SEARCH_DOWNLOAD_BINARY %}
+{% if SEARCH_DOWNLOAD_BINARY %}
 <script>
   Search.download(window.location.pathname.substr(0, window.location.pathname.lastIndexOf('/') + 1) + 'searchdata-v{{ SEARCHDATA_FORMAT_VERSION }}.bin');
 </script>
 <script src="searchdata-v{{ SEARCHDATA_FORMAT_VERSION }}.js" async="async"></script>
 {% endif %}
 {% endif %}
-{% if M_PAGE_FINE_PRINT %}
+{% if FINE_PRINT %}
 <footer><nav>
   <div class="m-container">
     <div class="m-row">
       <div class="m-col-l-10 m-push-l-1">
-        {% if M_PAGE_FINE_PRINT == '[default]' %}
+        {% if FINE_PRINT == '[default]' %}
         <p>{{ PROJECT_NAME }}{% if PROJECT_BRIEF %} {{ PROJECT_BRIEF }}{% endif %}. Created with {% if DOXYGEN_VERSION %}<a href="https://doxygen.org/">Doxygen</a> {{ DOXYGEN_VERSION }} and {% endif %}<a href="https://mcss.mosra.cz/">m.css</a>.</p>
         {% else %}
-        {{ M_PAGE_FINE_PRINT|replace('{doxygen_version}', DOXYGEN_VERSION) }}
+        {{ FINE_PRINT|replace('{doxygen_version}', DOXYGEN_VERSION) }}
         {% endif %}
       </div>
     </div>
index 7400a5d18f31e9a7039ba4634a31540496646327..a4cacf7edd40ec1200978580ed23b81befccc52c 100644 (file)
@@ -6,7 +6,7 @@
         <ul class="m-doc">
           {% for i in index.files recursive %}
           {% if i.children %}
-          <li class="m-doc-collapsible{% if loop.depth > M_FILE_TREE_EXPAND_LEVELS %} collapsed{% endif %}">
+          <li class="m-doc-collapsible{% if loop.depth > FILE_INDEX_EXPAND_LEVELS %} collapsed{% endif %}">
             <a href="#" onclick="return toggle(this)">{{ i.kind }}</a> <a href="{{ i.url }}" class="m-doc">{{ i.name }}</a>{% if i.deprecated %} <span class="m-label m-danger">{{ i.deprecated }}</span>{% endif %}{% if i.since %} {{ i.since }}{% endif %} <span class="m-doc">{{ i.brief }}</span>
             <ul class="m-doc">
 {{ loop(i.children)|rtrim|indent(4, true) }}
index d477ab27e06e2783effcf72b921b90ef7f58d98c..a328c7cb59fdaaf41745be238f36589960aed345 100644 (file)
@@ -2,8 +2,8 @@
 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
   <ShortName>{{ PROJECT_NAME }}{% if PROJECT_BRIEF %} {{ PROJECT_BRIEF }}{% endif %}</ShortName>
   <Description>Search {{ PROJECT_NAME }} documentation</Description>
-  {% if M_FAVICON %}
-  <Image type="{{ M_FAVICON[1] }}">{{ M_SEARCH_BASE_URL|urljoin(M_FAVICON[0])|e }}</Image>
+  {% if FAVICON %}
+  <Image type="{{ FAVICON[1] }}">{{ SEARCH_BASE_URL|urljoin(FAVICON[0])|e }}</Image>
   {% endif %}
-  <Url type="text/html" template="{{ M_SEARCH_BASE_URL }}?q={searchTerms}#search"/>
+  <Url type="text/html" template="{{ SEARCH_BASE_URL }}?q={searchTerms}#search"/>
 </OpenSearchDescription>
index f2b12d4f60928b83c10e52ea0e4eb6fa63cd7c7e..8cd6038956ce13d7ffec434f14eaef4544f79703 100644 (file)
 #   DEALINGS IN THE SOFTWARE.
 #
 
+import copy
 import os
 import shutil
 import subprocess
 import unittest
 
-from doxygen import State, parse_doxyfile, run, default_templates, default_wildcard, default_index_pages
+from doxygen import State, parse_doxyfile, run, default_templates, default_wildcard, default_index_pages, default_config
 
 def doxygen_version():
     return subprocess.check_output(['doxygen', '-v']).decode('utf-8').strip()
@@ -45,7 +46,7 @@ class BaseTestCase(unittest.TestCase):
         if os.path.exists(os.path.join(self.path, 'html')): shutil.rmtree(os.path.join(self.path, 'html'))
 
     def run_doxygen(self, templates=default_templates, wildcard=default_wildcard, index_pages=default_index_pages):
-        state = State()
+        state = State(copy.deepcopy(default_config))
         parse_doxyfile(state, os.path.join(self.path, 'Doxyfile'))
         run(state, templates=templates, wildcard=wildcard, index_pages=index_pages, sort_globbed_files=True)
 
index 8c9510eb52dd0daa67a16f578e4ebdd421a576e1..bc619e0817c377ae8f6631ec0704eea487590a53 100644 (file)
@@ -3,26 +3,3 @@
 
 # Quotes
 PROJECT_BRIEF = "is cool"
-
-# Multiple lines
-HTML_EXTRA_FILES = \
-    css \
-    "another.png" \
-    \
-    "hello"
-
-# Adding
-HTML_EXTRA_STYLESHEET = a.css
-HTML_EXTRA_STYLESHEET += b.css
-
-# Escaping
-M_PAGE_HEADER = 'this is "quotes" \'apostrophes\''
-M_PAGE_FINE_PRINT = "this is \"quotes\""
-
-# Commented with a hashbang
-##! M_LINKS_NAVBAR1 = pages \
-##!     modules
-
-# Commented with a hashbang and no space
-##!M_LINKS_NAVBAR2 = files \
-##!annotated
diff --git a/documentation/test_doxygen/doxyfile/Doxyfile-legacy b/documentation/test_doxygen/doxyfile/Doxyfile-legacy
new file mode 100644 (file)
index 0000000..8c9510e
--- /dev/null
@@ -0,0 +1,28 @@
+# Includes
+@INCLUDE = Doxyfile-another
+
+# Quotes
+PROJECT_BRIEF = "is cool"
+
+# Multiple lines
+HTML_EXTRA_FILES = \
+    css \
+    "another.png" \
+    \
+    "hello"
+
+# Adding
+HTML_EXTRA_STYLESHEET = a.css
+HTML_EXTRA_STYLESHEET += b.css
+
+# Escaping
+M_PAGE_HEADER = 'this is "quotes" \'apostrophes\''
+M_PAGE_FINE_PRINT = "this is \"quotes\""
+
+# Commented with a hashbang
+##! M_LINKS_NAVBAR1 = pages \
+##!     modules
+
+# Commented with a hashbang and no space
+##!M_LINKS_NAVBAR2 = files \
+##!annotated
index 2e9dc9db09b04aa2b71b72d2d6dd866d9bedb7be..00f6cf450192d3dd619263ed23d2e971c9876d8e 100644 (file)
 #   DEALINGS IN THE SOFTWARE.
 #
 
+import copy
 import os
 import shutil
 import subprocess
 import unittest
 
-from doxygen import parse_doxyfile, State
+from doxygen import parse_doxyfile, State, default_config
 
 from . import BaseTestCase
 
@@ -38,29 +39,44 @@ class Doxyfile(unittest.TestCase):
         # Display ALL THE DIFFS
         self.maxDiff = None
 
-    def test(self):
-        state = State()
-        parse_doxyfile(state, 'test_doxygen/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',
-            'M_CLASS_TREE_EXPAND_LEVELS': 1,
-            'M_EXPAND_INNER_TYPES': False,
-            'M_FAVICON': 'favicon-dark.png',
-            'M_FILE_TREE_EXPAND_LEVELS': 1,
-            'M_LINKS_NAVBAR1': ['pages', 'modules'],
-            'M_LINKS_NAVBAR2': ['files', 'annotated'], # different order
-            'M_MATH_CACHE_FILE': 'm.math.cache',
-            'M_PAGE_FINE_PRINT': 'this is "quotes"',
-            'M_PAGE_HEADER': 'this is "quotes" \'apostrophes\'',
-            'M_SEARCH_DISABLED': False,
-            'M_SEARCH_DOWNLOAD_BINARY': False,
-            'M_SEARCH_BASE_URL': '',
-            'M_SEARCH_EXTERNAL_URL': '',
-            'M_SEARCH_HELP':
+    expected_doxyfile = {
+        'DOT_FONTNAME': 'Helvetica',
+        'DOT_FONTSIZE': 10,
+        'HTML_OUTPUT': 'html',
+        'OUTPUT_DIRECTORY': '',
+        'PROJECT_BRIEF': 'is cool',
+        'PROJECT_LOGO': '',
+        'PROJECT_NAME': 'My Pet Project',
+        'SHOW_INCLUDE_FILES': True,
+        'XML_OUTPUT': 'xml'
+    }
+    expected_config = {
+        'DOXYFILE': 'Doxyfile',
+
+        'FAVICON': ('favicon-dark.png', 'image/png'),
+        'LINKS_NAVBAR1': [(None, 'Pages', 'pages.html', 'pages', []),
+                          (None, 'Modules', 'modules.html', 'modules', [])],
+        # different order
+        'LINKS_NAVBAR2': [(None, 'Files', 'files.html', 'files', []),
+                          (None, 'Classes', 'annotated.html', 'annotated', [])],
+        'FINE_PRINT': 'this is "quotes"',
+        'THEME_COLOR': '#22272e',
+        'STYLESHEETS': ['a.css', 'b.css'],
+        'HTML_HEADER': None,
+        'EXTRA_FILES': ['css', 'another.png', 'hello'],
+        'PAGE_HEADER': 'this is "quotes" \'apostrophes\'',
+
+        'CLASS_INDEX_EXPAND_LEVELS': 1,
+        'CLASS_INDEX_EXPAND_INNER': False,
+        'FILE_INDEX_EXPAND_LEVELS': 1,
+
+        'M_MATH_CACHE_FILE': 'm.math.cache',
+
+        'SEARCH_DISABLED': False,
+        'SEARCH_DOWNLOAD_BINARY': False,
+        'SEARCH_BASE_URL': None,
+        'SEARCH_EXTERNAL_URL': None,
+        'SEARCH_HELP':
 """<p class="m-noindent">Search for symbols, directories, files, pages or
 modules. You can omit any prefix from the symbol or file path; adding a
 <code>:</code> or <code>/</code> suffix lists all members of given symbol or
@@ -73,19 +89,42 @@ copy a link to the result using <span class="m-label m-dim">⌘</span>
 <span class="m-label m-dim">L</span> while <span class="m-label m-dim">⌘</span>
 <span class="m-label m-dim">M</span> produces a Markdown link.</p>
 """,
-            'M_SHOW_UNDOCUMENTED': False,
-            'M_THEME_COLOR': '#22272e',
-            'M_VERSION_LABELS': False,
-            'OUTPUT_DIRECTORY': '',
-            'PROJECT_BRIEF': 'is cool',
-            'PROJECT_LOGO': '',
-            'PROJECT_NAME': 'My Pet Project',
-            'SHOW_INCLUDE_FILES': True,
-            'XML_OUTPUT': 'xml'
-        })
+
+        'SHOW_UNDOCUMENTED': False,
+        'VERSION_LABELS': False,
+    }
+
+    def test(self):
+        # Basically mirroring what's in the Doxyfile-legacy. It's silly because
+        # we don't need to check most of these here anyway but whatever. To
+        # make this a bit saner, all existing tests are using the
+        # "legacy Doxyfile" config anyway, so it should be tested more than
+        # enough... until we port away from that. This should get then further
+        # extended to cover the cases that are no longer tested by other code.
+        state = State({**copy.deepcopy(default_config), **{
+            'EXTRA_FILES': ['css', 'another.png', 'hello'],
+            'STYLESHEETS': ['a.css', 'b.css'],
+            'PAGE_HEADER': 'this is "quotes" \'apostrophes\'',
+            'FINE_PRINT': 'this is "quotes"',
+            'LINKS_NAVBAR1': [(None, 'pages', []),
+                              (None, 'modules', [])],
+            'LINKS_NAVBAR2': [(None, 'files', []),
+                              (None, 'annotated', [])]
+        }})
+
+        parse_doxyfile(state, 'test_doxygen/doxyfile/Doxyfile')
+        self.assertEqual(state.doxyfile, self.expected_doxyfile)
+        self.assertEqual(state.config, self.expected_config)
+
+    def test_legacy(self):
+        state = State(copy.deepcopy(default_config))
+
+        parse_doxyfile(state, 'test_doxygen/doxyfile/Doxyfile-legacy')
+        self.assertEqual(state.doxyfile, self.expected_doxyfile)
+        self.assertEqual(state.config, self.expected_config)
 
     def test_subdirs(self):
-        state = State()
+        state = State(copy.deepcopy(default_config))
         with self.assertRaises(NotImplementedError):
             parse_doxyfile(state, 'test_doxygen/doxyfile/Doxyfile-subdirs')