From f13fb799356061ee0eeb8d1e4698077c3e711e15 Mon Sep 17 00:00:00 2001 From: Markus Braun Date: Fri, 2 Dec 2022 12:41:33 +0100 Subject: [PATCH 1/8] chore(devcontainer): upgraded doxygen to latest version 1.9.5 Signed-off-by: Markus Braun --- .devcontainer/Dockerfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a78e2ed..d06e74b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -39,6 +39,14 @@ RUN apt-get update \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +# Update doxygen to latest +RUN wget -c https://www.doxygen.nl/files/doxygen-1.9.5.linux.bin.tar.gz -O /tmp/doxygen.tar.gz \ + && mkdir -p /tmp/doxygen \ + && tar -xzvf /tmp/doxygen.tar.gz --strip-components=1 -C /tmp/doxygen/ \ + && mv /tmp/doxygen/bin/* /usr/bin \ + && mv /tmp/doxygen/man/man1/* /usr/share/man/man1 \ + && rm -rf /tmp/doxygen/ + # Install plantUML RUN wget -c https://netcologne.dl.sourceforge.net/project/plantuml/plantuml.jar -O /tmp/plantuml.jar && \ mkdir -p /usr/share/plantuml && \ From 8c006152eabe88fa2ebc16044c60ee42182ac663 Mon Sep 17 00:00:00 2001 From: Markus Braun Date: Fri, 2 Dec 2022 12:42:38 +0100 Subject: [PATCH 2/8] chore(vscode): added doxysphinx cleanup task to debugging config Signed-off-by: Markus Braun --- .vscode/launch.json | 3 ++- .vscode/tasks.json | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 58b2a7d..75e4ecd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,7 +16,8 @@ "${workspaceFolder}", "${workspaceFolder}/.build", "${workspaceFolder}/demo/demo.doxyfile" - ] + ], + "preLaunchTask": "doxysphinx clean demo", }, { "name": "Debug: Doxysphinx Build Command Graphviz", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 36c342c..45232c4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,6 +3,20 @@ // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ + { + "label": "doxysphinx clean demo", + "type": "shell", + "command": "poetry", + "args": [ + "run", + "doxysphinx", + "--verbosity=DEBUG", + "clean", + ".", + ".build/html", + "demo/demo.doxyfile" + ] + }, { "label": "run precommit", "type": "shell", From 21b45dfaadbaa4fb157ec881bfb102c1ce6e605a Mon Sep 17 00:00:00 2001 From: Markus Braun Date: Fri, 2 Dec 2022 12:43:50 +0100 Subject: [PATCH 3/8] chore(cli): added version to doxysphinx run output Signed-off-by: Markus Braun --- doxysphinx/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doxysphinx/cli.py b/doxysphinx/cli.py index b432789..0fac583 100644 --- a/doxysphinx/cli.py +++ b/doxysphinx/cli.py @@ -26,6 +26,7 @@ see the correct signatures. """ +import importlib.metadata as metadata import logging from dataclasses import dataclass from pathlib import Path @@ -97,7 +98,8 @@ def cli(): files. This has the implication, that the doxygen html output directory (where the rst files are generated to) has to live inside sphinx's input tree. """ - click.secho("doxysphinx", fg="bright_white") + + click.secho(f"doxysphinx v{metadata.version('doxysphinx')}", fg="bright_white") @cli.command() From 81546a7f7243fba4b8cfa9fbc5406663fbe8b489 Mon Sep 17 00:00:00 2001 From: Markus Braun Date: Wed, 7 Dec 2022 14:53:51 +0100 Subject: [PATCH 4/8] feat(parser): Now rst inline syntax is supported for sphinx roles and domains. Closes #73 docs: rewritten some parts of the docs and added a syntax guide with rst inline syntax fix(parser): fixed doxygen comment parsing comment parsing was also removing asterisk bulletpoints and other asterisk based formatting. Signed-off-by: Markus Braun --- .vscode/settings.json | 3 + demo/demo.doxyfile | 42 +- demo/demo/src/block_rst.hpp | 252 ++++++++ demo/demo/src/comment_styles.hpp | 20 +- demo/demo/src/inline_rst.hpp | 143 +++++ docs/getting_started.md | 12 +- docs/resources/doxysphinx_logo.png | Bin 0 -> 56304 bytes docs/resources/doxysphinx_logo_1280.png | Bin 0 -> 55730 bytes docs/syntax/comment_styles_syntax.md | 68 +++ docs/syntax/rst_block_syntax.md | 124 ++++ docs/syntax/rst_inline_syntax.md | 118 ++++ docs/syntax/syntax_guide.md | 12 + doxysphinx/cli.py | 4 +- doxysphinx/doxygen.py | 3 +- doxysphinx/html_parser.py | 538 +++++++++++++++--- doxysphinx/resources/custom.scss | 13 + doxysphinx/writer.py | 49 +- index.md | 2 + pyproject.toml | 5 + tests/html_parser/test_files/.gitignore | 1 + .../test_files/demo_html_1.expected.html | 40 ++ .../test_files/demo_html_1.input.html | 39 ++ .../html_tags_in_rst_content.expected.html | 32 ++ .../html_tags_in_rst_content.input.html | 30 + .../test_files/pre_to_div.expected.html | 12 + .../test_files/pre_to_div.input.html | 17 + .../sequential_pre_tags.expected.html | 32 ++ .../test_files/sequential_pre_tags.input.html | 38 ++ tests/html_parser/test_helper_functions.py | 252 ++++++++ tests/html_parser/test_html_parser.py | 56 ++ .../test_remove_doxygen_comment_prefixes.py | 41 ++ tests/html_parser/test_rst_block_processor.py | 193 +++++++ .../html_parser/test_rst_inline_processor.py | 121 ++++ .../html_parser/test_speed_doxygen_comment.py | 139 +++++ .../test_speed_rst_block_content_parser.py | 110 ++++ tests/html_parser/test_utils.py | 21 + 36 files changed, 2444 insertions(+), 138 deletions(-) create mode 100644 demo/demo/src/block_rst.hpp create mode 100644 demo/demo/src/inline_rst.hpp create mode 100644 docs/resources/doxysphinx_logo.png create mode 100644 docs/resources/doxysphinx_logo_1280.png create mode 100644 docs/syntax/comment_styles_syntax.md create mode 100644 docs/syntax/rst_block_syntax.md create mode 100644 docs/syntax/rst_inline_syntax.md create mode 100644 docs/syntax/syntax_guide.md create mode 100644 tests/html_parser/test_files/.gitignore create mode 100644 tests/html_parser/test_files/demo_html_1.expected.html create mode 100644 tests/html_parser/test_files/demo_html_1.input.html create mode 100644 tests/html_parser/test_files/html_tags_in_rst_content.expected.html create mode 100644 tests/html_parser/test_files/html_tags_in_rst_content.input.html create mode 100644 tests/html_parser/test_files/pre_to_div.expected.html create mode 100644 tests/html_parser/test_files/pre_to_div.input.html create mode 100644 tests/html_parser/test_files/sequential_pre_tags.expected.html create mode 100644 tests/html_parser/test_files/sequential_pre_tags.input.html create mode 100644 tests/html_parser/test_helper_functions.py create mode 100644 tests/html_parser/test_html_parser.py create mode 100644 tests/html_parser/test_remove_doxygen_comment_prefixes.py create mode 100644 tests/html_parser/test_rst_block_processor.py create mode 100644 tests/html_parser/test_rst_inline_processor.py create mode 100644 tests/html_parser/test_speed_doxygen_comment.py create mode 100644 tests/html_parser/test_speed_rst_block_content_parser.py create mode 100644 tests/html_parser/test_utils.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 3610295..91b2544 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -42,6 +42,9 @@ "source.organizeImports": true } }, + "[html]": { + "editor.formatOnSave": false + }, "editor.minimap.maxColumn": 120, "editor.rulers": [ { diff --git a/demo/demo.doxyfile b/demo/demo.doxyfile index af1fed4..33dd992 100644 --- a/demo/demo.doxyfile +++ b/demo/demo.doxyfile @@ -274,6 +274,8 @@ TAB_SIZE = 4 ALIASES = "rst=\verbatim embed:rst:leading-asterisk" \ endrst=\endverbatim +ALIASES += rst_inline="\verbatim embed:rst:inline" +ALIASES += endrst_inline="\endverbatim" # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. For @@ -1681,17 +1683,6 @@ HTML_FORMULA_FORMAT = png FORMULA_FONTSIZE = 10 -# Use the FORMULA_TRANSPARENT tag to determine whether or not the images -# generated for formulas are transparent PNGs. Transparent PNGs are not -# supported properly for IE 6.0, but are supported on all modern browsers. -# -# Note that when changing this option you need to delete any form_*.png files in -# the HTML output directory before the changes have effect. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. - -FORMULA_TRANSPARENT = YES - # The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands # to create new LaTeX commands to be used in formulas as building blocks. See # the section "Including formulas" for details. @@ -2406,23 +2397,6 @@ HAVE_DOT = YES DOT_NUM_THREADS = 0 -# When you want a differently looking font in the dot files that doxygen -# generates you can specify the font name using DOT_FONTNAME. You need to make -# sure dot is able to find the font, which can be done by putting it in a -# standard location or by setting the DOTFONTPATH environment variable or by -# setting DOT_FONTPATH to the directory containing the font. -# The default value is: Helvetica. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_FONTNAME = Helvetica - -# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of -# dot graphs. -# Minimum value: 4, maximum value: 24, default value: 10. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_FONTSIZE = 10 - # By default doxygen will tell dot to use the default font as specified with # DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set # the path where dot can find it using this tag. @@ -2667,18 +2641,6 @@ DOT_GRAPH_MAX_NODES = 50 MAX_DOT_GRAPH_DEPTH = 0 -# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent -# background. This is disabled by default, because dot on Windows does not seem -# to support this out of the box. -# -# Warning: Depending on the platform used, enabling this option may lead to -# badly anti-aliased labels on the edges of a graph (i.e. they become hard to -# read). -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_TRANSPARENT = YES - # Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output # files in one run (i.e. multiple -o and -T options on the command line). This # makes dot run faster, but since only newer versions of dot (>1.8.10) support diff --git a/demo/demo/src/block_rst.hpp b/demo/demo/src/block_rst.hpp new file mode 100644 index 0000000..55687c2 --- /dev/null +++ b/demo/demo/src/block_rst.hpp @@ -0,0 +1,252 @@ +// ===================================================================================== +// C O P Y R I G H T +// ------------------------------------------------------------------------------------- +// Copyright (c) 2022 by Robert Bosch GmbH. All rights reserved. +// +// Author(s): +// - Markus Braun, :em engineering methods AG (contracted by Robert Bosch GmbH) +// ===================================================================================== +#ifndef DEMO__BLOCK_RST_INCLUDED +#define DEMO__BLOCK_RST_INCLUDED + +namespace doxysphinx +{ +namespace doxygen +{ + /// @brief Demonstration of block rst usage. See also `:doc:"Rst Block Syntax Documentation "`. + /// + class BlockRst + { + public: + /// @brief RestructuredText block with markdown fences. + /// + /// + /// **standard fences** + /// + /// *Syntax* + ///
+      /// /// ```
+      /// /// {rst}
+      /// /// .. tip::
+      /// ///
+      /// ///   If you see a tip around this text it worked!
+      /// /// ```
+      /// 
+ /// + /// *Example* + /// ``` + /// {rst} + /// .. tip:: + /// + /// If you see a tip around this text it worked! + /// ``` + /// + /// **special fences** + /// + /// *Syntax* + ///
+      /// /// ~~~~~~~~~~~~~~~~
+      /// /// {rst}
+      /// /// .. tip::
+      /// ///
+      /// ///    If you see a tip around this text it worked!
+      /// /// ~~~~~~~~~~~~~~~~
+      /// 
+ /// + /// *Example* + /// ~~~~~~~~~~~~~~~~ + /// {rst} + /// .. tip:: + /// + /// If you see a tip around this text it worked! + /// ~~~~~~~~~~~~~~~~ + /// + /// **autodetecting directives** + /// + /// *Syntax* + ///
+      /// /// ```
+      /// /// .. tip::
+      /// ///
+      /// ///    If you see a tip around this text it worked!
+      /// /// ```
+      /// 
+ /// + /// *Example* + /// ``` + /// .. tip:: + /// + /// If you see a tip around this text it worked! + /// ``` + void block_rst_via_markdown(); + + /// @brief RestructuredText block doxygen verbatim special command. + /// + /// **verbatim command** + /// + /// *Syntax* + ///
+      /// /// \\verbatim {rst}
+      /// /// .. tip::
+      /// ///
+      /// ///    If you see a tip around this text it worked!
+      /// /// \\endverbatim
+      /// 
+ /// + /// *Example* + /// \verbatim {rst} + /// .. tip:: + /// + /// If you see a tip around this text it worked! + /// \endverbatim + /// + /// + /// **verbatim command (breathe compatibility)** + /// + /// *Syntax* + ///
+      /// /// \@verbatim embed:rst:leading-slashes
+      /// /// .. tip::
+      /// ///
+      /// ///    If you see a tip around this text it worked!
+      /// /// \@endverbatim
+      /// 
+ /// + /// *Example* + /// \verbatim embed:rst:leading-slashes + /// .. tip:: + /// + /// If you see a tip around this text it worked! + /// \endverbatim + /// + /// + /// **autodetecting directives** + /// + /// *Syntax* + ///
+      /// /// \\verbatim
+      /// /// .. tip::
+      /// ///
+      /// ///    If you see a tip around this text it worked!
+      /// /// \\endverbatim
+      /// 
+ /// + /// *Example* + /// \verbatim + /// .. tip:: + /// + /// If you see a tip around this text it worked! + /// \endverbatim + /// + void block_rst_via_verbatim(); + + /// @brief RestructuredText block doxygen code special command. + /// + /// **code command** + /// + /// *Syntax* + ///
+      /// /// \\code
+      /// /// {rst}
+      /// /// .. tip::
+      /// ///
+      /// ///    If you see a tip around this text it worked!
+      /// /// \\endcode
+      /// 
+ /// + /// *Example* + /// \code + /// {rst} + /// .. tip:: + /// + /// If you see a tip around this text it worked! + /// \endcode + /// + /// + /// **code command (breathe compatibility)** + /// + /// *Syntax* + ///
+      /// /// \@code embed:rst:leading-slashes
+      /// /// .. tip::
+      /// ///
+      /// ///    If you see a tip around this text it worked!
+      /// /// \@code
+      /// 
+ /// + /// *Example* + /// @code embed:rst:leading-slashes + /// .. tip:: + /// + /// If you see a tip around this text it worked! + /// @endcode + /// + /// + /// **autodetecting directives** + /// + /// *Syntax* + ///
+      /// /// \\code
+      /// /// .. tip::
+      /// ///
+      /// ///    If you see a tip around this text it worked!
+      /// /// \\endcode
+      /// 
+ /// + /// *Example* + /// \code + /// .. tip:: + /// + /// If you see a tip around this text it worked! + /// \endcode + /// + void block_rst_via_code(); + + /// @brief RestructuredText block with html pre tag in doxygen. + /// + /// **<pre>-html-element** + /// + /// *Syntax* + ///
+      /// /// <pre> {rst}
+      /// /// .. tip::
+      /// ///
+      /// ///    If you see a tip around this text it worked!
+      /// /// </pre>
+      /// 
+ /// + /// *Example* + ///
 {rst}
+      /// .. tip::
+      ///
+      ///    If you see a tip around this text it worked!
+      /// 
+ /// + /// + /// **autodetecting directives** + /// + /// *Syntax* + ///
+      /// /// <pre>
+      /// /// .. tip::
+      /// ///
+      /// ///    If you see a tip around this text it worked!
+      /// /// </pre>
+      /// 
+ /// + /// *Example* + ///
+      /// .. tip::
+      ///
+      ///    If you see a tip around this text it worked!
+      /// 
+ /// + void block_rst_via_pre(); + + + }; // BlockRst + +} // doxygen +} // doxysphinx + +#endif diff --git a/demo/demo/src/comment_styles.hpp b/demo/demo/src/comment_styles.hpp index 7505db5..12a79ce 100644 --- a/demo/demo/src/comment_styles.hpp +++ b/demo/demo/src/comment_styles.hpp @@ -21,20 +21,8 @@ namespace doxysphinx namespace doxygen { -/** - @verbatim embed:rst - - Doxygen Comment Style Testfile - ============================== - - This file tests the different doxygen comment styles and the embedding of rst elements. - - It checks for the comment styles as documented `here in doxygen documentation `_. - - For integrating rst into doxygen we use the verbatim command as described `here in the breathe docs `_. - - @endverbatim -*/ +/// @brief Demonstration of doxygen comment style usage. See also `:doc:"Comment Syntax Documentation "`. +/// class CommentStyles { public: @@ -202,7 +190,7 @@ class CommentStyles /// \endverbatim void ensure_slash_style_comments_are_working_as_expected() const = 0; - /// \verbatim embed:rst:asterisk + /// \verbatim embed:rst:leading-asterisk /// .. admonition:: What you should see here /// /// This text should be in an admonition box. It was generated from a doxygen slash comment **without any special identation but with asterisk embed command**. @@ -211,7 +199,7 @@ class CommentStyles /// /// .. code:: cpp /// - /// /// \ verbatim embed:rst:asterisk + /// /// \ verbatim embed:rst:leading-asterisk /// /// /// /// ...rst-content-here... /// /// diff --git a/demo/demo/src/inline_rst.hpp b/demo/demo/src/inline_rst.hpp new file mode 100644 index 0000000..93e7ec6 --- /dev/null +++ b/demo/demo/src/inline_rst.hpp @@ -0,0 +1,143 @@ +// ===================================================================================== +// C O P Y R I G H T +// ------------------------------------------------------------------------------------- +// Copyright (c) 2022 by Robert Bosch GmbH. All rights reserved. +// +// Author(s): +// - Markus Braun, :em engineering methods AG (contracted by Robert Bosch GmbH) +// ===================================================================================== +#ifndef DEMO__INLINE_RST_INCLUDED +#define DEMO__INLINE_RST_INCLUDED + +namespace doxysphinx +{ +namespace doxygen +{ + /// @brief Demonstration of inline rst usage. See also `:doc:"Rst Inline Syntax Documentation "`. + class InlineRst + { + public: + /// @brief shows how to use inline rst roles in doxygen comments + /// + /// #### syntax + /// use this syntax in your doxygen comments (we left out the comment prefix [///, //! etc.] here). + /// + /// ```plain + /// 1) A html code element with quotes like this - :doc:"This is an inline link to the Main Documentation " - should work. + /// + /// 2) A html code element with ticks like this - :doc:'This is an inline link to the Main Documentation ' - should work. + /// + /// 3) A html code element with escaped backticks like this - :doc:\`This is an inline link to the Main Documentation \` - should work. + /// + /// 4) A tt element with quotes like this - :doc:"This is an inline link to the Main Documentation " - should work. + /// + /// 5) A tt element with ticks like this - :doc:'This is an inline link to the Main Documentation ' - should work. + /// + /// 6) A tt element with escaped backticks like this - :doc:\`This is an inline link to the Main Documentation \` - should work. + /// + /// 7) A markdown inline statement with quotes like this - `:doc:"This is an inline link to the Main Documentation "` - should work. + /// + /// Not working: + /// + /// 8!) A markdown inline statement with ticks like this - `:doc:'This is an inline link to the Main Documentation '` - won't work because + /// doxygen generates and symbols instead of \em code-tags in this case (which is strange btw...). + /// + /// 9!) Escaping backticks in markdown inline statement - `:doc:\`This is an inline link to the Main Documentation \`` - won't work because + /// doxygen preprocesses that in a strange way. + /// ``` + /// + /// #### visual example + /// + /// 1) A html code element with quotes like this - :doc:"This is an inline link to the Main Documentation " - should work. + /// + /// 2) A html code element with ticks like this - :doc:'This is an inline link to the Main Documentation ' - should work. + /// + /// 3) A html code element with escaped backticks like this - :doc:\`This is an inline link to the Main Documentation \` - should work. + /// + /// 4) A tt element with quotes like this - :doc:"This is an inline link to the Main Documentation " - should work. + /// + /// 5) A tt element with ticks like this - :doc:'This is an inline link to the Main Documentation ' - should work. + /// + /// 6) A tt element with escaped backticks like this - :doc:\`This is an inline link to the Main Documentation \` - should work. + /// + /// 7) A markdown inline statement with quotes like this - `:doc:"This is an inline link to the Main Documentation "` - should work. + /// + /// Not working + /// + /// 8!) A markdown inline statement with ticks like this - `:doc:'This is an inline link to the Main Documentation '` - won't work because + /// doxygen generates and symbols instead of \em code-tags in this case (which is strange btw...). + /// + /// 9!) Escaping backticks in markdown inline statement - `:doc:\`This is an inline link to the Main Documentation \`` - won't work because + /// doxygen preprocesses that in a strange way. + void inline_rst_syntax_in_comments(); + + /// @brief shows how to use inline rst roles in doxygen list comments + /// + /// #### syntax + /// use this syntax in your doxygen comments (we left out the comment prefix [///, //! etc.] here). + /// + /// ```plain + /// 1. A html code element with quotes like this - :doc:"This is an inline link to the Main Documentation " - should work. + /// 2. A html code element with ticks like this - :doc:'This is an inline link to the Main Documentation " - should work. + /// 3. A html code element with escaped backticks like this - :doc:\`This is an inline link to the Main Documentation \` - should work. + /// 4. A tt element with quotes like this - :doc:"This is an inline link to the Main Documentation " - should work. + /// 5. A tt element with ticks like this - :doc:'This is an inline link to the Main Documentation ' - should work. + /// 6. A tt element with escaped backticks like this - :doc:\`This is an inline link to the Main Documentation \` - should work. + /// 7. A markdown inline statement with quotes like this - `:doc:"This is an inline link to the Main Documentation "` - should work. + /// ``` + /// + /// #### visual example + /// + /// 1. A html code element with quotes like this - :doc:"This is an inline link to the Main Documentation " - should work. + /// 2. A html code element with ticks like this - :doc:'This is an inline link to the Main Documentation ' - should work. + /// 3. A html code element with escaped backticks like this - :doc:\`This is an inline link to the Main Documentation \` - should work. + /// 4. A tt element with quotes like this - :doc:"This is an inline link to the Main Documentation " - should work. + /// 5. A tt element with ticks like this - :doc:'This is an inline link to the Main Documentation ' - should work. + /// 6. A tt element with escaped backticks like this - :doc:\`This is an inline link to the Main Documentation \` - should work. + /// 7. A markdown inline statement with quotes like this - `:doc:"This is an inline link to the Main Documentation "` - should work. + void inline_rst_syntax_in_lists(); + + /// @brief shows how to use inline rst roles in doxygen list comments + /// + /// #### syntax + /// use this syntax in your doxygen comments (we left out the comment prefix [///, //! etc.] here). + /// + /// ```plain + /// markdown table: + /// + /// First Header | Second Header + /// ------------- | ----------------------------------- + /// Content Cell | `:doc:"Main Documentation "` + /// Content Cell | Content Cell + /// + /// html table: + /// + /// + /// + /// + ///
First HeaderSecond Header>
Content Cell`:doc:"Main Documentation "`
Content CellContent Cell
+ /// ``` + /// + /// #### visual example + /// + /// markdown table: + /// + /// First Header | Second Header + /// ------------- | ----------------------------------- + /// Content Cell | `:doc:"Main Documentation "` + /// Content Cell | Content Cell + /// + /// html table: + /// + /// + /// + /// + ///
First HeaderSecond Header
Content Cell`:doc:"Main Documentation "`
Content CellContent Cell
+ void inline_rst_syntax_in_tables(); + + }; // InlineRst + +} // doxygen +} // doxysphinx + +#endif diff --git a/docs/getting_started.md b/docs/getting_started.md index ee72b94..8fb2629 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -150,8 +150,6 @@ GENERATE_TREEVIEW = NO # Deactivate doxygens own treeview (as it doesn't DISABLE_INDEX = NO # Menu data is crucial for our TOC generation so it mustn't be disabled GENERATE_HTML = YES # Keep sure that you generate HTML which needed for doxysphinx -ALIASES = "rst=\verbatim embed:rst:leading-asterisk" \ - endrst=\endverbatim # This allows you to use rst blocks inside doxygen comments with @rst and @endrst CREATE_SUBDIRS = NO # NO is the default value and it should be no because doxysphinx can't handle subdirs right now. ``` @@ -193,6 +191,10 @@ DOT_IMAGE_FORMAT = svg # generates nicer svg images DOT_TRANSPARENT = YES # generate transparent images INTERACTIVE_SVG = YES # to be able to scroll and zoom into big images +# if you want to use aliases instead of markdown fences for commenting (see syntax guide) you have to add +# something like this (which doesn't hurt either): +ALIASES = "rst=\verbatim embed:rst:leading-asterisk" \ + endrst=\endverbatim # This allows you to use rst blocks inside doxygen comments with @rst and @endrst ``` ````{tip} @@ -300,6 +302,9 @@ Car(Engine& engine, Color& color) {}; ``` Note the `@rst` and `@endrst` tags. Inside these tags you can write any rst code. + +See also the [Syntax Guide](syntax/syntax_guide.md) for a complete documentation on how to comment for doxysphinx. + Now run doxygen, doxysphinx and sphinx and look at the generated documentation. You should see something like this: @@ -314,9 +319,12 @@ this: Further reading: +* To get to know the doxysphinx comment syntax -> see our [syntax guide](syntax/syntax_guide.md). * Maybe you want to know more about the inner workings? -> head over to the [reference](inner_workings.md) section. * Or look at some examples? -> [linking to doxygen](linking_to_doxygen.md). * Or do you want to contribute and bring doxysphinx to the next level? Read the [contributors guide](../CONTRIBUTE.md) and the [Developer Quickstart](dev_guide.md). Or just start documenting 😀. + +test test *test* test diff --git a/docs/resources/doxysphinx_logo.png b/docs/resources/doxysphinx_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..46188ae6c51197fb7aa0a4c1200cbdffaaf811ac GIT binary patch literal 56304 zcmeFYWn9yL`#6e$@C6B#5+s!F?gjxzcZamJ(ltT>MWl12!065aqXg*zGIFGJcZ?c4 zq$2j)`K|QCzv}o zMKq5wKaV}+0J^xC6^Lu|4GW773-DG}*Ee%-At2DyD+_&$+=PME++=dswG#kCdQsR7 z#WJ~|`a$FBKdQKYOQjX+St_}Tw^nTTSFl_6*ghA3V=s%Uj9_xn(){UZEc2GroqQc` zmyn{O!hV*(1oDvTkcK<5w%IW855U&W110c=&jLF(`CZ)YHd;H#|G^;izw3V~@c&B+ zJY^P|z{ZkcEddE04wLnys|6*1QyE<5hL)`vscGKPnr6A87XIquVu+Je*{$Is!9)Y8 zQiSIwWDBy+uKucw>D+>$j{TAjF|HkZ=A@Mkj@EQz8 z4tVY@(fR=3x$e|#SFo^HY5KdVjjPdl#0i{O4`Ws%D25mw{`;f7352Xg;;r&P?80uS z-KJgL@{7bowW^n3IEL7fub)2p*K0CHB^jV=``FtyQ2|kb1dF1L4a2eTm@OEic%J?n z;r0Ax(T&i`gze5(CPEW!aVTRg$NLwe|MP-v{=t=V8+bNwwgnL$sWbY@;%~w|4Eq1T zKpdsDd0`DBk)=Dr zNTB8WsSbV@63Wv6V;{; z-+eL>>5!!4ZD`4cL3%ypKS-~?u3g3ZGxUHaN{vlM`v2Hi;;}3CmaX!GYs^NrVgLC6 zb3yt)GHt={`1-8C<%Yn1f3Emx9i|K7lo0K|evE4ojEo zl=JWkj4mgMb@BWgXj1gZjd9D`bt!mQR0yS0&v|!lHGx4(nd-l2DR2Cp@k$6vmQUOy zjW|4Lttu!p6nuac0#~58cj*|VJAuQW`@3DNGI7G&?Y~Kq)j0z<4tipV6df?Aoc$M- zJH_$0gCGO;Y7)*ccUa3 zzBVMbL`h0yPwwQr3=Z)%7J}!77D$0j~ffC+JqzV-eUc0g?#@H zo|w!3z4(98fRuLUAy!ghiB5)+C-s!1)b1}sfHuV|ig$wX4rm!6Z#S2P1=s}R^my*q z-GWOYqE;2&L*o~&fdCA-HM1WN!ntVzES-5aLh=LXrTOBfn{fnPgVGd&sxzp!P9CiZ zO{wP33(hhE#P6I;)^`#D>xS+&z6-tcmvtQ-oZO~qU?&Uq_ZVHuzU}{n<3`g}^bb}X z*oIF^>qS{v02Ddi|2~Pq&&~OV)p974_=GABJD)KBl7Yd279^u>Z)248>6Egz9tUoo zrevFJ;P`ALBHGd6pD0ukj|da42k+n3cs6e|!K*S_g?y zwK+|N)?c#(lBlzlok+cmOQ~PkoVbKJs6Us!Zh;+(hRAPeeWjl}75iPjvx~J#PqRZ< z;&8bANTi&2+luLCK1lAA?rhJVOrRhxDI^5�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 literal 0 HcmV?d00001 diff --git a/docs/syntax/comment_styles_syntax.md b/docs/syntax/comment_styles_syntax.md new file mode 100644 index 0000000..875ad6e --- /dev/null +++ b/docs/syntax/comment_styles_syntax.md @@ -0,0 +1,68 @@ +# Comment Syntax + +## Supported Doxygen Comment Styles + +We're supporting the following Doxygen comment styles (see also +[Doxygen: Comment Blocks for C-Style Languages](https://doxygen.nl/manual/docblocks.html#cppblock)). + +### Triple-Slashes + +```cpp +/// @brief brings the unicorns back +/// +/// It does that with an extraordinary special top secret device of extraterrestrial origin. +void bring_the_unicorns_back(); +``` + +### Javadoc + +```cpp +/** + * @brief brings the unicorns back + * + * It does that with an extraordinary special top secret device of extraterrestrial origin. + */ +void bring_the_unicorns_back(); +``` + +or + +```cpp +/** + @brief brings the unicorns back + + It does that with an extraordinary special top secret device of extraterrestrial origin. + */ +void bring_the_unicorns_back(); +``` + +### Qt + +```cpp +/*! + * @brief brings the unicorns back + * + * It does that with an extraordinary special top secret device of extraterrestrial origin. + */ +void bring_the_unicorns_back(); +``` + +or + +```cpp +/*! + @brief brings the unicorns back + + It does that with an extraordinary special top secret device of extraterrestrial origin. + */ +void bring_the_unicorns_back(); +``` + +### Double-Slashes-With-Exclamation-Marks + +```cpp +//! @brief brings the unicorns back +//! +//! It does that with an extraordinary special top secret device of extraterrestrial origin. +void bring_the_unicorns_back(); +``` diff --git a/docs/syntax/rst_block_syntax.md b/docs/syntax/rst_block_syntax.md new file mode 100644 index 0000000..92efec2 --- /dev/null +++ b/docs/syntax/rst_block_syntax.md @@ -0,0 +1,124 @@ +# Rst Block Syntax + +## TLDR; Recommended Syntax + +- if you want to use the shortest possible syntax use [](#markdown-fences) with directive autodetection. +- if you're coming from breathe and already have your code commented with breathe markers or if you want + to have maximum compatibility: use [](doxygen-aliases). + +However if you experience any problems with doxygen parsing etc. you might try one of the other options described +in [](supported-rst-block-delimiters-in-doxygen-comments). + +## Markers + +For doxysphinx to be able to identify a rst block we only need to have some kind of "verbatim block" in html +output and a special marker at the beginning of the content. + +The marker can be one of these: + +- `{rst}` -> our (doxysphinx) own marker +- `embed:rst` +- `embed:rst:leading-asterisk` +- `embed:rst:leading-slashes` -> breathe compatibility markers + +After any marker there has to be a new line (content can start at next line). + +As chances are big that you just want to use a sphinx directive we've also got a **shortcut syntax** - +if the pre content starts with a directive you can leave out the `{rst}`-line, e.g. + +```cpp +/// ``` +/// .. directive:: title +/// DIRECTIVE CONTENT +/// ``` +``` + +will also be identified by doxysphinx as a rst block (and processed). + +## Supported rst block delimiters in doxygen comments + +Technically doxysphinx searches for `
`- or `
`-elements in doxygen html output +because these are the elements it uses for verbatim code block content. There are several ways in doxygen to +create these kind of elements: + +### Markdown Fences + +You can use the markdown code fences syntax as follows (you need to have markdown enabled in doxygen to use it): + +```cpp +/// ... +/// +/// ``` +/// {rst} +/// enter your rst content, +/// like directives, free text rst content, +/// etc... +/// ``` +/// +/// ... +``` + +```{warning} + + In markdown it's typical to have a language identifier right behind the beginning "fence". + Something like \`\`\`cpp for example. However the doxygen markdown parser will swallow anything + behind the delimiters (which means we couldn't make use of information there). So please do not fall into + the trap trying to start a rst block with e.g. \`\`\`{rst}. The `{rst}` marker (or any content + used by doxysphinx) has to be on the next line. (This is only true if you use markdown code fences - not for + the other options below). +``` + +### `\verbatim` special command + +You can use the verbatim special command in doxygen to create a pre-element: + +```cpp +/// ... +/// +/// \verbatim {rst} +/// enter your rst content, +/// like directives, free text rst content, +/// etc... +/// \endverbatim +/// +/// ... +``` + +### `
`-html-element
+
+As you can also use html in doxygen you can use the html `
`-element directly:
+
+```cpp
+/// ...
+///
+/// 
 {rst}
+/// enter your rst content,
+/// like directives, free text rst content,
+/// etc...
+/// 
+/// +/// ... +``` + +### Doxygen aliases + +As another shortcut you can also use doxygen aliases to create your own rst-block delimiters: + +```text +ALIAS = "rst=\verbatim embed:rst" +ALIAS += "endrst=\endverbatim" +``` + +And then use the alias like this: + +```cpp +/// ... +/// +/// \rst +/// enter your rst content, +/// like directives, free text rst content, +/// etc... +/// \endrst +/// +/// ... +``` diff --git a/docs/syntax/rst_inline_syntax.md b/docs/syntax/rst_inline_syntax.md new file mode 100644 index 0000000..dd05f3b --- /dev/null +++ b/docs/syntax/rst_inline_syntax.md @@ -0,0 +1,118 @@ +# Rst Inline Syntax + +## TLDR; Recommended Syntax + +Use the following (note that you have to replace the backticks usually used in rst/sphinx with quotes): + +```cpp +/// lorem ipsum, `:role:"content of the role"` dolor sit... +``` + +e.g. + +```cpp +/// Here you can find the `:doc:"Main Documentation "`. Please read it carefully. +``` + +This will work only if markdown support is activated in doxygen (highly recommended). +Futhermore, please note that **you can only use sphinx roles and domains** in the inline syntax for now +(reasoning see below). + +See below in the methods documentation for other options if you have markdown support disabled. + +## Technical details + +Skip this section if you're not interested in the technical details... + +Inline rst is a major problem because of the following: + +1. Paragraphs all over the place + + Doxygen uses paragraph `

`-html-tags for it's content. Paragraph tags cannot have other block-level + tags inside them (even no other paragraph tags). The browsers (chromium-based, firefox etc.) are quite + aggressive in fixing bad nestings here (just to be able to display a page) so e.g. if a nested `

`-tag + is noticed the browsers will close the outer `

`-tag right before the inner `

`-tag. This will linearize + the `

`-tags and the page could be rendered. + + When we now split our content for mixed rst content as described in `:doc:"/docs/inner_workings"` we end + up having raw-html blocks and inline-rst blocks (and also other rst blocks but that doesn't matter here). + Sphinx will automagically put `

`-tags around the inline-rst-block - it's doing that around all pure + text based content and we cannot change that. + + Most of the time this results in an html structure with nested p tags which will be "fixed" by the + browsers on loading/rendering of the html page. Why is this a problem? because we cannot style + (in a css sense) away the blockiness if we have only sibling `

`-tags. + Also we cannot fix the final html structure because we're too early in the process. We can only create rsts + which will then be picked up by sphinx to create html. + +2. Doxygen interpretation/preprocessing + + The main use case for inline rst are sphinx roles which are normally written in a form like: + + ```plain + :role_name:`role_content` + ``` + + but doxygens internal markdown support will parse the backticks as markdown inline code block and renders + code-tags all over the place then. + +the following solutions/hacks have been applied to overcome the problems: + +1. If we encounter a sphinx role in doxysphinx during original doxygen html parsing we change it's + parent html tag from `

`-tag to `

`-tag (because divs can have nested content). We also add a css + class which we use to style the "blockiness" away (display:inline). The technical implementation is + has more complexity - if you're interested just look into the code. + +2. Doxysphinx scans the html for ``-tags but that's not enough. For doxysphinx to consider a code tag as inline + sphinx snippet it has to be in the format ``:role:`content```. The content can be delimited not only + by backticks. + Backticks are also markdown's verbatim inline delimiters and therefore can only be used when escaped (and even + then they create problems with doxygen's way of parsing). + Therefore we're also supporting quotes and ticks or no quotes at all (in that case we put them automatically after the :role: part and before the end). + + The implication is that **you cannot use anything other than roles/domains for inline rst**. In practice this + means that you cannot use rst's external link syntax and references for now, which is however so cryptic that + we're quite sure that you would rather consider using doxygens link command or just a markdown link. + +## Supported rst inline delimiters in doxygen comments + +Technically doxysphinx searches for `
`- or `
`-elements in doxygen html output +because these are the elements it uses for verbatim code block content. There are several ways in doxygen to +create these kind of elements: + +### markdown inline block + +You can use markdown inline code syntax: + +```cpp +/// A markdown inline statement with quotes like this - `:doc:"Main Documentation "` - will work. +``` + +```{warning} +The role content delimiter has to be a quote ("). Ticks and escaped backticks won't work with markdown inline +code syntax because of doxygens parser. +``` + +### ``-html-element + +You can also use a html ``-element: + +```cpp +/// A html code element with quotes like this - :doc:"Main Documentation " - will work. +/// +/// A html code element with ticks like this - :doc:'Main Documentation ' - will work. +/// +/// A html code element with escaped backticks like this - :doc:\`Main Documentation \` - will work. +``` + +### ``-html-element + +You can also use a html ``-element: + +```cpp +/// A html tt element with quotes like this - :doc:"Main Documentation " - will work. +/// +/// A html tt element with ticks like this - :doc:'Main Documentation ' - will work. +/// +/// A html tt element with escaped backticks like this - :doc:\`Main Documentation \` - will work. +``` diff --git a/docs/syntax/syntax_guide.md b/docs/syntax/syntax_guide.md new file mode 100644 index 0000000..8749658 --- /dev/null +++ b/docs/syntax/syntax_guide.md @@ -0,0 +1,12 @@ +# Syntax Guide + +Here you can see how you have to write your comments in doxygen for doxysphinx to pick them up. + +```{toctree} +--- +maxdepth: 2 +--- +rst_block_syntax +rst_inline_syntax +comment_styles_syntax +``` diff --git a/doxysphinx/cli.py b/doxysphinx/cli.py index 0fac583..a0388e1 100644 --- a/doxysphinx/cli.py +++ b/doxysphinx/cli.py @@ -91,14 +91,12 @@ def _(function): @click.version_option() @click_log.simple_verbosity_option(_logger) def cli(): - """ - Integrates doxygen html documentation with sphinx. + """Integrates doxygen html documentation with sphinx. Doxysphinx typically should run right after doxygen. It will generate rst files out of doxygen's html files. This has the implication, that the doxygen html output directory (where the rst files are generated to) has to live inside sphinx's input tree. """ - click.secho(f"doxysphinx v{metadata.version('doxysphinx')}", fg="bright_white") diff --git a/doxysphinx/doxygen.py b/doxysphinx/doxygen.py index c4c4d10..3626735 100644 --- a/doxysphinx/doxygen.py +++ b/doxysphinx/doxygen.py @@ -145,7 +145,7 @@ class DoxygenSettingsValidator: "OUTPUT_DIRECTORY": "", "GENERATE_TREEVIEW": "NO", "DISABLE_INDEX": "NO", - "ALIASES": ["rst=\\verbatim embed:rst:leading-asterisk", "endrst=\\endverbatim"], + # "ALIASES": ["rst=\\verbatim embed:rst:leading-asterisk", "endrst=\\endverbatim"], "GENERATE_HTML": "YES", "CREATE_SUBDIRS": "NO", } @@ -157,7 +157,6 @@ class DoxygenSettingsValidator: optional_settings = { "SEARCHENGINE": "NO", "DOT_IMAGE_FORMAT": "svg", - "DOT_TRANSPARENT": "YES", "INTERACTIVE_SVG": "YES", "GENERATE_TAGFILE": "", } diff --git a/doxysphinx/html_parser.py b/doxysphinx/html_parser.py index b69e489..49c0020 100644 --- a/doxysphinx/html_parser.py +++ b/doxysphinx/html_parser.py @@ -13,12 +13,16 @@ must also transform rst snippets into -nodes. """ +import logging +import re from dataclasses import dataclass +from functools import lru_cache from pathlib import Path -from typing import Iterator, List, Protocol, Union +from textwrap import dedent +from typing import List, Optional, Protocol, Set -from lxml import etree # nosec: B410 -from lxml.etree import _ElementTree # nosec: B410 +from lxml import html as etree # nosec: B410 +from lxml.etree import _Element, _ElementTree # nosec: B410 @dataclass @@ -40,9 +44,8 @@ class HtmlParseResult: """The document title. This is the title that is visible e.g. in sphinx menu structure. """ - contains_rst: bool - """Whether the tree contains any rst snippet (this is needed for rst generation - in a writer) + used_snippet_formats: Optional[Set[str]] + """The list of snippet format that are used inside the html tree if any. """ tree: _ElementTree """The html/xml element tree. @@ -63,7 +66,6 @@ def __init__(self, source_directory: Path): :param source_directory: the parsing source directory (this is maybe necessary in some cases for resolvingrelative paths) """ - pass def parse(self, file: Path) -> HtmlParseResult: """Parse a html file. @@ -80,9 +82,398 @@ def parse(self, file: Path) -> HtmlParseResult: raise NotImplementedError +class ElementProcessor(Protocol): + """An ElementProcessor processes specific html elements, one at a time. + + Typically this is used to either clean up or transform the elements into a neutralized format. + """ + + elements: List[str] = [] + """A list of html element names this processor can process. + + This is for pre-filtering html elements (an optimization). This processors try_process method + is only called on these elements. + """ + + is_final: bool = True + """Whether other processors should be called after this one. + + With a "final processor" (is_final == True) processing of an element stops (no other processors considered) + once the try_process method returns True. + """ + + format: str = "None" + """The format this element processor processes... like 'rst', 'md' etc.""" + + def try_process(self, element: _Element) -> bool: + """tries to process an element. + + :param element: The element to check and process + :return: Whether the "processor did it's thing"/"processing was applied" (True) or not (False) + """ + return False + + +class RstInlineProcessor: + """Element Processor for inline rst elements.""" + + elements = ["code"] + format = "rst" + is_final = True + + rst_role_regex = re.compile( + r":(?P[A-Za-z0-9-_:]*?):[`'\"](?P.*?)[`'\"]", re.MULTILINE | re.DOTALL + ) + + def try_process(self, element: _Element) -> bool: + """Tries to process an rst inline element into a neutralized format. + + :param element: The html element to process + :return: True if the element was processed else False + """ + # check for content + if not element.text: + return False + + # check if syntax matches sphinx/rst role + normalized_content = element.text.strip() + match = self.rst_role_regex.match(normalized_content) + if match is None: + return False + + role = match.group("role_name") + content = match.group("role_content") + + element.tag = "snippet" + element.text = f":{role}:`{content}`" + + _ensure_newline_before_element(element) + _ensure_newline_after_element(element) + + element.attrib["type"] = "rst:inline" + + # and here the "secret ingredient" to get inline blocks working: + # If doxygen renders an outer

-tag then inside cannot be other block element tags (by html rules), + # and sphinx will wrap all blocks with

-tags. The browsers will then close opened p tags if they + # come to a nested block element tag (e.g. a div or another p-tag) which makes our inline css trick + # obsolete. That's why we change any parent p tag to a div tag here: + parent = element.getparent() + if parent is None: + raise AssertionError("parent is None which cannot happen!? Critical Error!") + + if parent.tag == "p": + parent.tag = "div" + parent.attrib["class"] = "doxysphinx-inline-parent" + + return True + + +class RstBlockProcessor: + """Element Processor for rst block elements.""" + + elements = ["code", "pre"] + format = "rst" + is_final = True + + # _marker_regex = re.compile(r"^(rst|restructuredtext|embed:rst(:leading-(asterisk|slashes))?)\s*\r?\n", re.MULTILINE) + + _marker_regex = re.compile( + r"^(" # begin of the line + r"{rst}" # doxysphinx marker + r"|\.\. [A-Za-z][A-Za-z0-9]+::.*?" # directive autodetection + r"|embed:rst(:leading-(asterisk|slashes))?" # breathe compatibility markers + r")\s*\r?\n", # end if the line + re.MULTILINE, + ) + + def try_process(self, element: _Element) -> bool: + """Tries to process an rst block element into a neutralized format. + + :param element: The html element to process + :return: True if the element was processed else False + """ + + text = _flattened_element_text(element) + if not text: + return False + + if content := _try_parse_rst_block_content(text): + + # add newlines around the element tags to have the beginning and closing tags at the beginning of line each + _ensure_newline_before_element(element) + _ensure_newline_after_element(element) + + # add newlines around the content if necessary to have the content in new lines + content = "\n" + content if not _starts_with_newline(content) else content + content = content + "\n" if not _ends_with_newline(content) else content + + # process/transform element + element.clear(keep_tail=True) # type: ignore + element.tag = "snippet" + element.text = content + element.attrib.clear() + element.attrib["type"] = "rst:block" + return True + + return False + + +class PreToDivProcessor: + """This Element Processor will change

-tags to 
tags. + + We do this because doxysphinx will linearize html output in the writer to have it in one line in + the raw html directive. However this will destroy the newlines in pre tags. To overcome that + We change the pre output here to a div with inner line divs (which is also supported by doxygen). + + This should be the last processor applied (when everything else is done). + The reason is that it gets really hard to debug if we change the structure inbetween processors. + """ + + elements = ["pre"] + format = "" + is_final = True + + def try_process(self, element: _Element) -> bool: + """Transforms a pre element into a div element. + + :param element: The html element to process + :return: True if the element was processed else False + """ + + text = _flattened_element_text(element) + if not text: + return False + + text = dedent(text) + + # remove first empty lines at the start and at the end if any + lines = text.split("\n") + if lines[0].strip() == "": + lines.pop(0) + if lines[-1].strip() == "": + lines.pop() + + element.clear(keep_tail=True) # type: ignore + element.tag = "div" + element.attrib["class"] = "fragment" + for line in lines: + line_div = etree.Element("div") + line_div.attrib["class"] = "line" + line_div.text = line + element.append(line_div) + + return True + + +class MarkdownRstBlockProcessor: + """Element Processor for doxygen markdown block elements. + + This processor will check if the first line in the markdown block is either `{rst}` (as marker) or + if the line + + Markdown block elements in doxygen are getting rendered different to verbatim content. + Each Markdown block (delimited with ```) will be something like this in html: + + .. code-block:: html + +
+
{rst}
+
This is rst content
+
+
anything can be used here...
+
+
like an admonition:
+
+
..admonition::
+
+
test
+
+ """ + + elements = ["div"] + format = "rst" + is_final = True + + _marker_regex = re.compile( + r"^(" # begin of the line + r"{rst}" # doxysphinx marker + r"|\.\. [A-Za-z][A-Za-z0-9]+::.*?" # directive autodetection + r"|embed:rst(:leading-(asterisk|slashes))?" # breathe compatibility markers + r")$", # end if the line + re.MULTILINE, + ) + + def try_process(self, element: _Element) -> bool: + """Tries to process an rst block element into a neutralized format. + + :param element: The html element to process + :return: True if the element was processed else False + """ + if element.get("class") != "fragment": + return False + + lines = [_flattened_element_text(e) for e in element if e.tag == "div" and e.get("class") == "line"] + + text = "\n".join(lines) + + if content := _try_parse_rst_block_content(text): + + # add newlines around the element tags to have the beginning and closing tags at the beginning of line each + _ensure_newline_before_element(element) + _ensure_newline_after_element(element) + + # add newlines around the content if necessary to have the content in new lines + content = "\n" + content if not _starts_with_newline(content) else content + content = content + "\n" if not _ends_with_newline(content) else content + + # process/transform element + element.clear(keep_tail=True) # type: ignore + element.tag = "snippet" + element.text = content + element.attrib.clear() + element.attrib["type"] = "rst:block" + return True + + return False + + +def _flattened_element_text(element: _Element) -> str: + """flattens (removes children but keeps the text and html nodes) and element text.""" + text = element.text + if not text: + return "" + + if len(element) > 0: # test if element has children + text = "".join(element.itertext()) # type: ignore + + # old implementation that will render the inner html out (maybe we need this in future?) + # rendered_html_including_children = etree.tostring(element, encoding="unicode", with_tail=False).strip() + # tag, tag_end_char, rest = rendered_html_including_children.partition(">") + # close_tag = f"" + # text = rest[: -len(close_tag)] + + return text + + +def _starts_with_newline(text: str): + return text.strip(" \t").startswith("\n") + + +def _ends_with_newline(text: str): + return text.strip(" \t").endswith("\n") + + +_doxygen_comment_cleanup_regex = re.compile(r"^[^\S\r\n]*(\*|\/\/\/|\/\/!)", re.MULTILINE) +# ^ ^ ^ ^ +# | | | | +# | | | +--- two slashee followed by a exclamation mark +# | | | (strange comments...) +# | | +--- three slashes (/// - ms style comments) +# | +--- single star (qt style comments) +# +--- whitespace without newlines (zero or more) + + +def _remove_doxygen_comment_prefixes_old(text: str) -> str: + """Removes doxygen comment prefixes from texts.""" + return _doxygen_comment_cleanup_regex.sub("", text) + + # old faster but incorrect implementation + # text = text.replace("\n*", "\n").replace("\n *", "\n") + # text = text.replace("\n///", "\n").replace("\n//!", "\n") + # return text + + +def _lstrip_str(to_strip: str, from_text: str) -> str: + ws_stripped = from_text.lstrip() + if ws_stripped.startswith(to_strip): + return ws_stripped[len(to_strip) :] + + # shouldn't happen very often for comments + return from_text + + +def _remove_doxygen_comment_prefixes(text: str) -> str: + stripped = text.lstrip() + # if leading slashes + if stripped.startswith("///"): + lines = [_lstrip_str("///", line) for line in text.split("\n")] + return "\n".join(lines) + # if asterisk + elif stripped.startswith("*"): + lines = [_lstrip_str("*", line) for line in text.split("\n")] + return "\n".join(lines) + elif stripped.startswith("//!"): + lines = [_lstrip_str("//!", line) for line in text.split("\n")] + return "\n".join(lines) + + return text + + +def _try_parse_rst_block_content(text: str) -> Optional[str]: + if not text: + return None + + stripped = text.strip() + first_line, _, all_lines_after = stripped.partition("\n") + cleaned_line = _remove_doxygen_comment_prefixes(first_line).strip() + + relevant_content = "" + if cleaned_line in ["{rst}", "embed:rst", "embed:rst:leading-slashes", "embed:rst:leading-asterisk"]: + relevant_content = all_lines_after + elif cleaned_line.startswith(".. ") and "::" in cleaned_line: + relevant_content = text + else: + return None + + clean_content = _remove_doxygen_comment_prefixes(relevant_content) + dedented_content = dedent(clean_content) + return dedented_content + + +def _ensure_newline_before_element(element: _Element): + """Ensures that there is at least one newline character (\\n) before the given element. + + We need this later during the write phase (see :mod:`writer`) which is line oriented. + When we have a newline in front of our elements we can find them more easily/efficiently. + """ + previous_tag = element.getprevious() + if previous_tag is not None: + if previous_tag.tail: + if not _ends_with_newline(previous_tag.tail): + previous_tag.tail = f"{previous_tag.tail}\n" + else: + previous_tag.tail = "\n" + else: + parent_tag = element.getparent() + if parent_tag is None: + return + if parent_tag.text: + if not _ends_with_newline(parent_tag.text): + parent_tag.text = f"{parent_tag.text}\n" + else: + parent_tag.text = "\n" + + +def _ensure_newline_after_element(element: _Element): + """Ensures that there is at least one newline character (\\n) after the given element. + + We need this later during the write phase (see :mod:`writer`) which is line oriented. + When we have a newline after of our elements we can find them more easily/efficiently. + """ + + if not element.tail: + element.tail = "\n" + return + + if not _starts_with_newline(element.tail): + element.tail = f"\n{element.tail}" + + class DoxygenHtmlParser: """Parser for Doxygen HTML output files.""" + _logger = logging.getLogger(__name__) + def __init__(self, source_directory: Path): """ Create an instance of a doxygen html parser. @@ -90,7 +481,14 @@ def __init__(self, source_directory: Path): :param source_directory: the directory where the html files are located. """ self._source_directory = source_directory - self._parser = etree.HTMLParser() + # self._parser = etree.HTMLParser(huge_tree=True, recover=False) + + self._processors: List[ElementProcessor] = [ + RstInlineProcessor(), + RstBlockProcessor(), + MarkdownRstBlockProcessor(), + PreToDivProcessor(), + ] def parse(self, file: Path) -> HtmlParseResult: """ @@ -101,83 +499,87 @@ def parse(self, file: Path) -> HtmlParseResult: :return: The result of the parsing :rtype: ParseResult """ - tree = etree.parse(file.as_posix(), self._parser) # type: ignore # nosec B320 + + self._logger.debug(f"[{file}]: parsing ...") + + tree = etree.parse(file.as_posix()) # type: ignore # nosec B320 + + self._logger.debug(f"[{file}]: extracting meta infos...") meta_title: str = tree.find("//title").text # type: ignore first, *_, last = meta_title.split(":") project = first.strip() title = last.strip() - rst_found = self._normalize_tree(tree) + self._logger.debug(f"[{file}]: normalizing tree...") + + used_snippet_formats = self._normalize_tree_and_get_used_formats(tree) + + self._logger.debug(f"[{file}]: done.") + + return HtmlParseResult(file, project, meta_title, title, used_snippet_formats, tree) + + def _should_parse(self, source: str) -> bool: + # if no supported element (identified by closing tag) is in the file... + if not any(f"" in source for element in self._all_supported_elements()): + # fast exit + return False + + # get content of each element and - return HtmlParseResult(file, project, meta_title, title, rst_found, tree) + return True - def _normalize_tree(self, tree) -> bool: + @lru_cache(maxsize=2) + def _all_supported_elements(self) -> Set[str]: + return {e for p in self._processors for e in p.elements} + + def _normalize_tree_and_get_used_formats(self, tree) -> Set[str]: """ Normalize a doxygen html tree. - Searches for pre and verbatim tags, re-formats them and creates -tags out of it. Will also put a newline - behind the closing tag because it's necessary to have lines that can be clearly assigned to either html-content - or rst content (and in the un-normalized source html we've got them mixed at the closing tag). - """ - rst_found = False - for pre_tag in tree.iter("pre", "verbatim"): - if content := self._parse_content(pre_tag.text): - pre_tag.tag = "rst" - pre_tag.text = "\n".join(content) + "\n" - - # add newline after the tag (so that we can split it up later) - pre_tag.tail = "\n" if not pre_tag.tail else f"\n{pre_tag.tail}" - - # add newline before the tag (so that we can split it up later) - previous_tag = pre_tag.getprevious() - if previous_tag is not None: - if previous_tag.tail: - if not previous_tag.tail.endswith("\n"): - previous_tag.tail = f"{previous_tag.tail}\n" - else: - previous_tag.tail = "\n" - else: - parent_tag = pre_tag.getparent() - if parent_tag.text: - if not parent_tag.text.endswith("\n"): - parent_tag.text = f"{parent_tag.text}\n" - else: - parent_tag.text = "\n" - - pre_tag.attrib.clear() - rst_found = True - return rst_found - - def _parse_content(self, text: str) -> Union[List[str], None]: + Searches for pre and code tags, re-formats them and creates different -tags out of it. + Will also put a newline behind the closing tag because it's necessary to have lines that can be clearly + assigned to either html-content or snippet content (and in the un-normalized source html we've got them mixed + at the closing tag). """ - Parse the content of @rst directives (=pre/verbatim html nodes) in doxygen comments into a line list. - If the text contains not the right markers e.g. when using a standard verbatim - block and no rst block in doxygen comments this method returns None. - """ - normalized_text = text.strip() + used_snipped_formats = set() - marker_checks = ((x, normalized_text.startswith(x)) for x in self._supported_markers()) + # prefetch element candidates. + # We do that because if there are bugs in a processor which will change the tree one might get strange + # behaviors here (processors not applied because the elements where changed during iteration). + # So this is just a means to make debugging easier... + element_candidates = list(tree.iter(*self._all_supported_elements())) - for marker, found in marker_checks: - if not found: - continue + # search for all supported elements in element tree + for element in element_candidates: - content = "\n" + normalized_text[len(marker) :] - # remove doxygen comments - content = content.replace("\n*", "\n").replace("\n *", "\n") - content = content.replace("\n///", "\n").replace("\n//!", "\n") + # try to apply each processor... + for processor in self._processors: - content_lines = content.split("\n")[1:] + # if the current element isn't supported by the current processor skip to the next one + if element.tag not in processor.elements: + continue - return content_lines + # try to process the element + if not self._try_process(element, processor): + continue - return None + # if it was processed add the used format to the output... + if processor.format: + used_snipped_formats.add(processor.format) + + # if the processor is final (no further processors considered) -> break the loop + if processor.is_final: + break + + return used_snipped_formats + + def _try_process(self, element: _Element, processor: ElementProcessor) -> bool: + # fail if element isn't supported by processor + if element.tag not in processor.elements: + return False - @staticmethod - def _supported_markers() -> Iterator[str]: - yield "embed:rst:leading-asterisk" - yield "embed:rst:leading-slashes" - yield "embed:rst:inline" - yield "embed:rst" + # fail if processing returns + processed = processor.try_process(element) + return processed diff --git a/doxysphinx/resources/custom.scss b/doxysphinx/resources/custom.scss index 66c5348..d9d84df 100644 --- a/doxysphinx/resources/custom.scss +++ b/doxysphinx/resources/custom.scss @@ -93,6 +93,19 @@ html { box-shadow: none; border-bottom: 1px solid var(--separator-color); } + + /* css hack for inline rst content. + as sphinx will automatically create paragraphs (

) around all blocks, we'll always get new paragraphs + for our inline content in doxygen html. So we add a "marker span block" right before the inline content + block and then style the "paragraphiness (/blockrendering)" away here with the next sibling selector (+). + */ + // .doxysphinx-p { + // display: block; + // } + + .doxysphinx-inline-parent p { + display: inline; + } } /** content fixes - members (e.g. in class view) */ diff --git a/doxysphinx/writer.py b/doxysphinx/writer.py index a998ba3..e079291 100644 --- a/doxysphinx/writer.py +++ b/doxysphinx/writer.py @@ -59,6 +59,12 @@ class RstWriter: _logger = logging.getLogger(__name__) + # compiled regex for combining adjacent rst blocks + _rst_join_regex = re.compile(r"\s*") + + # regex for searching inline elements + _rst_element_regex = re.compile(r".*?)\">((?P.*?))?$") + def __init__(self, source_directory: Path, toc_generator_type: Type[TocGenerator] = DoxygenTocGenerator): """ Create a new rst writer. @@ -80,9 +86,6 @@ def __init__(self, source_directory: Path, toc_generator_type: Type[TocGenerator } ) - # compiled regex for combining adjacent rst blocks - self._rst_join_regex = re.compile(r"\s*") - def _rst_safe_encode(self, text: str) -> str: return text.translate(self._rst_safe_encode_map) @@ -104,7 +107,7 @@ def write(self, parse_result: HtmlParseResult, target_file: Path) -> Path: toc = self._toc_gen.generate_toc_for(html_file) content = [] - if parse_result.contains_rst: + if parse_result.used_snippet_formats: # for rst containing htmls we create a mixed (raw html + rst block) rst self._logger.debug(f"writing mixed rst for {parse_result.html_input_file}") content.extend(self._mixed_rst(tree)) @@ -211,16 +214,48 @@ def _iterate_html(self, line_iter: Iterator[str], content: List[str]): while True: try: current = next(line_iter) - if current.startswith(""): + + if match := self._rst_element_regex.match(current): content.append(f" {buffer}") buffer = "" - self._iterate_rst(line_iter, content) + snippet_type = match.group("type") + if snippet_type == "rst:inline": + inline_rst = match.group("inline_content") + self._append_inline_rst_and_prefix(inline_rst, content) + content.extend(self._raw_directive()) + # self._iterate_rst(line_iter, content) + # this is done by _iterate_rst isn't it? -> content.extend(self._raw_directive()) + else: + self._iterate_rst(line_iter, content) else: buffer += current except StopIteration: break content.append(f" {buffer}") + def _append_inline_rst_and_prefix(self, inline_content: str, content: List[str]): + decoded_line = html.unescape(inline_content.strip()) + last_content_line = content.pop() + if last_content_line.endswith(" "): + last_content_line = f"{last_content_line[:-1]} " + # last_content_line += ' ' + content.append(last_content_line) + content.append("") + content.append(f"{decoded_line}") + content.append("") + + # def _append_inline_rst_and_prefix(self, inline_content: str, content: List[str]): + # decoded_line = html.unescape(inline_content.strip()) + # content.append("") + # # content.append(".. container:: doxysphinx-inline-rst-content-before-marker") + # # content.append("") + # # content.append(" dummy") + # # content.append("") + # content.append(".. container:: doxysphinx-inline-content-wrapper") + # content.append("") + # content.append(f" {decoded_line}") + # content.append("") + def _iterate_rst(self, line_iter: Iterator[str], content: List[str]): """Iterate over rst lines.""" content.append("") @@ -230,7 +265,7 @@ def _iterate_rst(self, line_iter: Iterator[str], content: List[str]): while True: try: current = next(line_iter) - if current.startswith(""): + if current.strip().startswith(""): # dedent buffer and convert it to lines dedented_buffer = dedent(buffer) buffer_lines = dedented_buffer.split("\n") diff --git a/index.md b/index.md index babd134..08f5b3f 100644 --- a/index.md +++ b/index.md @@ -17,6 +17,7 @@ :caption: Usage docs/getting_started.md +docs/syntax/syntax_guide.md docs/alternatives.md docs/faq.md ``` @@ -36,6 +37,7 @@ docs/linking_needs.md :caption: Demo documentations :maxdepth: 1 Doxygen Demo +docs/test.rst ``` ```{toctree} diff --git a/pyproject.toml b/pyproject.toml index 57a0b31..c65741d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,6 +102,8 @@ build-backend = "poetry.core.masonry.api" log_level = "DEBUG" testpaths = ["tests"] junit_logging = "out-err" +markers = ["speed: manual speed tests"] +addopts = "-m 'not speed'" [tool.semantic_release] version_source = "commit" @@ -158,5 +160,8 @@ ignore_missing_imports = true match_dir = "doxysphinx" ignore = "D301,D213,D212,D203" +[tool.pylint] +disable = ["logging-fstring-interpolation"] + [tool.pylint.FORMAT] max-line-length = 120 diff --git a/tests/html_parser/test_files/.gitignore b/tests/html_parser/test_files/.gitignore new file mode 100644 index 0000000..d6657b0 --- /dev/null +++ b/tests/html_parser/test_files/.gitignore @@ -0,0 +1 @@ +*.parsed.html diff --git a/tests/html_parser/test_files/demo_html_1.expected.html b/tests/html_parser/test_files/demo_html_1.expected.html new file mode 100644 index 0000000..78572b7 --- /dev/null +++ b/tests/html_parser/test_files/demo_html_1.expected.html @@ -0,0 +1,40 @@ + + + + + demo : html 1 + + + +

+

A car.

+ +.. warning:: + This car is a top secret prototype. + + +

Here is a plot to show it's true power:

+ +.. plot:: + + import matplotlib + import matplotlib.pyplot as plt + import numpy as np + + # Data for plotting + t = np.arange(0.0, 2.0, 0.01) + s = 1 + np.sin(2 * np.pi * t) + + fig, ax = plt.subplots() + ax.plot(t, s) + + ax.set(xlabel='time (s)', ylabel='voltage (mV)', + title='About as simple as it gets, folks') + ax.grid() + + plt.show() + +
+ + + diff --git a/tests/html_parser/test_files/demo_html_1.input.html b/tests/html_parser/test_files/demo_html_1.input.html new file mode 100644 index 0000000..58381d4 --- /dev/null +++ b/tests/html_parser/test_files/demo_html_1.input.html @@ -0,0 +1,39 @@ + + + + demo : html 1 + + + +
+

A car.

+
embed:rst:leading-asterisk
+            *  .. warning::
+            *     This car is a top secret prototype.
+            * 
+            *  
+

Here is a plot to show it's true power:

+
embed:rst:leading-asterisk
+            *  .. plot::
+            * 
+            *    import matplotlib
+            *    import matplotlib.pyplot as plt
+            *    import numpy as np
+            * 
+            *    # Data for plotting
+            *    t = np.arange(0.0, 2.0, 0.01)
+            *    s = 1 + np.sin(2 * np.pi * t)
+            * 
+            *    fig, ax = plt.subplots()
+            *    ax.plot(t, s)
+            * 
+            *    ax.set(xlabel='time (s)', ylabel='voltage (mV)',
+            *          title='About as simple as it gets, folks')
+            *    ax.grid()
+            * 
+            *    plt.show()
+            *  
+
+ + + diff --git a/tests/html_parser/test_files/html_tags_in_rst_content.expected.html b/tests/html_parser/test_files/html_tags_in_rst_content.expected.html new file mode 100644 index 0000000..984d222 --- /dev/null +++ b/tests/html_parser/test_files/html_tags_in_rst_content.expected.html @@ -0,0 +1,32 @@ + + + + + demo : html tags in rst content + + + + +.. warning:: no content after the delimiters + + In markdown it's typical to have the language right behind the beginning codeblock delimiter. + Something like ```cpp for example. However the doxygen markdown parser will swallow + anything behin the delimiters (which means we couldn't make use of information there). So please do + not fall into the trap trying to start a rst block with ```{rst}! + + Just Text + +.. tip:: + + If you see a tip around this text it worked! + + + +.. tip:: another test + + this is just another test + here's a codeblock right at the end + + + + diff --git a/tests/html_parser/test_files/html_tags_in_rst_content.input.html b/tests/html_parser/test_files/html_tags_in_rst_content.input.html new file mode 100644 index 0000000..3ed9680 --- /dev/null +++ b/tests/html_parser/test_files/html_tags_in_rst_content.input.html @@ -0,0 +1,30 @@ + + + + + demo : html tags in rst content + + + +
+    .. warning:: no content after the delimiters
+    
+       In markdown it's typical to have the language right behind the beginning codeblock delimiter.
+       Something like ```cpp for example. However the doxygen markdown parser will swallow
+       anything behin the delimiters (which means we couldn't make use of information there). So please do
+       not fall into the trap trying to start a rst block with ```{rst}!
+    
+ Just Text +
{rst}
+
.. tip::
+
+
If you see a tip around this text it worked!
+
+
+        .. tip:: another test
+
+           this is just another test
+           here's a codeblock right at the end
+ + + diff --git a/tests/html_parser/test_files/pre_to_div.expected.html b/tests/html_parser/test_files/pre_to_div.expected.html new file mode 100644 index 0000000..1cafe7a --- /dev/null +++ b/tests/html_parser/test_files/pre_to_div.expected.html @@ -0,0 +1,12 @@ + + + + + demo : Pre to div element conversion + + + +
This is preformatted content
without any rst in it.
The parser should change this to a div tag
with class "fragment" and subdivs
with class "line" for each line.
+ + + diff --git a/tests/html_parser/test_files/pre_to_div.input.html b/tests/html_parser/test_files/pre_to_div.input.html new file mode 100644 index 0000000..0d933a3 --- /dev/null +++ b/tests/html_parser/test_files/pre_to_div.input.html @@ -0,0 +1,17 @@ + + + + demo : Pre to div element conversion + + + +
+        This is preformatted content
+        without any rst in it.
+        The parser should change this to a div tag
+        with class "fragment" and subdivs 
+        with class "line" for each line.
+    
+ + + diff --git a/tests/html_parser/test_files/sequential_pre_tags.expected.html b/tests/html_parser/test_files/sequential_pre_tags.expected.html new file mode 100644 index 0000000..451b15b --- /dev/null +++ b/tests/html_parser/test_files/sequential_pre_tags.expected.html @@ -0,0 +1,32 @@ + + + + + demo : Sequential pre tags - last one was missing + + + + +
+ +

rst block with html pre tag in doxygen.

+

<pre>-html-element

+

Syntax

+
/// <pre> {rst}
/// .. tip::
///
/// If you see a tip around this text it worked!
/// </pre>
+

Example

+ +.. tip:: + +If you see a tip around this text it worked! + +

TEST WO ISSES DENN? SAPRALOT!!

+ +.. info:: + +Beside the `{rst}`-marker you can also use `{embed:rst}`, `{embed:rst:leading-slashes}`, +`{embed:rst:leading-asterisk}`, or the directive autodetection feature. + +
+ + + diff --git a/tests/html_parser/test_files/sequential_pre_tags.input.html b/tests/html_parser/test_files/sequential_pre_tags.input.html new file mode 100644 index 0000000..bdc5219 --- /dev/null +++ b/tests/html_parser/test_files/sequential_pre_tags.input.html @@ -0,0 +1,38 @@ + + + + demo : Sequential pre tags - last one was missing + + + + +
+ +

rst block with html pre tag in doxygen.

+

<pre>-html-element

+

Syntax

+
+/// <pre> {rst}
+/// .. tip::
+/// 
+///    If you see a tip around this text it worked!
+/// </pre>
+
+

Example

+
 {rst}
+.. tip::
+
+If you see a tip around this text it worked!
+
+

TEST WO ISSES DENN? SAPRALOT!!

+
+{rst}
+.. info::
+
+Beside the `{rst}`-marker you can also use `{embed:rst}`, `{embed:rst:leading-slashes}`, 
+`{embed:rst:leading-asterisk}`, or the directive autodetection feature.
+
+
+ + + diff --git a/tests/html_parser/test_helper_functions.py b/tests/html_parser/test_helper_functions.py new file mode 100644 index 0000000..30da30f --- /dev/null +++ b/tests/html_parser/test_helper_functions.py @@ -0,0 +1,252 @@ +import re +from typing import List + +import pytest +from lxml.etree import HTMLParser, _Element, fromstring, tostring + +from doxysphinx.html_parser import ( + _ensure_newline_after_element, + _ensure_newline_before_element, + _remove_doxygen_comment_prefixes, + _try_parse_rst_block_content, +) + +single_element_in_parent = """ + + + + + +""" + +element_with_siblings_before = """ + + +
First
+
Second
+ + + +""" + +element_with_siblings_after = """ + + + +
First
+
Second
+ + +""" + +element_with_siblings_around = """ + + +
First
+
Second
+ +
Third
+
Forth
+ + +""" + +element_with_text_before = """ + + + Lorem ipsum dolor sit before + + + +""" + +element_with_text_after = """ + + + + Lorem ipsum dolor sit after + + +""" + +element_with_text_around = """ + + + Lorem ipsum dolor sit before + + Lorem ipsum dolor sit after + + +""" + +element_with_newlines_before = """ + + + + + + + + + +""" + +element_with_newlines_after = """ + + + + + + + + + +""" + +element_with_newlines_around = """ + + + + + + + + + + + + +""" + + +def _build_test_set() -> List: + input = [ + (single_element_in_parent, "single_element_in_parent"), + (element_with_siblings_before, "element_with_siblings_before"), + (element_with_siblings_after, "element_with_siblings_after"), + (element_with_siblings_around, "element_with_siblings_around"), + (element_with_text_before, "element_with_text_before"), + (element_with_text_after, "element_with_text_after"), + (element_with_text_around, "element_with_text_around"), + (element_with_newlines_before, "element_with_newlines_before"), + (element_with_newlines_after, "element_with_newlines_after"), + (element_with_newlines_around, "element_with_newlines_around"), + ] + + result = [] + for data, id in input: + result.append(pytest.param(data, id=f"pretty_{id}")) + for data, id in input: + whitespaceless_data = data.replace("\n", "").replace(" ", "").replace("> ", ">").replace(" <", "<") + result.append(pytest.param(whitespaceless_data, id=f"condensed_{id}")) + return result + + +DEFAULT_TEST_SET = _build_test_set() + + +def _load_code_element(html_string: str) -> _Element: + parser: Any = HTMLParser() + e = fromstring(html_string, parser) + result: Any = e.find(".//code") + return result + + +def _count_newlines_until_non_whitespace(text: str) -> int: + count: int = 0 + for i, v in enumerate(text): + if v.isalnum(): + break + if v == "\n": + count = count + 1 + return count + + +@pytest.mark.parametrize( + "test, expected", + [ + ("", 0), + ("fdgfdkgfjg \n gdfkgjfkgfdj \ngkfdjgfdkg\ndfgfdkgjf", 0), + ("\n", 1), + ("\n\n\n fd kjg fgkjg\n\n", 3), + (" \n \n \t\t\t \n\n\n\n", 3), + ], +) +def test_count_newlines_until_non_whitespace(test: str, expected: int): + assert _count_newlines_until_non_whitespace(test) == expected + + +@pytest.mark.parametrize("html_string", DEFAULT_TEST_SET) +def test_ensure_newline_before_element(html_string: str): + prefix, _ = html_string.split("") + newlines_before_processing = _count_newlines_until_non_whitespace(prefix[::-1]) + + code_element = _load_code_element(html_string) + + _ensure_newline_before_element(code_element) + + root = code_element.getroottree().getroot() + html_output = tostring(root, encoding="unicode") + + prefix, _ = html_output.split("") + newlines_after_processing = _count_newlines_until_non_whitespace(prefix[::-1]) + + assert (newlines_after_processing == newlines_before_processing + 1) or ( + newlines_after_processing == newlines_before_processing + ) + assert newlines_after_processing >= 1 + + +@pytest.mark.parametrize("html_string", DEFAULT_TEST_SET) +def test_ensure_newline_after_element(html_string: str): + _, suffix = html_string.split("") + newlines_before_processing = _count_newlines_until_non_whitespace(suffix) + + code_element = _load_code_element(html_string) + + _ensure_newline_after_element(code_element) + + root = code_element.getroottree().getroot() + html_output = tostring(root, encoding="unicode") + + _, suffix = html_output.split("") + newlines_after_processing = _count_newlines_until_non_whitespace(suffix) + + assert (newlines_after_processing == newlines_before_processing + 1) or ( + newlines_after_processing == newlines_before_processing + ) + assert newlines_after_processing >= 1 + + +@pytest.mark.parametrize( + "input, expected", + [ + # main doxysphinx marker with some variants regarding whitespaces + pytest.param("{rst}\nFIRST_LINE\nSECOND_LINE", "FIRST_LINE\nSECOND_LINE"), + pytest.param(" {rst}\nFIRST_LINE\nSECOND_LINE", "FIRST_LINE\nSECOND_LINE"), + pytest.param(" \n {rst}\nFIRST_LINE\nSECOND_LINE", "FIRST_LINE\nSECOND_LINE"), + pytest.param("{rst}\nFIRST_LINE\nSECOND_LINE", "FIRST_LINE\nSECOND_LINE"), + pytest.param(" {rst}\n FIRST_LINE\n SECOND_LINE", "FIRST_LINE\nSECOND_LINE"), + # breathe compatibility markers + pytest.param("embed:rst\nFIRST_LINE\nSECOND_LINE", "FIRST_LINE\nSECOND_LINE"), + pytest.param("embed:rst:leading-slashes\nFIRST_LINE\nSECOND_LINE", "FIRST_LINE\nSECOND_LINE"), + pytest.param("embed:rst:leading-asterisk\nFIRST_LINE\nSECOND_LINE", "FIRST_LINE\nSECOND_LINE"), + # directive auto detection + pytest.param(".. directive::\nFIRST_LINE\nSECOND_LINE", ".. directive::\nFIRST_LINE\nSECOND_LINE"), + pytest.param(".. directive:: title\nFIRST_LINE\nSECOND_LINE", ".. directive:: title\nFIRST_LINE\nSECOND_LINE"), + pytest.param( + " .. directive:: title\n FIRST_LINE\n SECOND_LINE", + ".. directive:: title\n FIRST_LINE\n SECOND_LINE", + ), + pytest.param("* .. directive::\n* FIRST_LINE\n* SECOND_LINE", ".. directive::\nFIRST_LINE\nSECOND_LINE"), + # negative tests + pytest.param("{rst} content that shouldn't be here\nFIRST_LINE\nSECOND_LINE", None), + pytest.param("rst\nFIRST_LINE\nSECOND_LINE", None), + pytest.param("FIRST_LINE\nSECOND_LINE", None), + pytest.param("..directive::no\nWrong directive, missing space between dots", None), + pytest.param("", None), + pytest.param("{markdown}\n\njust a test", None), + ], +) +def test_try_parse_rst_block_content(input, expected): + assert _try_parse_rst_block_content(input) == expected diff --git a/tests/html_parser/test_html_parser.py b/tests/html_parser/test_html_parser.py new file mode 100644 index 0000000..96309eb --- /dev/null +++ b/tests/html_parser/test_html_parser.py @@ -0,0 +1,56 @@ +import re +from pathlib import Path +from typing import Any, Iterable, List, Tuple + +import lxml.etree as etree +import pytest + +from doxysphinx.html_parser import DoxygenHtmlParser, HtmlParser + + +def _read_data() -> List[Any]: + results = [] + test_files = Path(__file__).parent / "test_files" + for input_file in test_files.glob("*.input.html"): + expected_name = Path(input_file.stem).stem + expected_file = input_file.parent / (expected_name + ".expected.html") + if not expected_file.exists(): + continue + results.append((input_file, expected_file)) + return results + + +def ids_for(values: List[Any]) -> List[str]: + results = [] + + for v in values: + first, second = v + name = Path(first.stem).stem + results.append(name) + + return results + + +DATA = _read_data() + + +@pytest.mark.parametrize("input, expected", DATA, ids=ids_for(DATA)) +def test_html_parser_works_as_expected(input: Path, expected: Path): + """_summary_ + + :param input_file: _description_ + :param expected_file: _description_ + """ + x = DoxygenHtmlParser(Path(__file__).parent) + result = x.parse(input) + + parsed_html = etree.tostring( + result.tree, + encoding="unicode", + ) + parsed_output = input.parent / (Path(input.stem).stem + ".parsed.html") + parsed_output.write_text(parsed_html) + + expected_html = expected.read_text() + + assert parsed_html.strip() == expected_html.strip() diff --git a/tests/html_parser/test_remove_doxygen_comment_prefixes.py b/tests/html_parser/test_remove_doxygen_comment_prefixes.py new file mode 100644 index 0000000..b41ada4 --- /dev/null +++ b/tests/html_parser/test_remove_doxygen_comment_prefixes.py @@ -0,0 +1,41 @@ +import pytest + +from doxysphinx.html_parser import _remove_doxygen_comment_prefixes + +regression_input_1 = """ + .. admonition:: What you should see here + + This text should be in an admonition box. It was generated from a doxygen javadoc comment **without any special identation**. + + *Note that we had to add a space between the backslash and the verbatim command in the code because doxygen parsing will freak out if we add the command there...* + + .. code:: cpp + + /** + \ verbatim embed:rst + + ...rst-content-here... + + \ endverbatim + *\/""" + + +@pytest.mark.parametrize( + "text, expected", + [ + # comment styles test + pytest.param("\n/// first\n/// second", "\n first\n second"), + pytest.param("\n* first\n* second", "\n first\n second"), + pytest.param("\n//! first\n//! second", "\n first\n second"), + pytest.param("\n /// first\n /// second", "\n first\n second"), + pytest.param("\n * first\n * second", "\n first\n second"), + pytest.param("\n //! first\n //! second", "\n first\n second"), + # mixed styles should take first one + pytest.param("/// first\n/// second\n * third\n/// forth", " first\n second\n * third\n forth"), + # test previous regressions + pytest.param(regression_input_1, regression_input_1), + ], +) +def test_remove_doxygen_comment_markers(text: str, expected: str): + result = _remove_doxygen_comment_prefixes(text) + assert result == expected diff --git a/tests/html_parser/test_rst_block_processor.py b/tests/html_parser/test_rst_block_processor.py new file mode 100644 index 0000000..6ffefdf --- /dev/null +++ b/tests/html_parser/test_rst_block_processor.py @@ -0,0 +1,193 @@ +import re +from typing import Iterable + +import lxml.etree as etree +import pytest + +from doxysphinx.html_parser import RstBlockProcessor +from doxysphinx.utils.contexts import TimedContext + +rst_block_with_doxysphinx_marker = """ +
+
{rst}
+    *    .. need:: test
+    *       :status: open
+    *
+    *       This is the description
+    
+
+""" + +rst_block_with_doxysphinx_autodetect_directive = """ +
+
+    *    .. need:: test
+    *       :status: open
+    *
+    *       This is the description
+    
+
+""" + + +rst_block_with_doxysphinx_marker_and_strange_comments = """ +
+
{rst}
+       //!    .. need:: test
+       //!       :status: open
+       //!  
+       //!       This is the description
+    
+
+""" + +rst_block_with_breathe_general_marker = """ +
+
embed:rst
+        *  .. need:: test
+        *     :status: open
+        *
+        *     This is the description
+    
+
+""" + +rst_block_with_breathe_leading_asterisk_marker = """ +
+
embed:rst:leading-asterisk
+      *.. need:: test
+      *   :status: open
+      *
+      *   This is the description
+    
+
+""" + +rst_block_with_breathe_leading_slashes_marker = """ +
+
embed:rst:leading-slashes
+        ///.. need:: test
+        ///   :status: open
+        ///
+        ///   This is the description
+    
+
+""" + +invalid_rst_block_without_any_marker = """ +
+
+    ..need: test
+       :status: open
+
+       This is the description
+    
+
+""" + +invalid_rst_block_with_breathe_inline_rst_marker = """ +
+
embed:rst:inline
+    .. need:: test
+       :status: open
+
+       This is the description
+    
+
+""" + +expected_rst_block = """ +
+ + .. need:: test + :status: open + + This is the description + +
+""" + + +def _load_code_element(html_string: str) -> etree._Element: + parser: Any = etree.HTMLParser() + e = etree.fromstring(html_string, parser) + result: Any = e.find(f".//pre") + if result is None: + result = e.find(f".//code") + return result + + +def _clean_html_string(html: str): + return html.replace("\n", "").replace(" ", "").replace("> ", ">").replace(" <", "<").strip() + + +@pytest.mark.parametrize( + "input, expected", + [ + pytest.param(rst_block_with_doxysphinx_marker, expected_rst_block, id="rst_block_with_doxysphinx_marker"), + pytest.param( + rst_block_with_doxysphinx_autodetect_directive, + expected_rst_block, + id="rst_block_with_doxysphinx_autodetect_directive", + ), + pytest.param( + rst_block_with_doxysphinx_marker_and_strange_comments, + expected_rst_block, + id="rst_block_with_doxysphinx_marker_and_strange_comments", + ), + pytest.param( + rst_block_with_breathe_general_marker, expected_rst_block, id="rst_block_with_breathe_general_marker" + ), + pytest.param( + rst_block_with_breathe_leading_asterisk_marker, + expected_rst_block, + id="rst_block_with_breathe_leading_asterisk_marker", + ), + pytest.param( + rst_block_with_breathe_leading_slashes_marker, + expected_rst_block, + id="rst_block_with_breathe_leading_slashes_marker", + ), + ], +) +def test_rst_block_processor_works_as_expected(input: str, expected: str): + element = _load_code_element(input) + + proc = RstBlockProcessor() + result = proc.try_process(element) + assert result is True + root_div = element.getparent() + + rendered_element = etree.tostring(root_div, encoding="unicode") + a = _clean_html_string(rendered_element) + b = _clean_html_string(expected) + assert a == b + + +@pytest.mark.parametrize( + "input, expected", + [ + pytest.param( + invalid_rst_block_without_any_marker, + invalid_rst_block_without_any_marker, + id="invalid_rst_block_without_any_marker", + ), + pytest.param( + invalid_rst_block_with_breathe_inline_rst_marker, + invalid_rst_block_with_breathe_inline_rst_marker, + id="invalid_rst_block_with_breathe_inline_rst_marker", + ), + ], +) +def test_rst_block_processor_with_invalid_blocks_does_nothing(input: str, expected: str): + element = _load_code_element(input) + + proc = RstBlockProcessor() + result = proc.try_process(element) + assert result is False + root_div = element.getparent() + + rendered_element = etree.tostring(root_div, encoding="unicode", pretty_print=True) + a = rendered_element.strip() + b = expected.strip() + assert a == b diff --git a/tests/html_parser/test_rst_inline_processor.py b/tests/html_parser/test_rst_inline_processor.py new file mode 100644 index 0000000..2641914 --- /dev/null +++ b/tests/html_parser/test_rst_inline_processor.py @@ -0,0 +1,121 @@ +import lxml.etree as etree +import pytest + +from doxysphinx.html_parser import RstInlineProcessor + +inline_rst_role_with_backticks = """ +
+ Lorem ipsum pretext :doc:`Home <index>`, lorem ipsum posttext. +
+""" + +inline_rst_role_with_apostrophe = """ +
+ Lorem ipsum pretext :doc:'Home <index>', lorem ipsum posttext. +
+""" + +inline_rst_role_with_quote = """ +
+ Lorem ipsum pretext :doc:"Home <index>", lorem ipsum posttext. +
+""" + +invalid_rst_role_1 = """ +
+ Lorem ipsum pretext .. admonition:: Hello There!, lorem ipsum posttext. +
+""" + +invalid_rst_role_2 = """ +
+ Lorem ipsum pretext + rst + ..admonition:: this is rst + + rendered text + +
+""" + +invalid_rst_role_3 = """ +
+""" + +expected_rst_role = """ +
+ Lorem ipsum pretext + :doc:`Home <index>` + , lorem ipsum posttext. +
+""" + +inline_rst_domain_with_quote = """ +
+ This is a link to a python class :py:doxysphinx:"my_awesome_func(input: str)". Should work! +
+""" + +expected_rst_domain = """ +
+ This is a link to a python class + :py:doxysphinx:`my_awesome_func(input: str)` + . Should work! +
+""" + + +def _load_code_element(html_string: str) -> etree._Element: + parser: Any = etree.HTMLParser() + e = etree.fromstring(html_string, parser) + result: Any = e.find(".//code") + return result + + +def _clean_html_string(html: str): + return html.replace("\n", "").replace(" ", "").replace("> ", ">").replace(" <", "<").strip() + + +@pytest.mark.parametrize( + "input, expected", + [ + pytest.param(inline_rst_role_with_backticks, expected_rst_role, id="inline_rst_role_with_backticks"), + pytest.param(inline_rst_role_with_apostrophe, expected_rst_role, id="inline_rst_role_with_apostrophe"), + pytest.param(inline_rst_role_with_quote, expected_rst_role, id="inline_rst_role_with_quote"), + pytest.param(inline_rst_domain_with_quote, expected_rst_domain, id="inline_rst_domain_with_quote"), + ], +) +def test_rst_inline_processor(input: str, expected: str): + element = _load_code_element(input) + + proc = RstInlineProcessor() + result = proc.try_process(element) + assert result is True + root_div = element.getparent() + + rendered_element = etree.tostring(root_div, encoding="unicode", pretty_print=True) + a = _clean_html_string(rendered_element) + b = _clean_html_string(expected) + assert a == b + + +@pytest.mark.parametrize( + "input, expected", + [ + pytest.param(invalid_rst_role_1, invalid_rst_role_1, id="invalid_rst_role_1"), + pytest.param(invalid_rst_role_2, invalid_rst_role_2, id="invalid_rst_role_1"), + pytest.param(invalid_rst_role_3, invalid_rst_role_3, id="invalid_rst_role_1"), + ], +) +def test_rst_inline_processor_doesnt_parse_invalid_data(input: str, expected: str): + element = _load_code_element(input) + + proc = RstInlineProcessor() + result = proc.try_process(element) + assert result is False + root_div = element.getparent() + + rendered_element = etree.tostring(root_div, encoding="unicode", pretty_print=True) + a = _clean_html_string(rendered_element) + b = _clean_html_string(expected) + assert a == b diff --git a/tests/html_parser/test_speed_doxygen_comment.py b/tests/html_parser/test_speed_doxygen_comment.py new file mode 100644 index 0000000..5dfb3c0 --- /dev/null +++ b/tests/html_parser/test_speed_doxygen_comment.py @@ -0,0 +1,139 @@ +import re + +import pytest + +from doxysphinx.utils.contexts import TimedContext + +# SPEED TEST ONLY +# run with pytest -s to see the console output with the timings + +SUBJECT = """*rst +* first +* second +* third + +embed:rst +/// first +/// second +/// third + +embed:rst +/// first +/// second +/// third + +embed:rst +* first +* second +* third + +embed:rst + * first + * second + * third + +embed:rst +//! first +//! second +//! third +""" + +EXPECTED = """rst + first + second + third + +embed:rst + first + second + third + +embed:rst + first + second + third + +embed:rst + first + second + third + +embed:rst + first + second + third + +embed:rst + first + second + third +""" + + +def _remove_doxygen_comment_markers_replace(text: str) -> str: + """Removes doxygen comment prefixes from texts.""" + + text = text.replace("\n *", "\n").replace("\n*", "\n") + text = text.replace("\n///", "\n").replace("\n//!", "\n") + return text + + +# 17 secs +def _remove_doxygen_comment_markers_arrays(text: str) -> str: + """Removes doxygen comment prefixes from texts.""" + + lines = text.split("\n") + result = [] + for l in lines: + clean = l.strip() + if clean.startswith("*"): + result.append(clean[1:]) + elif clean.startswith("///") or clean.startswith("//!"): + result.append(clean[3:]) + return "\n".join(result) + + +comment_cleanup_regex = re.compile(r"^\s*(\*|\/\/\/|\/\/!)", re.MULTILINE) + + +def _remove_doxygen_comment_markers_regex_sub(text: str) -> str: + """Removes doxygen comment prefixes from texts.""" + + return comment_cleanup_regex.sub("", text) + + +@pytest.mark.parametrize( + "function, input, expected", + [ + # (_remove_doxygen_comment_markers_replace, SUBJECT, EXPECTED), + (_remove_doxygen_comment_markers_regex_sub, SUBJECT, EXPECTED), + ], +) +def test_functions_work_as_expected(function, input, expected): + assert function(input) == expected + + +@pytest.mark.speed +def test_comment_cleaner_speed(): + count = 1000000 + + # with TimedContext() as t1: + # for i in range(0, count): + # result = _remove_doxygen_comment_markers_replace(SUBJECT) + # assert result == EXPECTED + # print(f"replace: {t1.elapsed_humanized()}") + + with TimedContext() as t2: + for i in range(0, count): + result = _remove_doxygen_comment_markers_regex_sub(SUBJECT) + assert result == EXPECTED + print(f"regex sub: {t2.elapsed_humanized()}") + + +## RESULTS: replace = 2 seconds +## regex sub = 8 seconds +## so classic is way faster +## however as we need to get rid of the whitespace upfront we've got a problem + +if __name__ == "__main__": + test_comment_cleaner_speed() diff --git a/tests/html_parser/test_speed_rst_block_content_parser.py b/tests/html_parser/test_speed_rst_block_content_parser.py new file mode 100644 index 0000000..6e6ed93 --- /dev/null +++ b/tests/html_parser/test_speed_rst_block_content_parser.py @@ -0,0 +1,110 @@ +import re +import sys +from pathlib import Path +from typing import Iterable + +import pytest + +from doxysphinx.utils.contexts import TimedContext + +current_dir = str(Path(__file__).parent) +if current_dir not in sys.path: + sys.path.append(current_dir) + +from test_utils import load_code_element + + +def _supported_markers() -> Iterable[str]: + # doxysphinx markers + yield "rst" + yield "restructuredtext" + # breathe compatibility markers + yield "embed:rst:leading-asterisk" + yield "embed:rst:leading-slashes" + yield "embed:rst" + + +def _parse_content_classic(text: str) -> Iterable[str]: + + normalized_text = text.strip() + + marker_checks = [(x, normalized_text.startswith(x)) for x in _supported_markers()] + + for marker, found in marker_checks: + if not found: + continue + + content = "\n" + normalized_text[len(marker) :] # remove marker at the front + + content_lines = content.split("\n")[1:] # split into lines ignoring the first \n (the "marker"-line) + + yield from content_lines + break # stop for loop when we have a match + + +_search_regex = re.compile( + r"^(?Prst|restructuredtext|embed:rst(:leading-(asterisk|slashes))?)\s*\r?\n$", re.MULTILINE +) +_marker_regex = re.compile(r"^(rst|restructuredtext|embed:rst(:leading-(asterisk|slashes))?)\s*\r?\n", re.MULTILINE) + + +def _parse_content_regex(text: str) -> Iterable[str]: + + normalized_text = text.strip() + + if match := _search_regex.match(normalized_text): + marker = match.group("marker") + content = "\n" + normalized_text[len(marker) :] # remove marker at the front + + content_lines = content.split("\n")[1:] # split into lines ignoring the first \n (the "marker"-line) + + yield from content_lines + + +def _parse_content_new_regex(text: str) -> str: + + if match := _marker_regex.match(text): + # remove marker line + content = text[match.end() :] + + return content + + return "" + + +rst_block_with_breathe_leading_asterisk_marker = """ +
+
embed:rst:leading-asterisk
+    .. need:: test
+       :status: open
+
+       This is the description
+    
+
+""" + + +@pytest.mark.speed +def test_speed(): + + element = load_code_element(rst_block_with_breathe_leading_asterisk_marker) + content = element.text + count = 100000 + with TimedContext() as t1: + for i in range(0, count): + test = list(_parse_content_classic(content)) + print(f"classic implementation: {t1.elapsed()}") + + with TimedContext() as t2: + for i in range(0, count): + test = list(_parse_content_regex(content)) + print(f"regex implementation: {t2.elapsed()}") + + with TimedContext() as t3: + for i in range(0, count): + test = _parse_content_new_regex(content) + print(f"regex new implementation: {t3.elapsed()}") + + +if __name__ == "__main__": + test_speed() diff --git a/tests/html_parser/test_utils.py b/tests/html_parser/test_utils.py new file mode 100644 index 0000000..848ac22 --- /dev/null +++ b/tests/html_parser/test_utils.py @@ -0,0 +1,21 @@ +from typing import Any + +import lxml.etree as etree + + +def load_code_element(html_string: str) -> etree._Element: + """loads a single pre or code element in an html snippet string. + + There has to be only one pre or code element!! + """ + parser: Any = etree.HTMLParser() + e = etree.fromstring(html_string, parser) + result: Any = e.find(f".//pre") + if result is None: + result = e.find(f".//code") + return result + + +def clean_html_string(html: str) -> str: + """Removes whitespaces from html strings.""" + return html.replace("\n", "").replace(" ", "").replace("> ", ">").replace(" <", "<").strip() From c6a0c6fba7ca33085b78bb207265c5f136c6f6cf Mon Sep 17 00:00:00 2001 From: Markus Braun Date: Wed, 7 Dec 2022 15:17:01 +0100 Subject: [PATCH 5/8] chore(cleanup): code cleanup + minor doc fixes Signed-off-by: Markus Braun --- CONTRIBUTE.md | 2 +- conf.py | 1 + ...ease_process.md => release_process.md.tbd} | 0 doxysphinx/html_parser.py | 43 +++---------------- index.md | 1 - 5 files changed, 9 insertions(+), 38 deletions(-) rename docs/{release_process.md => release_process.md.tbd} (100%) diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md index c4245b4..80ab2a2 100644 --- a/CONTRIBUTE.md +++ b/CONTRIBUTE.md @@ -248,7 +248,7 @@ in the Signed-off-by tag. If your contribution is covered by this project's DCO's clause "(c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it", please add the -appropriate copyright holder(s) to the [NOTICE.md](NOTICE.md) file as part of your +appropriate copyright holder(s) to the [NOTICE.md](https://github.com/boschglobal/doxysphinx/blob/main/NOTICE.md) file as part of your contribution. [SubmittingPatches]: diff --git a/conf.py b/conf.py index 2d889b9..0ce1b85 100644 --- a/conf.py +++ b/conf.py @@ -56,6 +56,7 @@ "CHANGELOG.md", "README.md", "external/README.md", + "tests", ) # -- Options for HTML output ------------------------------------------------- diff --git a/docs/release_process.md b/docs/release_process.md.tbd similarity index 100% rename from docs/release_process.md rename to docs/release_process.md.tbd diff --git a/doxysphinx/html_parser.py b/doxysphinx/html_parser.py index 49c0020..d611f3d 100644 --- a/doxysphinx/html_parser.py +++ b/doxysphinx/html_parser.py @@ -7,10 +7,10 @@ # - Markus Braun, :em engineering methods AG (contracted by Robert Bosch GmbH) # ===================================================================================== """ -The html_parser module contains the html parsers that will load the html files. +The html_parser module contains the html parser that will load and process the html files. -To allow several :mod:`writer` implementation to pick up and handle the result of that parsing a html parser -must also transform rst snippets into -nodes. +To allow several :mod:`writer` implementations to pick up and handle the result of that parsing a html parser +in a neutral way the parser will change all relevant rst/sphinx markup elements to ``-elements. """ import logging @@ -27,7 +27,7 @@ @dataclass class HtmlParseResult: - """Capsules a parsed html tree with meta information.""" + """Capsules a parsed and processed html tree with meta information.""" html_input_file: Path """The html file that was parsed. @@ -175,8 +175,6 @@ class RstBlockProcessor: format = "rst" is_final = True - # _marker_regex = re.compile(r"^(rst|restructuredtext|embed:rst(:leading-(asterisk|slashes))?)\s*\r?\n", re.MULTILINE) - _marker_regex = re.compile( r"^(" # begin of the line r"{rst}" # doxysphinx marker @@ -363,26 +361,6 @@ def _ends_with_newline(text: str): return text.strip(" \t").endswith("\n") -_doxygen_comment_cleanup_regex = re.compile(r"^[^\S\r\n]*(\*|\/\/\/|\/\/!)", re.MULTILINE) -# ^ ^ ^ ^ -# | | | | -# | | | +--- two slashee followed by a exclamation mark -# | | | (strange comments...) -# | | +--- three slashes (/// - ms style comments) -# | +--- single star (qt style comments) -# +--- whitespace without newlines (zero or more) - - -def _remove_doxygen_comment_prefixes_old(text: str) -> str: - """Removes doxygen comment prefixes from texts.""" - return _doxygen_comment_cleanup_regex.sub("", text) - - # old faster but incorrect implementation - # text = text.replace("\n*", "\n").replace("\n *", "\n") - # text = text.replace("\n///", "\n").replace("\n//!", "\n") - # return text - - def _lstrip_str(to_strip: str, from_text: str) -> str: ws_stripped = from_text.lstrip() if ws_stripped.startswith(to_strip): @@ -394,14 +372,15 @@ def _lstrip_str(to_strip: str, from_text: str) -> str: def _remove_doxygen_comment_prefixes(text: str) -> str: stripped = text.lstrip() - # if leading slashes + # if leading slashes syntax if stripped.startswith("///"): lines = [_lstrip_str("///", line) for line in text.split("\n")] return "\n".join(lines) - # if asterisk + # if asterisk syntax elif stripped.startswith("*"): lines = [_lstrip_str("*", line) for line in text.split("\n")] return "\n".join(lines) + # if doubleslash exclamationmark syntax elif stripped.startswith("//!"): lines = [_lstrip_str("//!", line) for line in text.split("\n")] return "\n".join(lines) @@ -500,23 +479,15 @@ def parse(self, file: Path) -> HtmlParseResult: :rtype: ParseResult """ - self._logger.debug(f"[{file}]: parsing ...") - tree = etree.parse(file.as_posix()) # type: ignore # nosec B320 - self._logger.debug(f"[{file}]: extracting meta infos...") - meta_title: str = tree.find("//title").text # type: ignore first, *_, last = meta_title.split(":") project = first.strip() title = last.strip() - self._logger.debug(f"[{file}]: normalizing tree...") - used_snippet_formats = self._normalize_tree_and_get_used_formats(tree) - self._logger.debug(f"[{file}]: done.") - return HtmlParseResult(file, project, meta_title, title, used_snippet_formats, tree) def _should_parse(self, source: str) -> bool: diff --git a/index.md b/index.md index 08f5b3f..a72da56 100644 --- a/index.md +++ b/index.md @@ -37,7 +37,6 @@ docs/linking_needs.md :caption: Demo documentations :maxdepth: 1 Doxygen Demo -docs/test.rst ``` ```{toctree} From d328e76fe0e0640816907f90b8a5576dc569c26c Mon Sep 17 00:00:00 2001 From: Markus Braun Date: Wed, 7 Dec 2022 16:16:16 +0100 Subject: [PATCH 6/8] docs: minor documentation tweaks Signed-off-by: Markus Braun --- .gitignore | 1 + conf.py | 2 +- docs/getting_started.md | 11 +++----- docs/syntax/rst_block_syntax.md | 21 ++++++++++---- docs/syntax/rst_inline_syntax.md | 48 ++++++++++++++++++++------------ doxysphinx/html_parser.py | 6 ++-- 6 files changed, 54 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 382c629..68b6ee3 100644 --- a/.gitignore +++ b/.gitignore @@ -182,3 +182,4 @@ docs/doxygen/graphviz/ demo/ocilib/ demo/graphviz/ tests/toc/*.rst +docs/auto_api diff --git a/conf.py b/conf.py index 0ce1b85..1a1b36a 100644 --- a/conf.py +++ b/conf.py @@ -103,7 +103,7 @@ autoapi_dirs = ["doxysphinx"] autoapi_root = "docs/auto_api" autoapi_options = ["members", "undoc-members", "show-inheritance", "show-inheritance-diagram", "show-module-summary"] -autoapi_keep_files = False +autoapi_keep_files = True autoapi_add_toctree_entry = False autodoc_typehints = "signature" diff --git a/docs/getting_started.md b/docs/getting_started.md index 8fb2629..a08026b 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -135,9 +135,8 @@ The class :demo:`doxysphinx::rst::Car` implements the car. Next you have to prepare your doxygen configuration file (doxyfile) to have compatible settings with doxysphinx. -The following settings are mandatory and will be checked by a validator (only when using the doxyfile as input): - -You can prepare or optimize your doxygen configuration file (doxyfile) by doing the following: +The following settings are mandatory and will be checked by a validator if you use your doxyfile as input for +doxysphinx (if you use the doxygen html output directory instead validation will be skipped): ### Mandatory settings @@ -174,7 +173,7 @@ look at the [alternatives](./alternatives.md). ### Recommended settings -these settings are optional but strongly recommended (you will be warned in case of some value deviations): +these settings are optional but strongly recommended (you will be notified in case of some value deviations): ```yaml @@ -261,7 +260,7 @@ Please note that sphinx has slightly different output directories depending on t ### Clean -If you want to clean the files doxysphinx generated please use the clean command: +If you want to remove the files doxysphinx generated please use the clean command: ```bash doxysphinx clean @@ -326,5 +325,3 @@ Further reading: [Developer Quickstart](dev_guide.md). Or just start documenting 😀. - -test test *test* test diff --git a/docs/syntax/rst_block_syntax.md b/docs/syntax/rst_block_syntax.md index 92efec2..985331a 100644 --- a/docs/syntax/rst_block_syntax.md +++ b/docs/syntax/rst_block_syntax.md @@ -1,13 +1,15 @@ # Rst Block Syntax +For creating blocks of restructured text content in C++ documentation comments that will be rendered by Sphinx. + ## TLDR; Recommended Syntax - if you want to use the shortest possible syntax use [](#markdown-fences) with directive autodetection. - if you're coming from breathe and already have your code commented with breathe markers or if you want - to have maximum compatibility: use [](doxygen-aliases). + to have maximum compatibility: use [](#doxygen-aliases). However if you experience any problems with doxygen parsing etc. you might try one of the other options described -in [](supported-rst-block-delimiters-in-doxygen-comments). +in [](#supported-rst-block-delimiters-in-doxygen-comments). ## Markers @@ -23,8 +25,11 @@ The marker can be one of these: After any marker there has to be a new line (content can start at next line). -As chances are big that you just want to use a sphinx directive we've also got a **shortcut syntax** - -if the pre content starts with a directive you can leave out the `{rst}`-line, e.g. +### Directive Autodetection + +As chances are quite big that you just want to use a sphinx directive we've also got an autodetection feature: +if the "verbatim content" starts with a directive you can leave out the markers (in that case the directive syntax is +the marker), for example... ```cpp /// ``` @@ -33,7 +38,7 @@ if the pre content starts with a directive you can leave out the `{rst}`-line, e /// ``` ``` -will also be identified by doxysphinx as a rst block (and processed). +...will also be identified by doxysphinx as a rst block (and processed). ## Supported rst block delimiters in doxygen comments @@ -64,7 +69,7 @@ You can use the markdown code fences syntax as follows (you need to have markdow Something like \`\`\`cpp for example. However the doxygen markdown parser will swallow anything behind the delimiters (which means we couldn't make use of information there). So please do not fall into the trap trying to start a rst block with e.g. \`\`\`{rst}. The `{rst}` marker (or any content - used by doxysphinx) has to be on the next line. (This is only true if you use markdown code fences - not for + used by doxysphinx) **has to be on the next line**. (This is only true if you use markdown code fences - not for the other options below). ``` @@ -122,3 +127,7 @@ And then use the alias like this: /// /// ... ``` + +## More examples + +can be found in our demo documentation [here](../doxygen/demo/html/classdoxysphinx_1_1doxygen_1_1BlockRst.rst). diff --git a/docs/syntax/rst_inline_syntax.md b/docs/syntax/rst_inline_syntax.md index dd05f3b..f28d995 100644 --- a/docs/syntax/rst_inline_syntax.md +++ b/docs/syntax/rst_inline_syntax.md @@ -1,8 +1,11 @@ # Rst Inline Syntax +For creating inline restructured text content in C++ documentation comments that will be rendered by Sphinx. + ## TLDR; Recommended Syntax -Use the following (note that you have to replace the backticks usually used in rst/sphinx with quotes): +Use the following syntax in your C++ documentation comments to use a sphinx role in-line (note that you have +to replace the backticks usually used in rst/sphinx with quotes): ```cpp /// lorem ipsum, `:role:"content of the role"` dolor sit... @@ -15,22 +18,23 @@ e.g. ``` This will work only if markdown support is activated in doxygen (highly recommended). + Futhermore, please note that **you can only use sphinx roles and domains** in the inline syntax for now (reasoning see below). See below in the methods documentation for other options if you have markdown support disabled. -## Technical details +## Technical Details Skip this section if you're not interested in the technical details... Inline rst is a major problem because of the following: -1. Paragraphs all over the place +1. **Paragraphs all over the place** - Doxygen uses paragraph `

`-html-tags for it's content. Paragraph tags cannot have other block-level - tags inside them (even no other paragraph tags). The browsers (chromium-based, firefox etc.) are quite - aggressive in fixing bad nestings here (just to be able to display a page) so e.g. if a nested `

`-tag + Doxygen uses paragraphs (`

`-html-tags) for it's content. Paragraph tags cannot have other block-level + tags inside them (even no other paragraph tags). The browsers (chromium-based ones, Firefox etc.) are quite + aggressive in fixing bad nestings here (just to be able to display a page). So e.g. if a nested `

`-tag is noticed the browsers will close the outer `

`-tag right before the inner `

`-tag. This will linearize the `

`-tags and the page could be rendered. @@ -39,15 +43,16 @@ Inline rst is a major problem because of the following: Sphinx will automagically put `

`-tags around the inline-rst-block - it's doing that around all pure text based content and we cannot change that. - Most of the time this results in an html structure with nested p tags which will be "fixed" by the + Most of the time this results in an html structure with nested `

`-tags which will be "fixed" by the browsers on loading/rendering of the html page. Why is this a problem? because we cannot style - (in a css sense) away the blockiness if we have only sibling `

`-tags. + (in a css sense) away the blockiness if we have only sibling `

`-tags. But we have to for the content + to appear "in-line". Also we cannot fix the final html structure because we're too early in the process. We can only create rsts - which will then be picked up by sphinx to create html. + which will then be picked up by sphinx to create the final html. -2. Doxygen interpretation/preprocessing +2. **Doxygen interpretation/preprocessing** - The main use case for inline rst are sphinx roles which are normally written in a form like: + The main use case for inline rst are sphinx roles which are normally (in rst) written in a form like: ```plain :role_name:`role_content` @@ -58,22 +63,25 @@ Inline rst is a major problem because of the following: the following solutions/hacks have been applied to overcome the problems: -1. If we encounter a sphinx role in doxysphinx during original doxygen html parsing we change it's +1. **Html-Element-Transformation** + + If we encounter a sphinx role in doxysphinx during original doxygen html parsing we change it's parent html tag from `

`-tag to `

`-tag (because divs can have nested content). We also add a css class which we use to style the "blockiness" away (display:inline). The technical implementation is has more complexity - if you're interested just look into the code. -2. Doxysphinx scans the html for ``-tags but that's not enough. For doxysphinx to consider a code tag as inline - sphinx snippet it has to be in the format ``:role:`content```. The content can be delimited not only - by backticks. - Backticks are also markdown's verbatim inline delimiters and therefore can only be used when escaped (and even - then they create problems with doxygen's way of parsing). - Therefore we're also supporting quotes and ticks or no quotes at all (in that case we put them automatically after the :role: part and before the end). +2. **Adjusted Syntax for using inline rst and special parsing** + Doxysphinx scans the html for ``-tags but that's not enough. For doxysphinx to consider a ``-tag as inline + sphinx snippet it has to be in the format ``:role:`content``` - we validate the syntax here and if it doesn't match we ignore it. The implication is that **you cannot use anything other than roles/domains for inline rst**. In practice this means that you cannot use rst's external link syntax and references for now, which is however so cryptic that we're quite sure that you would rather consider using doxygens link command or just a markdown link. + Furthermore backticks are also markdown's verbatim inline delimiters and therefore can only be used when escaped (and even then they create problems with doxygen's way of parsing). + Therefore we're also supporting (and are recommending) quotes (") and ticks (') as role content delimiters. + So we relaxed the sphinx syntax a little bit here to work better in doxygen comments. + ## Supported rst inline delimiters in doxygen comments Technically doxysphinx searches for `
`- or `
`-elements in doxygen html output @@ -116,3 +124,7 @@ You can also use a html ``-element: /// /// A html tt element with escaped backticks like this - :doc:\`Main Documentation \` - will work. ``` + +## More examples + +can be found in our demo documentation [here](../doxygen/demo/html/classdoxysphinx_1_1doxygen_1_1InlineRst.rst). diff --git a/doxysphinx/html_parser.py b/doxysphinx/html_parser.py index d611f3d..bbcbffd 100644 --- a/doxysphinx/html_parser.py +++ b/doxysphinx/html_parser.py @@ -266,11 +266,11 @@ def try_process(self, element: _Element) -> bool: class MarkdownRstBlockProcessor: """Element Processor for doxygen markdown block elements. - This processor will check if the first line in the markdown block is either `{rst}` (as marker) or - if the line + This processor will check if the first line in the markdown block is either a supported marker or + a directive (auto detection feature). Markdown block elements in doxygen are getting rendered different to verbatim content. - Each Markdown block (delimited with ```) will be something like this in html: + Each Markdown block (delimited with 3 backticks) will be something like this in html: .. code-block:: html From 8b84b87e0f2251fed31414b9eb56ee7f38e1d50c Mon Sep 17 00:00:00 2001 From: Markus Braun Date: Fri, 9 Dec 2022 08:50:13 +0100 Subject: [PATCH 7/8] chore(code): fixed some module docs warnings Signed-off-by: Markus Braun --- doxysphinx/html_parser.py | 40 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/doxysphinx/html_parser.py b/doxysphinx/html_parser.py index bbcbffd..3ec6ccf 100644 --- a/doxysphinx/html_parser.py +++ b/doxysphinx/html_parser.py @@ -106,7 +106,7 @@ class ElementProcessor(Protocol): """The format this element processor processes... like 'rst', 'md' etc.""" def try_process(self, element: _Element) -> bool: - """tries to process an element. + """Try to process an element. :param element: The element to check and process :return: Whether the "processor did it's thing"/"processing was applied" (True) or not (False) @@ -185,12 +185,11 @@ class RstBlockProcessor: ) def try_process(self, element: _Element) -> bool: - """Tries to process an rst block element into a neutralized format. + """Try to process an rst block element into a neutralized format. :param element: The html element to process :return: True if the element was processed else False """ - text = _flattened_element_text(element) if not text: return False @@ -232,12 +231,11 @@ class PreToDivProcessor: is_final = True def try_process(self, element: _Element) -> bool: - """Transforms a pre element into a div element. + """Transform a pre element into a div element. :param element: The html element to process :return: True if the element was processed else False """ - text = _flattened_element_text(element) if not text: return False @@ -302,7 +300,7 @@ class MarkdownRstBlockProcessor: ) def try_process(self, element: _Element) -> bool: - """Tries to process an rst block element into a neutralized format. + """Try to process an rst block element into a neutralized format. :param element: The html element to process :return: True if the element was processed else False @@ -336,7 +334,7 @@ def try_process(self, element: _Element) -> bool: def _flattened_element_text(element: _Element) -> str: - """flattens (removes children but keeps the text and html nodes) and element text.""" + """flatten (removes children but keeps the text and html nodes) an element text.""" text = element.text if not text: return "" @@ -410,7 +408,7 @@ def _try_parse_rst_block_content(text: str) -> Optional[str]: def _ensure_newline_before_element(element: _Element): - """Ensures that there is at least one newline character (\\n) before the given element. + """Ensure that there is at least one newline character (\\n) before the given element. We need this later during the write phase (see :mod:`writer`) which is line oriented. When we have a newline in front of our elements we can find them more easily/efficiently. @@ -434,12 +432,11 @@ def _ensure_newline_before_element(element: _Element): def _ensure_newline_after_element(element: _Element): - """Ensures that there is at least one newline character (\\n) after the given element. + """Ensure that there is at least one newline character (\\n) after the given element. We need this later during the write phase (see :mod:`writer`) which is line oriented. When we have a newline after of our elements we can find them more easily/efficiently. """ - if not element.tail: element.tail = "\n" return @@ -453,6 +450,13 @@ class DoxygenHtmlParser: _logger = logging.getLogger(__name__) + _processors: List[ElementProcessor] = [ + RstInlineProcessor(), + RstBlockProcessor(), + MarkdownRstBlockProcessor(), + PreToDivProcessor(), + ] + def __init__(self, source_directory: Path): """ Create an instance of a doxygen html parser. @@ -462,23 +466,14 @@ def __init__(self, source_directory: Path): self._source_directory = source_directory # self._parser = etree.HTMLParser(huge_tree=True, recover=False) - self._processors: List[ElementProcessor] = [ - RstInlineProcessor(), - RstBlockProcessor(), - MarkdownRstBlockProcessor(), - PreToDivProcessor(), - ] - def parse(self, file: Path) -> HtmlParseResult: - """ - Parse a doxygen HTML file into an ElementTree and normalizes its inner data to contain -tags. + """Parse a doxygen HTML file into an ElementTree and normalize its inner data to contain -tags. :param file: The html file to parse :type file: Path :return: The result of the parsing :rtype: ParseResult """ - tree = etree.parse(file.as_posix()) # type: ignore # nosec B320 meta_title: str = tree.find("//title").text # type: ignore @@ -500,9 +495,10 @@ def _should_parse(self, source: str) -> bool: return True + @staticmethod @lru_cache(maxsize=2) - def _all_supported_elements(self) -> Set[str]: - return {e for p in self._processors for e in p.elements} + def _all_supported_elements() -> Set[str]: + return {e for p in DoxygenHtmlParser._processors for e in p.elements} def _normalize_tree_and_get_used_formats(self, tree) -> Set[str]: """ From 7580bbf12773f6a904673f9c7523b16a339c668a Mon Sep 17 00:00:00 2001 From: Markus Braun Date: Fri, 9 Dec 2022 08:57:18 +0100 Subject: [PATCH 8/8] chore(code): fixed some module docs warnings Signed-off-by: Markus Braun --- doxysphinx/html_parser.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doxysphinx/html_parser.py b/doxysphinx/html_parser.py index 3ec6ccf..5be3530 100644 --- a/doxysphinx/html_parser.py +++ b/doxysphinx/html_parser.py @@ -126,7 +126,7 @@ class RstInlineProcessor: ) def try_process(self, element: _Element) -> bool: - """Tries to process an rst inline element into a neutralized format. + """Try to process an rst inline element into a neutralized format. :param element: The html element to process :return: True if the element was processed else False @@ -334,7 +334,7 @@ def try_process(self, element: _Element) -> bool: def _flattened_element_text(element: _Element) -> str: - """flatten (removes children but keeps the text and html nodes) an element text.""" + """Flatten (removes children but keeps the text and html nodes) an element text.""" text = element.text if not text: return "" @@ -501,15 +501,13 @@ def _all_supported_elements() -> Set[str]: return {e for p in DoxygenHtmlParser._processors for e in p.elements} def _normalize_tree_and_get_used_formats(self, tree) -> Set[str]: - """ - Normalize a doxygen html tree. + """Normalize a doxygen html tree. Searches for pre and code tags, re-formats them and creates different -tags out of it. Will also put a newline behind the closing tag because it's necessary to have lines that can be clearly assigned to either html-content or snippet content (and in the un-normalized source html we've got them mixed at the closing tag). """ - used_snipped_formats = set() # prefetch element candidates.