diff --git a/.github/workflows/mkdoxy-test-demos.yaml b/.github/workflows/mkdoxy-test-demos.yaml index 8d09ba83..9fedfc55 100644 --- a/.github/workflows/mkdoxy-test-demos.yaml +++ b/.github/workflows/mkdoxy-test-demos.yaml @@ -24,7 +24,7 @@ jobs: python -m pip install -e ".[dev]" sudo apt-get install doxygen - name: Clone test repo - run: git clone https://github.com/JakubAndrysek/MkDoxy-demo.git demo + run: git clone --branch core-update https://github.com/JakubAndrysek/MkDoxy-demo.git demo - name: Build docs run: | cd demo diff --git a/devdeps.txt b/devdeps.txt index 43234dad..e0116ae5 100644 --- a/devdeps.txt +++ b/devdeps.txt @@ -7,3 +7,5 @@ pre-commit~=3.7.0 setuptools~=70.0.0 build~=1.2.2 twine~=5.1.1 +sourcery~=1.34.0 +click~=8.1.8 diff --git a/mkdocs.yml b/mkdocs.yml index c67d1c20..0e7b75c1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -78,11 +78,11 @@ plugins: enabled: !ENV [ENABLE_MKDOXY, True] projects: mkdoxyApi: - src-dirs: mkdoxy - full-doc: True - template-dir: templates-custom - doxy-cfg-file: demo-projects/animal/Doxyfile - doxy-cfg: + src_dirs: mkdoxy + full_doc: True + template_dir: templates-custom +# doxy_config_file: demo-projects/animal/Doxyfile + doxy_config_dict: FILE_PATTERNS: "*.py" EXAMPLE_PATH: "" RECURSIVE: True @@ -90,17 +90,44 @@ plugins: JAVADOC_AUTOBRIEF: True EXTRACT_ALL: True animal: - src-dirs: demo-projects/animal - full-doc: True - doxy-cfg: + src_dirs: demo-projects/animal + full_doc: True + doxy_config_dict: FILE_PATTERNS: "*.cpp *.h*" EXAMPLE_PATH: examples RECURSIVE: True - save-api: .mkdoxy - full-doc: True +# save_api: .mkdoxy + full_doc: True debug: False - ignore-errors: False - emojis-enabled: True + ignore_errors: False + +# - mkdoxy: +# enabled: !ENV [ENABLE_MKDOXY, True] +# projects: +# mkdoxyApi: +# src-dirs: mkdoxy +# full-doc: True +# template-dir: templates-custom +# doxy-cfg-file: demo-projects/animal/Doxyfile +# doxy-cfg: +# FILE_PATTERNS: "*.py" +# EXAMPLE_PATH: "" +# RECURSIVE: True +# OPTIMIZE_OUTPUT_JAVA: True +# JAVADOC_AUTOBRIEF: True +# EXTRACT_ALL: True +# animal: +# src-dirs: demo-projects/animal +# full-doc: True +# doxy-cfg: +# FILE_PATTERNS: "*.cpp *.h*" +# EXAMPLE_PATH: examples +# RECURSIVE: True +# save-api: .mkdoxy +# full-doc: True +# debug: False +# ignore-errors: False +# emojis-enabled: True markdown_extensions: - pymdownx.highlight diff --git a/mkdoxy/__main__.py b/mkdoxy/__main__.py new file mode 100644 index 00000000..0b3242c6 --- /dev/null +++ b/mkdoxy/__main__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +from mkdoxy.cli import main + +if __name__ == "__main__": + main() diff --git a/mkdoxy/cli.py b/mkdoxy/cli.py new file mode 100644 index 00000000..75d6a900 --- /dev/null +++ b/mkdoxy/cli.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +from pathlib import Path +import click +from mkdoxy.migration import update_new_config + + +@click.group() +def main(): + """mkdoxy - Command line tool for managing Doxygen configuration migration.""" + pass + + +@click.command() +@click.argument("yaml_file", type=click.Path(exists=True)) +@click.option("--no-backup", is_flag=True, help="Do not backup old config to mkdocs.1_old.yaml") +def migrate(yaml_file, no_backup): + """ + Migrate mkdoxy configuration to a new version. + + :param yaml_file: Path to the mkdocs.yaml file. + :param no_backup: Do not backup the old config to mkdocs.1_old.yaml. + """ + backup_file_name = "mkdocs.1_old.yaml" + update_new_config(Path(yaml_file), not no_backup, backup_file_name) + click.echo("Migration completed successfully") + if not no_backup: + click.echo(f"Old config was backed up as '{backup_file_name}'") + + +@click.command() +def version(): + """ + Display the version of the mkdoxy package. + """ + try: + import importlib.metadata + + package_version = importlib.metadata.version("mkdoxy") + except Exception: + package_version = "Unknown" + click.echo("MkDoxy: https://github.com/JakubAndrysek/MkDoxy") + click.echo(f"Version: {package_version}") + + +main.add_command(migrate) +main.add_command(version) + +if __name__ == "__main__": + main() diff --git a/mkdoxy/constants.py b/mkdoxy/constants.py index c9be98ea..efe314f1 100644 --- a/mkdoxy/constants.py +++ b/mkdoxy/constants.py @@ -168,3 +168,6 @@ class Visibility(Enum): PACKAGE = "package" PROTECTED = "protected" PRIVATE = "private" + + +JINJA_EXTENSIONS = (".jinja2", ".j2", ".jinja") diff --git a/mkdoxy/doxy_config.py b/mkdoxy/doxy_config.py new file mode 100644 index 00000000..956075db --- /dev/null +++ b/mkdoxy/doxy_config.py @@ -0,0 +1,204 @@ +import logging +from pathlib import Path + +from mkdocs.config import Config +from mkdocs.config import config_options as c + +log: logging.Logger = logging.getLogger("mkdocs") + +config_scheme_legacy = { + "full-doc": "full_doc", + "ignore-errors": "ignore_errors", + "save-api": "custom_api_folder", + "doxygen-bin-path": "doxygen_bin_path", +} + +config_project_legacy = { + "src-dirs": "src_dirs", + "full-doc": "full_doc", + "ignore-errors": "ignore_errors", + "doxy-cfg": "doxy_config_dict", + "doxy-cfg-file": "doxy_config_file", + "template-dir": "custom_template_dir", +} + + +class MkDoxyConfigProject(Config): + """! Configuration for each project in the MkDoxy configuration file. + @details New type of configuration for each project in the MkDoxy configuration file. + It will replace the old configuration type. + + @param src_dirs: (str) Source directories for Doxygen - INPUT + @param full_doc: (bool) Generate full documentation + @param debug: (bool) Debug mode + @param ignore_errors: (bool) Ignore errors + @param doxy_config_dict: (dict) Doxygen additional configuration + @param doxy_config_default: (bool) Use default MkDoxy Doxygen configuration + @param doxy_config_file: (str) Doxygen configuration file + @param doxy_config_file_force: (bool) Do not use default MkDoxy Doxygen configuration, use only Doxygen configuration file + @param custom_template_dir: (str) Custom template directory + """ + + src_dirs = c.Type(str) + full_doc = c.Type(bool, default=True) + debug = c.Type(bool, default=False) + ignore_errors = c.Type(bool, default=False) + doxy_config_dict = c.Type(dict, default={}) + doxy_config_default = c.Type(bool, default=True) + doxy_config_file = c.Optional(c.Type(Path)) + doxy_config_file_force = c.Type(bool, default=False) + custom_template_dir = c.Optional(c.Type(str)) + + +class MkDoxyConfig(Config): + """! Global configuration for the MkDoxy plugin. + @details New type of global configuration for the MkDoxy plugin. It will replace the old configuration type. + @param projects: (dict) Project configuration - multiple projects + @param full_doc: (bool) Generate full documentation - global (all projects) + @param debug: (bool) Debug mode + @param ignore_errors: (bool) Ignore errors + @param custom_api_folder: (str) Custom API folder for Doxygen and MD output (default in temp folder) + @param doxygen_bin_path: (str) Path to Doxygen binary - default "doxygen" + """ + + projects = c.DictOfItems(c.SubConfig(MkDoxyConfigProject), default={}) # project configuration - multiple projects + full_doc = c.Type(bool, default=True) # generate full documentation - global (all projects) + debug = c.Type(bool, default=False) # debug mode + ignore_errors = c.Type(bool, default=False) # ignore errors + custom_api_folder = c.Optional(c.Type(str)) # custom API folder for Doxygen and MD output (default in temp folder) + doxy_config_dict = c.Type( + dict, default={} + ) # Doxygen additional configuration - it is overwritten by project config + doxygen_bin_path = c.Type(Path, default=Path("doxygen")) # path to Doxygen binary (default "doxygen" + + generate_diagrams = c.Type(bool, default=False) # generate diagrams + generate_diagrams_format = c.Choice(("svg", "png", "jpg", "gif"), default="svg") # diagram format + generate_diagrams_type = c.Choice(("dot", "uml"), default="dot") # diagram type + + +# def load_config_by_key(key: str, legacy_key: str, config: Config, legacy: list) -> any: +# """! Load the configuration value from the global configuration +# @details Legacy config option is by default None, but if it is not None, it will print a warning and return value. +# @param key: (str) The new configuration key. +# @param legacy_key: (str) The legacy configuration key. +# @param config: (Config) The global configuration object. +# @param legacy: (list) The list of legacy configuration options. +# @return: (Optional[str]) The configuration value. +# """ +# if config.get(legacy_key) is not None: +# legacy.append(f"Found legacy configuration options: '{legacy_key}' -> replace with '{key}'") +# return config.get(legacy_key) +# return config.get(key) +# +# +# def process_configuration(config: Config) -> MkDoxyConfig: +# """! Process the configuration for the MkDoxy plugin +# @details Process the configuration for the MkDoxy plugin and validate the configuration. +# It will try to load new configuration, but it will also check for legacy configuration options. +# @param config: (Config) The global configuration object. +# @return: (MkDoxyConfig) The new validated configuration object. +# @throws ConfigurationError: If the configuration is invalid. +# """ +# legacy_options = [] +# doxy_config = MkDoxyConfig() +# doxy_config.full_doc = load_config_by_key("full_doc", "full-doc", config, legacy_options) +# doxy_config.debug = config.get("debug", False) +# doxy_config.ignore_errors = load_config_by_key("ignore_errors", "ignore-errors", config, legacy_options) +# doxy_config.custom_api_folder = load_config_by_key("custom_api_folder", "save-api", config, legacy_options) +# doxy_config.doxygen_bin_path = load_config_by_key("doxygen_bin_path", "doxygen-bin-path", config, legacy_options) +# +# doxy_config.generate_diagrams = config.get("generate_diagrams") +# doxy_config.generate_diagrams_format = config.get("generate_diagrams_format") +# doxy_config.generate_diagrams_type = config.get("generate_diagrams_type") +# +# # Validate the global configuration +# validate_project_config(doxy_config, legacy_options) +# +# # Validate and load project configuration +# for project_name, project_cfg in config.get("projects", {}).items(): +# doxy_config.projects[project_name] = load_project_config(project_cfg, project_name) +# +# return doxy_config +# +# +# def validate_project_config(doxy_cfg: Config, legacy_options: list[str]) -> None: +# """! Validate the project configuration for the MkDoxy plugin +# @details Validate the project configuration for the MkDoxy plugin and check for errors and warnings. +# @param doxy_cfg: (MkDoxyConfig) The project configuration object. +# @param legacy_options: (list) The list of problems. +# @return: None +# @throws ConfigurationError: If the configuration is invalid. +# """ +# if legacy_options: +# log.warning("Found some legacy configuration options, please update your configuration!") +# log.warning("Run command 'mkdoxy migrate mkdocs.yaml' to update your configuration to the new format!") +# log.warning("More information in the documentation: https://mkdoxy.kubaandrysek.cz/") +# for problem in legacy_options: +# log.warning(f" -> {problem}") +# +# failed, warnings = doxy_cfg.validate() +# +# for config_name, warning in warnings: +# log.warning(f" -> Config value: '{config_name}'. Warning: {warning}") +# +# for config_name, error in failed: +# log.error(f" -> Config value: '{config_name}'. Error: {error}") +# raise exceptions.ConfigurationError(f"Config value: '{config_name}'. Error: {error}") +# +# +# def load_project_config_by_key(key: str, legacy_key: str, project_cfg: dict, project_name: str, problems: list) -> any: +# """! Load the project configuration value from the project configuration +# @details Legacy project config option is by default None, but if it is not None, +# it will print a warning and return the value. +# @param key: (str) The new project configuration key. +# @param legacy_key: (str) The legacy project configuration key. +# @param project_cfg: (dict) The project configuration object. +# @param project_name: (str) The project name. +# @param problems: (list) The list of problems. +# @return: (Optional[str]) The project configuration value. +# """ +# if project_cfg.get(legacy_key) is not None: +# problems.append( +# f"Found legacy configuration options: '{legacy_key}' -> replace with '{key}'" +# f" in project '{project_name}'" +# ) +# return project_cfg.get(legacy_key) +# return project_cfg.get(key) +# +# +# def load_project_config(project_cfg: dict, project_name: str) -> MkDoxyConfigProject: +# """! Load the project configuration for the MkDoxy plugin +# @details Load the project configuration for the MkDoxy plugin and validate the configuration. +# @param project_cfg: (dict) The project configuration object. +# @param project_name: (str) The project name. +# @return: (MkDoxyConfigProject) The new validated project configuration object. +# """ +# legacy_options = [] +# doxy_project_cfg = MkDoxyConfigProject() +# doxy_project_cfg.src_dirs = load_project_config_by_key( +# "src_dirs", "src-dirs", project_cfg, project_name, legacy_options +# ) +# +# doxy_project_cfg.full_doc = load_project_config_by_key( +# "full_doc", "full-doc", project_cfg, project_name, legacy_options +# ) +# doxy_project_cfg.debug = project_cfg.get("debug", False) +# doxy_project_cfg.ignore_errors = load_project_config_by_key( +# "ignore_errors", "ignore-errors", project_cfg, project_name, legacy_options +# ) +# doxy_project_cfg.doxy_config_dict = load_project_config_by_key( +# "doxy_config_dict", "doxy-cfg", project_cfg, project_name, legacy_options +# ) +# +# validate_config_file: Optional[str] = load_project_config_by_key( +# "doxy_config_file", "doxy-cfg-file", project_cfg, project_name, legacy_options +# ) +# doxy_project_cfg.doxy_config_file = None if validate_config_file is None else Path(validate_config_file) +# +# validate_template_dir: Optional[str] = load_project_config_by_key( +# "custom_template_dir", "template-dir", project_cfg, project_name, legacy_options +# ) +# doxy_project_cfg.custom_template_dir = None if validate_template_dir is None else Path(validate_template_dir) +# +# validate_project_config(doxy_project_cfg, legacy_options) +# return doxy_project_cfg diff --git a/mkdoxy/doxygen_generator.py b/mkdoxy/doxygen_generator.py new file mode 100644 index 00000000..09f8d5f3 --- /dev/null +++ b/mkdoxy/doxygen_generator.py @@ -0,0 +1,401 @@ +import hashlib +import logging +import os +import re +import shutil +from pathlib import Path +from subprocess import PIPE, Popen + +from mkdocs import exceptions + +from mkdoxy.doxy_config import MkDoxyConfig, MkDoxyConfigProject + +log: logging.Logger = logging.getLogger("mkdocs") + + +class DoxygenGenerator: + """! Class for running Doxygen. + @details This class is used to run Doxygen and parse the XML output. + """ + + def __init__( + self, + doxy_config: MkDoxyConfig, + project_config: MkDoxyConfigProject, + temp_doxy_folder: Path, + ): + """! Constructor. + @details + @param doxy_config: (MkDoxyConfig) Doxygen configuration. + @param project_config: (MkDoxyConfigProject) Project configuration. + @param temp_doxy_folder: (Path) Temporary Doxygen folder. + """ + self.doxy_config = doxy_config + self.project_config = project_config + self.temp_doxy_folder = temp_doxy_folder + + if not self.is_doxygen_valid_path(doxy_config.doxygen_bin_path): + raise DoxygenBinPathNotValid( + f"Invalid Doxygen binary path: {doxy_config.doxygen_bin_path}\n" + f"Make sure Doxygen is installed and the path is correct.\n" + f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/#configure-custom-doxygen-binary." + ) + + @staticmethod + def get_doxy_format_config() -> dict: + """ + @brief Get the default Doxygen format configuration. + @details Default Doxygen configuration options: + @details - GENERATE_XML: YES + @details - GENERATE_HTML: NO + @details - GENERATE_LATEX: NO + """ + return { + "GENERATE_XML": True, + "GENERATE_HTML": False, + "GENERATE_LATEX": False, + } + + @staticmethod + def get_doxy_default_config() -> dict: + """ + @brief Get the default Doxygen configuration. + @details Default Doxygen configuration options: + @details - DOXYFILE_ENCODING: UTF-8 + @details - RECURSIVE: YES + @details - EXAMPLE_PATH: examples + @details - SHOW_NAMESPACES: YES + """ + return { + "DOXYFILE_ENCODING": "UTF-8", + "RECURSIVE": True, + "EXAMPLE_PATH": "examples", + "SHOW_NAMESPACES": True, + } + + def get_doxy_diagrams_config(self) -> dict: + """ + @brief Get the Doxygen diagrams configuration. + @details Doxygen diagrams configuration options: + @details - HAVE_DOT: YES + @details - DOT_IMAGE_FORMATS: + @details - UML_LOOK: YES if is "uml", NO otherwise + @details - DOT_CLEANUP: NO + @details - GENERATE_LEGEND: NO + @details - SEARCHENGINE: NO + @details - GENERATE_HTML: YES (required for diagrams) + """ + return { + "HAVE_DOT": True, + "DOT_IMAGE_FORMATS": self.doxy_config.generate_diagrams_format, + "UML_LOOK": self.doxy_config.generate_diagrams_type == "uml", + "DOT_CLEANUP": False, + "GENERATE_LEGEND": False, + "SEARCHENGINE": False, + "GENERATE_HTML": True, + } + + # have to be tested + # doxy_config["CLASS_DIAGRAMS"] = "YES" + # doxy_config["COLLABORATION_GRAPH"] = "YES" + # doxy_config["INCLUDE_GRAPH"] = "YES" + # doxy_config["GRAPHICAL_HIERARCHY"] = "YES" + # doxy_config["CALL_GRAPH"] = "YES" + # doxy_config["CALLER_GRAPH"] = "YES" + + def get_doxy_config_file(self): + """! Get the Doxygen configuration from the provided file. + @details + @return: (dict) Doxygen configuration from the provided file. + """ + return self.str2dox_dict(self.get_doxy_config_file_raw(), self.project_config.doxy_config_file) + + def get_doxy_config_file_raw(self): + """! Get the Doxygen configuration from the provided file. + @details + @return: (str) Doxygen configuration from the provided file. + """ + try: + with open(self.project_config.doxy_config_file, "r") as file: + return file.read() + except FileNotFoundError as e: + raise DoxygenCustomConfigNotFound( + f"Custom Doxygen config file not found\n" + f"Make sure the path is correct." + f"Loaded path: '{self.project_config.doxy_config_file}'\n" + f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/#configure-custom-doxygen-configuration-file.\n" + ) from e + + def get_merged_doxy_dict(self) -> dict: + """! Get the merged Doxygen configuration. + @details The merged Doxygen configuration is created by merging multiple configurations. + @details The hierarchy is as follows: + @details - If a Doxygen config file is provided, it is used. + @details - If not, the default Doxygen configuration is used. + @details - Merge the INPUT directories from the mkdocs.yml file with the Doxygen configuration. + @details - Add the OUTPUT_DIRECTORY to the temporary Doxygen folder. + @details - Update configuration with the project format configuration. + @details - Update configuration with the default configuration. + @details - Update configuration with the project configuration. + @details - Update configuration with the diagrams configuration if enabled. + @return: (dict) Merged Doxygen configuration. + """ + doxy_dict = {} + + # Update with Doxygen config file if provided + if self.project_config.doxy_config_file: + doxy_dict.update(self.get_doxy_config_file()) + else: + doxy_dict.update(self.get_doxy_default_config()) + + # Merge INPUT directories from the mkdocs.yml file with the Doxygen configuration + doxy_dict["INPUT"] = self.merge_doxygen_input( + self.project_config.src_dirs, doxy_dict.get("INPUT", ""), self.get_doxygen_run_folder() + ) + + # OUTPUT_DIRECTORY is always the temporary Doxygen folder + doxy_dict["OUTPUT_DIRECTORY"] = str(self.temp_doxy_folder) + + # Update with the project format configuration + doxy_dict.update(self.get_doxy_format_config()) + + # Update with the default configuration + doxy_dict.update(self.doxy_config.doxy_config_dict) + + # Update with the project configuration + doxy_dict.update(self.project_config.doxy_config_dict) + + if self.doxy_config.generate_diagrams: + doxy_dict.update(self.get_doxy_diagrams_config()) + + if doxy_dict["INPUT"] == "": + raise exceptions.PluginError( + "No INPUT directories provided for Doxygen.\n" + "Make sure to provide at least one source directory." + "Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/#configure-custom-doxygen-configuration-file." + ) + + log.debug(f"- Doxygen INPUT: {doxy_dict['INPUT']}") + + return doxy_dict + + @staticmethod + def merge_doxygen_input(src_dirs: str, doxy_input: str, doxygen_run_folder: Path) -> str: + """! Merge the INPUT directories from the mkdocs.yml file with the Doxygen configuration. + + @details Both `src_dirs` and `doxy_input` should be space-separated strings. + Each path is resolved relative to `doxygen_run_folder`. + The function returns a space-separated string of unique relative paths. + + @param src_dirs: (str) Source directories from the mkdocs.yml file. + @param doxy_input: (str) Doxygen INPUT directories. + @param doxygen_run_folder: (Path) The folder to execute + @return: (str) Merged INPUT directories. + """ + # If either input is empty, return the other. + if not src_dirs: + return doxy_input + if not doxy_input: + return src_dirs + + base_dir = doxygen_run_folder.resolve() + + abs_paths = {(base_dir / path_str).resolve() for path_str in src_dirs.split()} + for path_str in doxy_input.split(): + abs_paths.add((base_dir / path_str).resolve()) + + # Convert absolute paths back to relative ones and sort for consistency + relative_paths = sorted(os.path.relpath(p, base_dir) for p in abs_paths) + + return " ".join(relative_paths) + + @staticmethod + def is_doxygen_valid_path(doxygen_bin_path: Path) -> bool: + """! Check if the Doxygen binary path is valid. + @details Accepts a full path or just 'doxygen' if it exists in the system's PATH. + @param doxygen_bin_path: (str) The path to the Doxygen binary or just 'doxygen'. + @return: (bool) True if the Doxygen binary path is valid, False otherwise. + """ + # If the path is just 'doxygen', search for it in the system's PATH + if str(doxygen_bin_path) == "doxygen": + return shutil.which("doxygen") is not None + + # Use pathlib to check if the provided full path is a file and executable + return doxygen_bin_path.is_file() and os.access(doxygen_bin_path, os.X_OK) + + # Source of dox_dict2str: https://xdress-fabio.readthedocs.io/en/latest/_modules/xdress/doxygen.html#XDressPlugin + + @staticmethod + def str2dox_dict(dox_str: str, config_file: str = "???") -> dict: + """! Convert a string from a doxygen config file to a dictionary. + @details + @param dox_str: (str) String from a doxygen config file. + @return: (dict) Dictionary. + """ + dox_dict = {} + dox_str = re.sub(r"\\\s*\n\s*", "", dox_str) + pattern = r"^\s*([^=\s]+)\s*(=|\+=)\s*(.*)$" + + try: + for line in dox_str.split("\n"): + if line.strip() == "" or line.startswith("#"): + continue + match = re.match(pattern, line) + if not match: + raise DoxygenCustomConfigNotValid( + f"Invalid line: '{line}'" + f"In custom Doxygen config file: {config_file}\n" + f"Make sure the file is in standard Doxygen format." + f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." + ) + key, operator, value = match.groups() + value = value.strip() + if operator == "=": + if value == "YES": + dox_dict[key] = True + elif value == "NO": + dox_dict[key] = False + else: + dox_dict[key] = value + if operator == "+=": + dox_dict[key] = f"{dox_dict[key]} {value}" + except ValueError as e: + raise DoxygenCustomConfigNotValid( + f"Invalid custom Doxygen config file: {config_file}\n" + f"Make sure the file is in standard Doxygen format." + f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." + ) from e + return dox_dict + + @staticmethod + def dox_dict2str(dox_dict: dict) -> str: + """! Convert a dictionary to a string that can be written to a doxygen config file. + @details + @param dox_dict: (dict) Dictionary to convert. + @return: (str) String that can be written to a doxygen config file. + """ + string = "" + new_line = "{option} = {value}\n" + items = sorted(dox_dict.items()) + for key, value in items: + if value is True: + value_transformed = "YES" + elif value is False: + value_transformed = "NO" + else: + value_transformed = value + + string += new_line.format(option=key.upper(), value=value_transformed) + + # Don't need an empty line at the end + return string.strip() + + @staticmethod + def hash_write(file_name: Path, hash_key: str): + """! Write the hash to the file. + @details + @param file_name: (Path) Path to the file where the hash will be saved. + @param hash_key: (str) Hash. + """ + with open(file_name, "w") as hash_file: + hash_file.write(hash_key) + + @staticmethod + def hash_read(file_name: Path) -> str: + """! Read the hash from the file. + @details + @param file_name: (Path) Path to the file with the hash. + @return: (str) Hash. + """ + with open(file_name, "r") as hash_file: + return str(hash_file.read()) + + def has_changes(self) -> bool: + """! Check if the source files have changed since the last run. + @details + @return: (bool) True if the source files have changed since the last run. + """ + sha1 = hashlib.sha1() + sources = self.project_config.src_dirs.split(" ") + # Code from https://stackoverflow.com/a/22058673/15411117 + BUF_SIZE = 65536 # let's read stuff in 64kb chunks! + for source in sources: + for path in Path(source).rglob("*.*"): + if path.is_file(): + with open(path, "rb") as file: + while True: + data = file.read(BUF_SIZE) + if not data: + break + sha1.update(data) + + hash_new = sha1.hexdigest() + hash_file_name: Path = Path("mkdoxy_hash.txt") + hash_file_path = Path.joinpath(self.temp_doxy_folder, hash_file_name) + if hash_file_path.is_file(): + hash_old = self.hash_read(hash_file_path) + if hash_new == hash_old: + return False # No changes in the source files + + self.hash_write(hash_file_path, hash_new) + return True + + def run(self) -> None: + """! Run Doxygen with the current configuration using the Popen class. + @details + """ + doxy_builder = Popen( + [self.doxy_config.doxygen_bin_path, "-"], + stdout=PIPE, + stdin=PIPE, + stderr=PIPE, + ) + + if self.project_config.doxy_config_file_force: + doxy_str = self.get_doxy_config_file_raw() + else: + doxy_str = self.dox_dict2str(self.get_merged_doxy_dict()) + stdout_data, stderr_data = doxy_builder.communicate(input=doxy_str.encode("utf-8")) + if doxy_builder.returncode != 0: + error_message = ( + f"Error running Doxygen (exit code {doxy_builder.returncode}): {stderr_data.decode('utf-8')}" + ) + raise exceptions.PluginError(error_message) + + def get_output_xml_folder(self) -> Path: + """! Get the path to the XML output folder. + @details + @return: (Path) Path to the XML output folder. + """ + return Path.joinpath(self.temp_doxy_folder, Path("xml")) + + def get_output_html_folder(self) -> Path: + """! Get the path to the HTML output folder. + @details + @return: (Path) Path to the HTML output folder. + """ + return Path.joinpath(self.temp_doxy_folder, Path("html")) + + def get_doxygen_run_folder(self): + """! Get the working directory to execute Doxygen in. Important to resolve relative paths. + @details When a doxygen config file is provided, this is its containing directory. Otherwise, it's the current + working directory. + @return: (Path) Path to the folder to execute Doxygen in. + """ + if not self.project_config.doxy_config_file: + return Path.cwd() + + return Path(self.project_config.doxy_config_file).parent + + +# not valid path exception +class DoxygenBinPathNotValid(exceptions.PluginError): + pass + + +class DoxygenCustomConfigNotFound(exceptions.PluginError): + pass + + +class DoxygenCustomConfigNotValid(exceptions.PluginError): + pass diff --git a/mkdoxy/doxyrun.py b/mkdoxy/doxyrun.py deleted file mode 100644 index ffd1473a..00000000 --- a/mkdoxy/doxyrun.py +++ /dev/null @@ -1,275 +0,0 @@ -import hashlib -import logging -import os -import shutil -import re - -from pathlib import Path, PurePath -from subprocess import PIPE, Popen -from typing import Optional - -log: logging.Logger = logging.getLogger("mkdocs") - - -class DoxygenRun: - """! Class for running Doxygen. - @details This class is used to run Doxygen and parse the XML output. - """ - - def __init__( - self, - doxygenBinPath: str, - doxygenSource: str, - tempDoxyFolder: str, - doxyCfgNew, - doxyConfigFile: Optional[str] = None, - ): - """! Constructor. - Default Doxygen config options: - - - INPUT: - - OUTPUT_DIRECTORY: - - DOXYFILE_ENCODING: UTF-8 - - GENERATE_XML: YES - - RECURSIVE: YES - - EXAMPLE_PATH: examples - - SHOW_NAMESPACES: YES - - GENERATE_HTML: NO - - GENERATE_LATEX: NO - - @details - @param doxygenBinPath: (str) Path to the Doxygen binary. - @param doxygenSource: (str) Source files for Doxygen. - @param tempDoxyFolder: (str) Temporary folder for Doxygen. - @param doxyConfigFile: (str) Path to a Doxygen config file. - @param doxyCfgNew: (dict) New Doxygen config options that will be added to the default config (new options will overwrite default options) - """ # noqa: E501 - - if not self.is_doxygen_valid_path(doxygenBinPath): - raise DoxygenBinPathNotValid( - f"Invalid Doxygen binary path: {doxygenBinPath}\n" - f"Make sure Doxygen is installed and the path is correct.\n" - f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/#configure-custom-doxygen-binary." - ) - - self.doxygenBinPath: str = doxygenBinPath - self.doxygenSource: str = doxygenSource - self.tempDoxyFolder: str = tempDoxyFolder - self.doxyConfigFile: Optional[str] = doxyConfigFile - self.hashFileName: str = "hashChanges.yaml" - self.hashFilePath: PurePath = PurePath.joinpath(Path(self.tempDoxyFolder), Path(self.hashFileName)) - self.doxyCfg: dict = self.setDoxyCfg(doxyCfgNew) - - def setDoxyCfg(self, doxyCfgNew: dict) -> dict: - """! Set the Doxygen configuration. - @details If a custom Doxygen config file is provided, it will be used. Otherwise, default options will be used. - @details Order of application of parameters: - @details 1. Custom Doxygen config file - @details 2. If not provided, default options - in documentation - @details 3. New Doxygen config options from mkdocs.yml - @details 3. Overwrite INPUT and OUTPUT_DIRECTORY with the provided values for correct plugin operation. - - @details Overwrite options description: - @details - INPUT: - @details - OUTPUT_DIRECTORY: - - @details Default Doxygen config options: - @details - DOXYFILE_ENCODING: UTF-8 - @details - GENERATE_XML: YES - @details - RECURSIVE: YES - @details - EXAMPLE_PATH: examples - @details - SHOW_NAMESPACES: YES - @details - GENERATE_HTML: NO - @details - GENERATE_LATEX: NO - @param doxyCfgNew: (dict) New Doxygen config options that will be - added to the default config (new options will overwrite default options) - @return: (dict) Doxygen configuration. - """ - doxyCfg = {} - - if self.doxyConfigFile is not None and self.doxyConfigFile != "": - try: - with open(self.doxyConfigFile, "r") as file: - doxyCfg.update(self.str2dox_dict(file.read())) - except FileNotFoundError as e: - raise DoxygenCustomConfigNotFound( - f"Custom Doxygen config file not found: {self.doxyConfigFile}\n" - f"Make sure the path is correct." - f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/#configure-custom-doxygen-configuration-file." - ) from e - else: - doxyCfg = { - "DOXYFILE_ENCODING": "UTF-8", - "GENERATE_XML": "YES", - "RECURSIVE": "YES", - "EXAMPLE_PATH": "examples", - "SHOW_NAMESPACES": "YES", - "GENERATE_HTML": "NO", - "GENERATE_LATEX": "NO", - } - - doxyCfg.update(doxyCfgNew) - doxyCfg["INPUT"] = self.doxygenSource - doxyCfg["OUTPUT_DIRECTORY"] = self.tempDoxyFolder - return doxyCfg - - def is_doxygen_valid_path(self, doxygen_bin_path: str) -> bool: - """! Check if the Doxygen binary path is valid. - @details Accepts a full path or just 'doxygen' if it exists in the system's PATH. - @param doxygen_bin_path: (str) The path to the Doxygen binary or just 'doxygen'. - @return: (bool) True if the Doxygen binary path is valid, False otherwise. - """ - # If the path is just 'doxygen', search for it in the system's PATH - if doxygen_bin_path.lower() == "doxygen": - return shutil.which("doxygen") is not None - - # Use pathlib to check if the provided full path is a file and executable - path = Path(doxygen_bin_path) - return path.is_file() and os.access(path, os.X_OK) - - # Source of dox_dict2str: https://xdress-fabio.readthedocs.io/en/latest/_modules/xdress/doxygen.html#XDressPlugin - def dox_dict2str(self, dox_dict: dict) -> str: - """! Convert a dictionary to a string that can be written to a doxygen config file. - @details - @param dox_dict: (dict) Dictionary to convert. - @return: (str) String that can be written to a doxygen config file. - """ - s = "" - new_line = "{option} = {value}\n" - for key, value in dox_dict.items(): - if value is True: - _value = "YES" - elif value is False: - _value = "NO" - else: - _value = value - - s += new_line.format(option=key.upper(), value=_value) - - # Don't need an empty line at the end - return s.strip() - - def str2dox_dict(self, dox_str: str) -> dict: - """! Convert a string from a doxygen config file to a dictionary. - @details - @param dox_str: (str) String from a doxygen config file. - @return: (dict) Dictionary. - """ - dox_dict = {} - dox_str = re.sub(r"\\\s*\n\s*", "", dox_str) - pattern = r"^\s*([^=\s]+)\s*(=|\+=)\s*(.*)$" - - try: - for line in dox_str.split("\n"): - if line.strip() == "" or line.startswith("#"): - continue - match = re.match(pattern, line) - if not match: - raise DoxygenCustomConfigNotValid( - f"Invalid line: '{line}'" - f"In custom Doxygen config file: {self.doxyConfigFile}\n" - f"Make sure the file is in standard Doxygen format." - f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." - ) - key, operator, value = match.groups() - value = value.strip() - if operator == "=": - if value == "YES": - dox_dict[key] = True - elif value == "NO": - dox_dict[key] = False - else: - dox_dict[key] = value - if operator == "+=": - dox_dict[key] = f"{dox_dict[key]} {value}" - except ValueError as e: - raise DoxygenCustomConfigNotValid( - f"Invalid custom Doxygen config file: {self.doxyConfigFile}\n" - f"Make sure the file is in standard Doxygen format." - f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." - ) from e - return dox_dict - - def hasChanged(self) -> bool: - """! Check if the source files have changed since the last run. - @details - @return: (bool) True if the source files have changed since the last run. - """ - - def hashWrite(filename: PurePath, hash: str): - with open(filename, "w") as file: - file.write(hash) - - def hashRead(filename: PurePath) -> str: - with open(filename, "r") as file: - return str(file.read()) - - sha1 = hashlib.sha1() - srcs = self.doxygenSource.split(" ") - for src in srcs: - for path in Path(src).rglob("*.*"): - # # Code from https://stackoverflow.com/a/22058673/15411117 - # # BUF_SIZE is totally arbitrary, change for your app! - BUF_SIZE = 65536 # let's read stuff in 64kb chunks! - if path.is_file(): - with open(path, "rb") as f: - while True: - data = f.read(BUF_SIZE) - if not data: - break - sha1.update(data) - # print(f"{path}: {sha1.hexdigest()}") - - hashNew = sha1.hexdigest() - if Path(self.hashFilePath).is_file(): - hashOld = hashRead(self.hashFilePath) - if hashNew == hashOld: - return False - - hashWrite(self.hashFilePath, hashNew) - return True - - def run(self): - """! Run Doxygen with the current configuration using the Popen class. - @details - """ - doxyBuilder = Popen( - [self.doxygenBinPath, "-"], - stdout=PIPE, - stdin=PIPE, - stderr=PIPE, - ) - (doxyBuilder.communicate(self.dox_dict2str(self.doxyCfg).encode("utf-8"))[0].decode().strip()) - # log.info(self.destinationDir) - # log.info(stdout_data) - - def checkAndRun(self): - """! Check if the source files have changed since the last run and run Doxygen if they have. - @details - @return: (bool) True if Doxygen was run. - """ - if self.hasChanged(): - self.run() - return True - else: - return False - - def getOutputFolder(self) -> PurePath: - """! Get the path to the XML output folder. - @details - @return: (PurePath) Path to the XML output folder. - """ - return Path.joinpath(Path(self.tempDoxyFolder), Path("xml")) - - -# not valid path exception -class DoxygenBinPathNotValid(Exception): - pass - - -class DoxygenCustomConfigNotFound(Exception): - pass - - -class DoxygenCustomConfigNotValid(Exception): - pass diff --git a/mkdoxy/generatorBase.py b/mkdoxy/generatorBase.py index 6e2f6905..1295154a 100644 --- a/mkdoxy/generatorBase.py +++ b/mkdoxy/generatorBase.py @@ -8,7 +8,7 @@ from mkdocs import exceptions import mkdoxy -from mkdoxy.constants import Kind +from mkdoxy.constants import Kind, JINJA_EXTENSIONS from mkdoxy.filters import use_code_language from mkdoxy.node import DummyNode, Node from mkdoxy.utils import ( @@ -43,12 +43,11 @@ def __init__(self, templateDir: str = "", ignore_errors: bool = False, debug: bo environment.filters["use_code_language"] = use_code_language # code from https://github.com/daizutabi/mkapi/blob/master/mkapi/core/renderer.py#L29-L38 path = os.path.join(os.path.dirname(mkdoxy.__file__), "templates") - ENDING = (".jinja2", ".j2", ".jinja") for fileName in os.listdir(path): filePath = os.path.join(path, fileName) # accept any case of the file ending - if fileName.lower().endswith(ENDING): + if fileName.lower().endswith(JINJA_EXTENSIONS): with open(filePath, "r") as file: name = os.path.splitext(fileName)[0] fileTemplate, metaData = parseTemplateFile(file.read()) @@ -56,7 +55,7 @@ def __init__(self, templateDir: str = "", ignore_errors: bool = False, debug: bo self.metaData[name] = metaData else: log.error( - f"Trying to load unsupported file '{filePath}'. Supported file ends with {ENDING}." + f"Trying to load unsupported file '{filePath}'. Supported file ends with {JINJA_EXTENSIONS}." f"Look at documentation: https://mkdoxy.kubaandrysek.cz/usage/#custom-jinja-templates." ) @@ -67,7 +66,7 @@ def __init__(self, templateDir: str = "", ignore_errors: bool = False, debug: bo # load custom templates and overwrite default templates - if they exist for fileName in os.listdir(templateDir): filePath = os.path.join(templateDir, fileName) - if fileName.lower().endswith(ENDING): + if fileName.lower().endswith(JINJA_EXTENSIONS): with open(filePath, "r") as file: name = os.path.splitext(fileName)[0] fileTemplate, metaData = parseTemplateFile(file.read()) @@ -76,7 +75,7 @@ def __init__(self, templateDir: str = "", ignore_errors: bool = False, debug: bo log.info(f"Overwriting template '{name}' with custom template.") else: log.error( - f"Trying to load unsupported file '{filePath}'. Supported file ends with {ENDING}." + f"Trying to load unsupported file '{filePath}'. Supported file ends with {JINJA_EXTENSIONS}." f"Look at documentation: https://mkdoxy.kubaandrysek.cz/usage/#custom-jinja-templates." ) diff --git a/mkdoxy/markdown.py b/mkdoxy/markdown.py index 2740f250..7305d10b 100644 --- a/mkdoxy/markdown.py +++ b/mkdoxy/markdown.py @@ -6,7 +6,7 @@ def escape(s: str) -> str: ret = ret.replace("_", "\\_") ret = ret.replace("<", "<") ret = ret.replace(">", ">") - return ret.replace("|", "\|") + return ret.replace("|", "\\|") class MdRenderer: diff --git a/mkdoxy/migration.py b/mkdoxy/migration.py new file mode 100644 index 00000000..d09f17a2 --- /dev/null +++ b/mkdoxy/migration.py @@ -0,0 +1,56 @@ +import logging +import re +import shutil +from pathlib import Path + +log = logging.getLogger("mkdoxy.migration") + + +def update_new_config(yaml_file: Path, backup: bool, backup_file_name: str) -> None: + """ + Migrate MkDoxy configuration to the new version by replacing legacy keys + directly in the text file—preserving comments and structure. + + Legacy keys are replaced only on non-comment lines. + + :param yaml_file: Path to the mkdocs YAML configuration file. + :param backup: If True, a backup of the original file is created. + :param backup_file_name: The filename to use for the backup. + """ + if backup: + backup_path = yaml_file.parent / backup_file_name + shutil.copy2(yaml_file, backup_path) + log.info(f"Backup created at {backup_path}") + + text = yaml_file.read_text(encoding="utf-8") + + # Merge global and project legacy mappings. + legacy_mapping = {} + legacy_mapping.update( + { + "full-doc": "full_doc", + "ignore-errors": "ignore_errors", + "save-api": "custom_api_folder", + "doxygen-bin-path": "doxygen_bin_path", + } + ) + legacy_mapping.update( + { + "src-dirs": "src_dirs", + "full-doc": "full_doc", + "ignore-errors": "ignore_errors", + "doxy-cfg": "doxy_config_dict", + "doxy-cfg-file": "doxy_config_file", + "template-dir": "custom_template_dir", + } + ) + + # Replace each legacy key only on lines that are not comments. + for old_key, new_key in legacy_mapping.items(): + # Pattern matches lines that do not start with a comment (after optional whitespace), + # then the legacy key followed by optional spaces and a colon. + pattern = re.compile(rf"(?m)^(?!\s*#)(\s*){re.escape(old_key)}(\s*:)", re.UNICODE) + text = pattern.sub(rf"\1{new_key}\2", text) + + yaml_file.write_text(text, encoding="utf-8") + log.info("Migration completed successfully") diff --git a/mkdoxy/plugin.py b/mkdoxy/plugin.py index 20063227..bd55da65 100644 --- a/mkdoxy/plugin.py +++ b/mkdoxy/plugin.py @@ -5,53 +5,36 @@ """ import logging -from pathlib import Path, PurePath +from pathlib import Path -from mkdocs import exceptions -from mkdocs.config import Config, base, config_options +from mkdocs.config import Config from mkdocs.plugins import BasePlugin -from mkdocs.structure import files, pages +from mkdocs.structure.files import Files +from mkdocs.structure.pages import Page from mkdoxy.cache import Cache from mkdoxy.doxygen import Doxygen -from mkdoxy.doxyrun import DoxygenRun +from mkdoxy.doxygen_generator import DoxygenGenerator from mkdoxy.generatorAuto import GeneratorAuto from mkdoxy.generatorBase import GeneratorBase from mkdoxy.generatorSnippets import GeneratorSnippets from mkdoxy.xml_parser import XmlParser +from mkdoxy.doxy_config import MkDoxyConfig log: logging.Logger = logging.getLogger("mkdocs") -pluginName: str = "MkDoxy" +plugin_name: str = "MkDoxy" -class MkDoxy(BasePlugin): +class MkDoxy(BasePlugin[MkDoxyConfig]): """! MkDocs plugin for generating documentation from Doxygen XML files.""" - # Config options for the plugin - config_scheme = ( - ("projects", config_options.Type(dict, default={})), - ("full-doc", config_options.Type(bool, default=True)), - ("debug", config_options.Type(bool, default=False)), - ("ignore-errors", config_options.Type(bool, default=False)), - ("save-api", config_options.Type(str, default="")), - ("enabled", config_options.Type(bool, default=True)), - ( - "doxygen-bin-path", - config_options.Type(str, default="doxygen", required=False), - ), - ) - - # Config options for each project - config_project = ( - ("src-dirs", config_options.Type(str)), - ("full-doc", config_options.Type(bool, default=True)), - ("debug", config_options.Type(bool, default=False)), - # ('ignore-errors', config_options.Type(bool, default=False)), - ("api-path", config_options.Type(str, default=".")), - ("doxy-cfg", config_options.Type(dict, default={}, required=False)), - ("doxy-cfg-file", config_options.Type(str, default="", required=False)), - ("template-dir", config_options.Type(str, default="", required=False)), - ) + def __init__(self): + self.generator_base: dict[str, GeneratorBase] = {} + self.doxygen: dict[str, Doxygen] = {} + self.default_template_config = { + "indent_level": 0, + } + # check deprecated config here def is_enabled(self) -> bool: """! Checks if the plugin is enabled @@ -60,145 +43,107 @@ def is_enabled(self) -> bool: """ return self.config.get("enabled") - def on_files(self, files: files.Files, config: base.Config) -> files.Files: + def on_files(self, files: Files, config: Config) -> Files: """! Called after files have been gathered by MkDocs. - @details + @details generate automatic documentation and append files in the list of files to be processed by mkdocs @param files: (Files) The files gathered by MkDocs. @param config: (Config) The global configuration object. @return: (Files) The files gathered by MkDocs. """ - if not self.is_enabled(): - return files - - def checkConfig(config_project, proData, strict: bool): - cfg = Config(config_project, "") - cfg.load_dict(proData) - errors, warnings = cfg.validate() - for config_name, warning in warnings: - log.warning(f" -> Config value: '{config_name}' in project '{project_name}'. Warning: {warning}") - for config_name, error in errors: - log.error(f" -> Config value: '{config_name}' in project '{project_name}'. Error: {error}") - - if len(errors) > 0: - raise exceptions.Abort(f"Aborted with {len(errors)} Configuration Errors!") - elif strict and len(warnings) > 0: - raise exceptions.Abort(f"Aborted with {len(warnings)} Configuration Warnings in 'strict' mode!") - - def tempDir(siteDir: str, tempDir: str, projectName: str) -> str: - tempDoxyDir = PurePath.joinpath(Path(siteDir), Path(tempDir), Path(projectName)) - tempDoxyDir.mkdir(parents=True, exist_ok=True) - return str(tempDoxyDir) - - self.doxygen = {} - self.generatorBase = {} - self.projects_config: dict[str, dict[str, any]] = self.config["projects"] - self.debug = self.config.get("debug", False) - - # generate automatic documentation and append files in the list of files to be processed by mkdocs - self.defaultTemplateConfig: dict = { - "indent_level": 0, - } - - log.info(f"Start plugin {pluginName}") - - for project_name, project_data in self.projects_config.items(): - log.info(f"-> Start project '{project_name}'") + for project_name, project_config in self.config.projects.items(): + log.info(f"-> Processing project '{project_name}'") - # Check project config -> raise exceptions - checkConfig(self.config_project, project_data, config["strict"]) - - if self.config.get("save-api"): - tempDirApi = tempDir("", self.config.get("save-api"), project_name) + # Generate Doxygen and MD files to user defined folder or default temp folder + if self.config.custom_api_folder: + temp_doxy_folder = Path.joinpath(Path(self.config.custom_api_folder), Path(project_name)) else: - tempDirApi = tempDir(config["site_dir"], "assets/.doxy/", project_name) + temp_doxy_folder = Path.joinpath(Path(config["site_dir"]), Path("assets/.doxy"), Path(project_name)) + + # Create temp dir for Doxygen if not exists + temp_doxy_folder.mkdir(parents=True, exist_ok=True) # Check src changes -> run Doxygen - doxygenRun = DoxygenRun( - self.config["doxygen-bin-path"], - project_data.get("src-dirs"), - tempDirApi, - project_data.get("doxy-cfg", {}), - project_data.get("doxy-cfg-file", ""), + doxygen = DoxygenGenerator( + self.config, + project_config, + temp_doxy_folder, ) - if doxygenRun.checkAndRun(): - log.info(" -> generating Doxygen files") + if doxygen.has_changes(): + log.info(" -> Generating Doxygen files started") + doxygen.run() + log.info(" -> Doxygen files generated") else: - log.info(" -> skip generating Doxygen files (nothing changes)") + log.info(" -> skip generating Doxygen files (nothing seems to have changed)") # Parse XML to basic structure cache = Cache() - parser = XmlParser(cache=cache, debug=self.debug) + parser = XmlParser(cache=cache, debug=self.config.debug) # Parse basic structure to recursive Nodes - self.doxygen[project_name] = Doxygen(doxygenRun.getOutputFolder(), parser=parser, cache=cache) + # TODO: Doxygen index_path should be Path object + self.doxygen[project_name] = Doxygen(str(doxygen.get_output_xml_folder()), parser=parser, cache=cache) # Print parsed files - if self.debug: + if self.config.debug: self.doxygen[project_name].printStructure() # Prepare generator for future use (GeneratorAuto, SnippetGenerator) - self.generatorBase[project_name] = GeneratorBase( - project_data.get("template-dir", ""), - ignore_errors=self.config["ignore-errors"], - debug=self.debug, + self.generator_base[project_name] = GeneratorBase( + project_config.custom_template_dir, + False, # ignore_errors=self.config.ignore_errors, + debug=self.config.debug, ) - if self.config["full-doc"] and project_data.get("full-doc", True): + if self.config.full_doc and project_config.full_doc: generatorAuto = GeneratorAuto( - generatorBase=self.generatorBase[project_name], - tempDoxyDir=tempDirApi, + generatorBase=self.generator_base[project_name], + tempDoxyDir=str(temp_doxy_folder), siteDir=config["site_dir"], - apiPath=project_data.get("api-path", project_name), + apiPath=project_name, doxygen=self.doxygen[project_name], useDirectoryUrls=config["use_directory_urls"], ) - project_config = self.defaultTemplateConfig.copy() - project_config.update(project_data) - generatorAuto.fullDoc(project_config) + template_config = self.default_template_config.copy() + + # Generate full documentation + generatorAuto.fullDoc(template_config) - generatorAuto.summary(project_config) + # Generate summary pages + generatorAuto.summary(template_config) + # Append files to be processed by MkDocs for file in generatorAuto.fullDocFiles: files.append(file) return files - def on_page_markdown( - self, - markdown: str, - page: pages.Page, - config: base.Config, - files: files.Files, - ) -> str: + def on_page_markdown(self, markdown: str, page: Page, config: Config, files: Files) -> str: """! Generate snippets and append them to the markdown. @details - - @param markdown (str): The markdown. - @param page (Page): The MkDocs page. - @param config (Config): The MkDocs config. - @param files (Files): The MkDocs files. - @return: (str) The markdown. + @param markdown: (str) The markdown content of the page. + @param page: (Page) The page object. + @param config: (Config) The global configuration object. + @param files: (Files) The files gathered by MkDocs. + @return: (str) The markdown content of the page. """ - if not self.is_enabled(): - return markdown - # update default template config with page meta - page_config = self.defaultTemplateConfig.copy() + # update default template config with page meta tags + page_config = self.default_template_config.copy() page_config.update(page.meta) - generatorSnippets = GeneratorSnippets( + generator_snippets = GeneratorSnippets( markdown=markdown, - generatorBase=self.generatorBase, + generatorBase=self.generator_base, doxygen=self.doxygen, - projects=self.projects_config, + projects=self.config.projects, useDirectoryUrls=config["use_directory_urls"], page=page, config=page_config, - debug=self.debug, + debug=self.config.debug, ) - return generatorSnippets.generate() + return generator_snippets.generate() # def on_serve(self, server): diff --git a/setup.py b/setup.py index 123418f3..2fa459e9 100755 --- a/setup.py +++ b/setup.py @@ -59,5 +59,10 @@ def import_dev_requirements(): ], packages=find_packages(), package_data={"mkdoxy": ["templates/*.jinja2"]}, - entry_points={"mkdocs.plugins": ["mkdoxy = mkdoxy.plugin:MkDoxy"]}, + entry_points={ + "mkdocs.plugins": ["mkdoxy = mkdoxy.plugin:MkDoxy"], + # folder mkdoxy/cli.py + # "console_scripts": ["mkdoxy = mkdoxy.cli:cli"], + "console_scripts": ["mkdoxy = mkdoxy.__main__:main"], + }, ) diff --git a/tests/data/Doxyfile b/tests/config/data/Doxyfile similarity index 100% rename from tests/data/Doxyfile rename to tests/config/data/Doxyfile diff --git a/tests/test_doxyrun.py b/tests/config/test_doxyrun.py similarity index 61% rename from tests/test_doxyrun.py rename to tests/config/test_doxyrun.py index f5470f7d..953d7ea8 100644 --- a/tests/test_doxyrun.py +++ b/tests/config/test_doxyrun.py @@ -1,64 +1,60 @@ import pytest -from mkdoxy.doxyrun import DoxygenCustomConfigNotValid, DoxygenRun + +from pathlib import Path +from mkdoxy.doxy_config import MkDoxyConfig, MkDoxyConfigProject +from mkdoxy.doxygen_generator import DoxygenGenerator, DoxygenCustomConfigNotValid def test_dox_dict2str(): dox_dict = { "DOXYFILE_ENCODING": "UTF-8", - "GENERATE_XML": True, - "RECURSIVE": True, "EXAMPLE_PATH": "examples", - "SHOW_NAMESPACES": True, "GENERATE_HTML": False, "GENERATE_LATEX": False, + "GENERATE_XML": True, + "RECURSIVE": True, + "SHOW_NAMESPACES": True, } - doxygen_run = DoxygenRun( - doxygenBinPath="doxygen", - doxygenSource="/path/to/source/files", - tempDoxyFolder="/path/to/temp/folder", - doxyCfgNew=dox_dict, - ) - - result = doxygen_run.dox_dict2str(dox_dict) - expected_result = ( - "DOXYFILE_ENCODING = UTF-8\nGENERATE_XML = YES" - "\nRECURSIVE = YES\nEXAMPLE_PATH = examples" - "\nSHOW_NAMESPACES = YES\nGENERATE_HTML = NO" - "\nGENERATE_LATEX = NO" + "DOXYFILE_ENCODING = UTF-8\n" + "EXAMPLE_PATH = examples\n" + "GENERATE_HTML = NO\n" + "GENERATE_LATEX = NO\n" + "GENERATE_XML = YES\n" + "RECURSIVE = YES\n" + "SHOW_NAMESPACES = YES" ) - assert result == expected_result + assert DoxygenGenerator.dox_dict2str(dox_dict) == expected_result # Sets the Doxygen configuration using a custom config file def test_set_doxy_cfg_custom_file(): - dox_dict = {} - - doxygen_run = DoxygenRun( - doxygenBinPath="doxygen", - doxygenSource="/path/to/source/files", - tempDoxyFolder="/path/to/temp/folder", - doxyConfigFile="./tests/data/Doxyfile", - doxyCfgNew=dox_dict, + project = MkDoxyConfigProject() + project.src_dirs = "/path/to/source/files" + + doxygen_run = DoxygenGenerator( + doxy_config=MkDoxyConfig(), + project_config=project, + temp_doxy_folder=Path("/path/to/temp/folder"), ) - result = doxygen_run.setDoxyCfg(dox_dict) + dox_dict = doxygen_run.get_merged_doxy_dict() expected_result = { "DOXYFILE_ENCODING": "UTF-8", - "GENERATE_XML": True, - "RECURSIVE": True, "EXAMPLE_PATH": "examples", - "SHOW_NAMESPACES": True, "GENERATE_HTML": False, "GENERATE_LATEX": False, + "GENERATE_XML": True, "INPUT": "/path/to/source/files", "OUTPUT_DIRECTORY": "/path/to/temp/folder", + "RECURSIVE": True, + "SHOW_NAMESPACES": True, } - assert result == expected_result + assert expected_result == dox_dict def test_str2dox_dict(): @@ -68,14 +64,7 @@ def test_str2dox_dict(): "SHOW_NAMESPACES = YES\nGENERATE_HTML = NO\nGENERATE_LATEX = NO" ) - doxygen_run = DoxygenRun( - doxygenBinPath="doxygen", - doxygenSource="/path/to/source/files", - tempDoxyFolder="/path/to/temp/folder", - doxyCfgNew={}, - ) - - result = doxygen_run.str2dox_dict(dox_str) + result = DoxygenGenerator.str2dox_dict(dox_str) expected_result = { "DOXYFILE_ENCODING": "UTF-8", @@ -101,15 +90,6 @@ def test_str2dox_dict_expanded_config(): "PREDEFINED = BUILD_DATE DOXYGEN=1\n" ) - doxygen_run = DoxygenRun( - doxygenBinPath="doxygen", - doxygenSource="/path/to/source/files", - tempDoxyFolder="/path/to/temp/folder", - doxyCfgNew={}, - ) - - result = doxygen_run.str2dox_dict(dox_str) - expected_result = { "PROJECT_LOGO": "", "ABBREVIATE_BRIEF": '"The $name class" is', @@ -117,68 +97,61 @@ def test_str2dox_dict_expanded_config(): "PREDEFINED": "BUILD_DATE DOXYGEN=1", } - assert result == expected_result + assert expected_result == DoxygenGenerator.str2dox_dict(dox_str) def test_str2dox_dict_expanded_config_errors(): - doxygen_run = DoxygenRun( - doxygenBinPath="doxygen", - doxygenSource="/path/to/source/files", - tempDoxyFolder="/path/to/temp/folder", - doxyCfgNew={}, - ) - dox_str = "ONLY_KEY\n" - error_message = str( + error_message = ( "Invalid line: 'ONLY_KEY'" - "In custom Doxygen config file: None\n" + "In custom Doxygen config file: QuestionMark\n" "Make sure the file is in standard Doxygen format." "Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." ) with pytest.raises(DoxygenCustomConfigNotValid, match=error_message): - doxygen_run.str2dox_dict(dox_str) + DoxygenGenerator.str2dox_dict(dox_str, "QuestionMark") dox_str = "= ONLY_VALUE\n" - error_message = str( + error_message = ( "Invalid line: '= ONLY_VALUE'" - "In custom Doxygen config file: None\n" + "In custom Doxygen config file: QuestionMark\n" "Make sure the file is in standard Doxygen format." "Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." ) with pytest.raises(DoxygenCustomConfigNotValid, match=error_message): - doxygen_run.str2dox_dict(dox_str) + DoxygenGenerator.str2dox_dict(dox_str, "QuestionMark") dox_str = "KEY WITH SPACES = VALUE\n" - error_message = str( + error_message = ( "Invalid line: 'KEY WITH SPACES = VALUE'" - "In custom Doxygen config file: None\n" + "In custom Doxygen config file: QuestionMark\n" "Make sure the file is in standard Doxygen format." "Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." ) with pytest.raises(DoxygenCustomConfigNotValid, match=error_message): - doxygen_run.str2dox_dict(dox_str) + DoxygenGenerator.str2dox_dict(dox_str, "QuestionMark") dox_str = "BAD_OPERATOR := VALUE\n" - error_message = str( + error_message = ( "Invalid line: 'BAD_OPERATOR := VALUE'" - "In custom Doxygen config file: None\n" + "In custom Doxygen config file: QuestionMark\n" "Make sure the file is in standard Doxygen format." "Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." ) with pytest.raises(DoxygenCustomConfigNotValid, match=error_message): - doxygen_run.str2dox_dict(dox_str) + DoxygenGenerator.str2dox_dict(dox_str, "QuestionMark") dox_str = "BAD_MULTILINE = BAD\n VALUE\n" - error_message = str( + error_message = ( "Invalid line: ' VALUE'" - "In custom Doxygen config file: None\n" + "In custom Doxygen config file: QuestionMark\n" "Make sure the file is in standard Doxygen format." "Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." ) with pytest.raises(DoxygenCustomConfigNotValid, match=error_message): - doxygen_run.str2dox_dict(dox_str) + DoxygenGenerator.str2dox_dict(dox_str, "QuestionMark") diff --git a/tests/config/test_doxyrun_helpers.py b/tests/config/test_doxyrun_helpers.py new file mode 100644 index 00000000..ee1e94b2 --- /dev/null +++ b/tests/config/test_doxyrun_helpers.py @@ -0,0 +1,51 @@ +from mkdoxy.doxygen_generator import DoxygenGenerator + + +def test_merge_doxygen_input_empty_src_dirs(tmp_path): + # When src_dirs is empty, the function should return doxy_input unchanged. + src_dirs = "" + doxy_input = "dir1 dir2" + run_folder = tmp_path / "run" + run_folder.mkdir() + + result = DoxygenGenerator.merge_doxygen_input(src_dirs, doxy_input, run_folder) + assert result == doxy_input + + +def test_merge_doxygen_input_empty_doxy_input(tmp_path): + # When doxy_input is empty, the function should return src_dirs unchanged. + src_dirs = "dir1 dir2" + doxy_input = "" + run_folder = tmp_path / "run" + run_folder.mkdir() + + result = DoxygenGenerator.merge_doxygen_input(src_dirs, doxy_input, run_folder) + assert result == src_dirs + + +def test_merge_doxygen_input_both_non_empty(tmp_path): + # When both src_dirs and doxy_input are provided, the result should contain all unique paths. + src_dirs = "a b" + doxy_input = "b c" + run_folder = tmp_path / "run" + run_folder.mkdir() + + result = DoxygenGenerator.merge_doxygen_input(src_dirs, doxy_input, run_folder) + # The returned string is a space-separated list of paths relative to run_folder. + # We compare as sets because the order may not be predictable. + result_set = set(result.split()) + expected_set = {"a", "b", "c"} + assert result_set == expected_set + + +def test_merge_doxygen_input_duplicates(tmp_path): + # Duplicate paths in the inputs should be deduplicated. + src_dirs = "a a" + doxy_input = "a" + run_folder = tmp_path / "run" + run_folder.mkdir() + + result = DoxygenGenerator.merge_doxygen_input(src_dirs, doxy_input, run_folder) + result_list = result.split() + # Expect only one occurrence of "a" + assert result_list == ["a"] diff --git a/tests/migration/data/1_expect.yaml b/tests/migration/data/1_expect.yaml new file mode 100644 index 00000000..7015437d --- /dev/null +++ b/tests/migration/data/1_expect.yaml @@ -0,0 +1,215 @@ +site_name: MkDoxy Demo +site_url: https://mkdoxy-demo.kubaandrysek.cz/ +site_author: Jakub Andrýsek +site_description: >- + This is a demo of MkDoxy, a tool for generating Doxygen documentation from Markdown files. + +# Repository +repo_name: JakubAndrysek/MkDoxy-demo +repo_url: https://github.com/JakubAndrysek/MkDoxy-demo + +# Copyright +copyright: Copyright © 2023 Jakub Andrýsek + +theme: + name: material + language: en + logo: assets/logo.png + favicon: assets/logo.png + features: + - navigation.tabs + - navigation.indexes + - navigation.top + - navigation.instant + - navigation.tracking + + icon: + repo: fontawesome/brands/github + + palette: + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: amber + accent: amber + toggle: + icon: material/brightness-4 + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: amber + accent: amber + toggle: + icon: material/brightness-7 + name: Switch to dark mode + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/JakubAndrysek + - icon: fontawesome/brands/twitter + link: https://twitter.com/KubaAndrysek + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/jakub-andrysek/ + analytics: + provider: google + property: G-6VB0GPP3MT + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: >- + Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: >- + Thanks for your feedback! + +use_directory_urls: True +#use_directory_urls: False + +plugins: + - search + - glightbox + - open-in-new-tab + - mkdoxy: + projects: + esp: + src_dirs: demo-projets/esp + full_doc: True + custom_template_dir: templates-custom + # Example of custom template: https://mkdoxy-demo.kubaandrysek.cz/esp/annotated/ + doxy_config_dict: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + animal: + src_dirs: demo-projets/animal + full_doc: True + doxy_config_dict: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + stm: + src_dirs: demo-projets/stm32 + full_doc: True + + jaculus: + src_dirs: demo-projets/jaculus/main demo-projets/jaculus/managed_components/jac-link demo-projets/jaculus/managed_components/jac-machine + full_doc: True + doxy_config_dict: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + + custom_api_folder: .mkdoxy + full_doc: True + debug: False + ignore_errors: False + +markdown_extensions: + - pymdownx.highlight + - pymdownx.superfences + +nav: + - "Home": "README.md" + - useage.md + - API Demo: + - api/index.md + - ESP-32: + - esp/index.md + - "Links": "esp/links.md" + - "Classes": + - "Class List": "esp/annotated.md" + - "Class Index": "esp/classes.md" + - "Class Hierarchy": "esp/hierarchy.md" + - "Class Members": "esp/class_members.md" + - "Class Member Functions": "esp/class_member_functions.md" + - "Class Member Variables": "esp/class_member_variables.md" + - "Class Member Typedefs": "esp/class_member_typedefs.md" + - "Class Member Enumerations": "esp/class_member_enums.md" + - "Namespaces": + - "Namespace List": "esp/namespaces.md" + - "Namespace Members": "esp/namespace_members.md" + - "Namespace Member Functions": "esp/namespace_member_functions.md" + - "Namespace Member Variables": "esp/namespace_member_variables.md" + - "Namespace Member Typedefs": "esp/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "esp/namespace_member_enums.md" + - "Functions": "esp/functions.md" + - "Variables": "esp/variables.md" + - "Macros": "esp/macros.md" + - "Files": "esp/files.md" + - STM-32: + - stm32/index.md + - "Links": "stm/links.md" + - "Classes": + - "Class List": "stm/annotated.md" + - "Class Index": "stm/classes.md" + - "Class Hierarchy": "stm/hierarchy.md" + - "Class Members": "stm/class_members.md" + - "Class Member Functions": "stm/class_member_functions.md" + - "Class Member Variables": "stm/class_member_variables.md" + - "Class Member Typedefs": "stm/class_member_typedefs.md" + - "Class Member Enumerations": "stm/class_member_enums.md" + - "Namespaces": + - "Namespace List": "stm/namespaces.md" + - "Namespace Members": "stm/namespace_members.md" + - "Namespace Member Functions": "stm/namespace_member_functions.md" + - "Namespace Member Variables": "stm/namespace_member_variables.md" + - "Namespace Member Typedefs": "stm/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "stm/namespace_member_enums.md" + - "Functions": "stm/functions.md" + - "Variables": "stm/variables.md" + - "Macros": "stm/macros.md" + - "Files": "stm/files.md" + - Animal: + - animal/index.md + - "Links": "animal/links.md" + - "Classes": + - "Class List": "animal/annotated.md" + - "Class Index": "animal/classes.md" + - "Class Hierarchy": "animal/hierarchy.md" + - "Class Members": "animal/class_members.md" + - "Class Member Functions": "animal/class_member_functions.md" + - "Class Member Variables": "animal/class_member_variables.md" + - "Class Member Typedefs": "animal/class_member_typedefs.md" + - "Class Member Enumerations": "animal/class_member_enums.md" + - "Namespaces": + - "Namespace List": "animal/namespaces.md" + - "Namespace Members": "animal/namespace_members.md" + - "Namespace Member Functions": "animal/namespace_member_functions.md" + - "Namespace Member Variables": "animal/namespace_member_variables.md" + - "Namespace Member Typedefs": "animal/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "animal/namespace_member_enums.md" + - "Functions": "animal/functions.md" + - "Variables": "animal/variables.md" + - "Macros": "animal/macros.md" + - "Files": "animal/files.md" + - Jaculus: + - jaculus/index.md + - "Links": "jaculus/links.md" + - "Classes": + - "Class List": "jaculus/annotated.md" + - "Class Index": "jaculus/classes.md" + - "Class Hierarchy": "jaculus/hierarchy.md" + - "Class Members": "jaculus/class_members.md" + - "Class Member Functions": "jaculus/class_member_functions.md" + - "Class Member Variables": "jaculus/class_member_variables.md" + - "Class Member Typedefs": "jaculus/class_member_typedefs.md" + - "Class Member Enumerations": "jaculus/class_member_enums.md" + - "Namespaces": + - "Namespace List": "jaculus/namespaces.md" + - "Namespace Members": "jaculus/namespace_members.md" + - "Namespace Member Functions": "jaculus/namespace_member_functions.md" + - "Namespace Member Variables": "jaculus/namespace_member_variables.md" + - "Namespace Member Typedefs": "jaculus/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "jaculus/namespace_member_enums.md" + - "Functions": "jaculus/functions.md" + - "Variables": "jaculus/variables.md" + - "Macros": "jaculus/macros.md" + - "Files": "jaculus/files.md" diff --git a/tests/migration/data/1_old.yaml b/tests/migration/data/1_old.yaml new file mode 100644 index 00000000..84e0dc23 --- /dev/null +++ b/tests/migration/data/1_old.yaml @@ -0,0 +1,215 @@ +site_name: MkDoxy Demo +site_url: https://mkdoxy-demo.kubaandrysek.cz/ +site_author: Jakub Andrýsek +site_description: >- + This is a demo of MkDoxy, a tool for generating Doxygen documentation from Markdown files. + +# Repository +repo_name: JakubAndrysek/MkDoxy-demo +repo_url: https://github.com/JakubAndrysek/MkDoxy-demo + +# Copyright +copyright: Copyright © 2023 Jakub Andrýsek + +theme: + name: material + language: en + logo: assets/logo.png + favicon: assets/logo.png + features: + - navigation.tabs + - navigation.indexes + - navigation.top + - navigation.instant + - navigation.tracking + + icon: + repo: fontawesome/brands/github + + palette: + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: amber + accent: amber + toggle: + icon: material/brightness-4 + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: amber + accent: amber + toggle: + icon: material/brightness-7 + name: Switch to dark mode + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/JakubAndrysek + - icon: fontawesome/brands/twitter + link: https://twitter.com/KubaAndrysek + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/jakub-andrysek/ + analytics: + provider: google + property: G-6VB0GPP3MT + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: >- + Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: >- + Thanks for your feedback! + +use_directory_urls: True +#use_directory_urls: False + +plugins: + - search + - glightbox + - open-in-new-tab + - mkdoxy: + projects: + esp: + src-dirs: demo-projets/esp + full-doc: True + template-dir: templates-custom + # Example of custom template: https://mkdoxy-demo.kubaandrysek.cz/esp/annotated/ + doxy-cfg: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + animal: + src-dirs: demo-projets/animal + full-doc: True + doxy-cfg: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + stm: + src-dirs: demo-projets/stm32 + full-doc: True + + jaculus: + src-dirs: demo-projets/jaculus/main demo-projets/jaculus/managed_components/jac-link demo-projets/jaculus/managed_components/jac-machine + full-doc: True + doxy-cfg: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + + save-api: .mkdoxy + full-doc: True + debug: False + ignore-errors: False + +markdown_extensions: + - pymdownx.highlight + - pymdownx.superfences + +nav: + - "Home": "README.md" + - useage.md + - API Demo: + - api/index.md + - ESP-32: + - esp/index.md + - "Links": "esp/links.md" + - "Classes": + - "Class List": "esp/annotated.md" + - "Class Index": "esp/classes.md" + - "Class Hierarchy": "esp/hierarchy.md" + - "Class Members": "esp/class_members.md" + - "Class Member Functions": "esp/class_member_functions.md" + - "Class Member Variables": "esp/class_member_variables.md" + - "Class Member Typedefs": "esp/class_member_typedefs.md" + - "Class Member Enumerations": "esp/class_member_enums.md" + - "Namespaces": + - "Namespace List": "esp/namespaces.md" + - "Namespace Members": "esp/namespace_members.md" + - "Namespace Member Functions": "esp/namespace_member_functions.md" + - "Namespace Member Variables": "esp/namespace_member_variables.md" + - "Namespace Member Typedefs": "esp/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "esp/namespace_member_enums.md" + - "Functions": "esp/functions.md" + - "Variables": "esp/variables.md" + - "Macros": "esp/macros.md" + - "Files": "esp/files.md" + - STM-32: + - stm32/index.md + - "Links": "stm/links.md" + - "Classes": + - "Class List": "stm/annotated.md" + - "Class Index": "stm/classes.md" + - "Class Hierarchy": "stm/hierarchy.md" + - "Class Members": "stm/class_members.md" + - "Class Member Functions": "stm/class_member_functions.md" + - "Class Member Variables": "stm/class_member_variables.md" + - "Class Member Typedefs": "stm/class_member_typedefs.md" + - "Class Member Enumerations": "stm/class_member_enums.md" + - "Namespaces": + - "Namespace List": "stm/namespaces.md" + - "Namespace Members": "stm/namespace_members.md" + - "Namespace Member Functions": "stm/namespace_member_functions.md" + - "Namespace Member Variables": "stm/namespace_member_variables.md" + - "Namespace Member Typedefs": "stm/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "stm/namespace_member_enums.md" + - "Functions": "stm/functions.md" + - "Variables": "stm/variables.md" + - "Macros": "stm/macros.md" + - "Files": "stm/files.md" + - Animal: + - animal/index.md + - "Links": "animal/links.md" + - "Classes": + - "Class List": "animal/annotated.md" + - "Class Index": "animal/classes.md" + - "Class Hierarchy": "animal/hierarchy.md" + - "Class Members": "animal/class_members.md" + - "Class Member Functions": "animal/class_member_functions.md" + - "Class Member Variables": "animal/class_member_variables.md" + - "Class Member Typedefs": "animal/class_member_typedefs.md" + - "Class Member Enumerations": "animal/class_member_enums.md" + - "Namespaces": + - "Namespace List": "animal/namespaces.md" + - "Namespace Members": "animal/namespace_members.md" + - "Namespace Member Functions": "animal/namespace_member_functions.md" + - "Namespace Member Variables": "animal/namespace_member_variables.md" + - "Namespace Member Typedefs": "animal/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "animal/namespace_member_enums.md" + - "Functions": "animal/functions.md" + - "Variables": "animal/variables.md" + - "Macros": "animal/macros.md" + - "Files": "animal/files.md" + - Jaculus: + - jaculus/index.md + - "Links": "jaculus/links.md" + - "Classes": + - "Class List": "jaculus/annotated.md" + - "Class Index": "jaculus/classes.md" + - "Class Hierarchy": "jaculus/hierarchy.md" + - "Class Members": "jaculus/class_members.md" + - "Class Member Functions": "jaculus/class_member_functions.md" + - "Class Member Variables": "jaculus/class_member_variables.md" + - "Class Member Typedefs": "jaculus/class_member_typedefs.md" + - "Class Member Enumerations": "jaculus/class_member_enums.md" + - "Namespaces": + - "Namespace List": "jaculus/namespaces.md" + - "Namespace Members": "jaculus/namespace_members.md" + - "Namespace Member Functions": "jaculus/namespace_member_functions.md" + - "Namespace Member Variables": "jaculus/namespace_member_variables.md" + - "Namespace Member Typedefs": "jaculus/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "jaculus/namespace_member_enums.md" + - "Functions": "jaculus/functions.md" + - "Variables": "jaculus/variables.md" + - "Macros": "jaculus/macros.md" + - "Files": "jaculus/files.md" diff --git a/tests/migration/data/2_expect.yaml b/tests/migration/data/2_expect.yaml new file mode 100644 index 00000000..f2ac4049 --- /dev/null +++ b/tests/migration/data/2_expect.yaml @@ -0,0 +1,208 @@ +site_name: MkDoxy +site_url: https://mkdoxy.kubaandrysek.cz/ +site_author: Jakub Andrýsek +site_description: >- + MkDoxy -> MkDocs + Doxygen. Easy documentation generator with code snippets. + +# Repository +repo_name: JakubAndrysek/MkDoxy/ +repo_url: https://github.com/JakubAndrysek/MkDoxy/ + +# Copyright +copyright: Copyright © 2023 Jakub Andrýsek + +theme: + name: material + language: en + logo: assets/logo.png + favicon: assets/logo.png + features: + - navigation.tabs + - navigation.indexes + - navigation.top + + icon: + repo: fontawesome/brands/github + + palette: + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: orange + accent: orange + toggle: + icon: material/brightness-4 + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: orange + accent: orange + toggle: + icon: material/brightness-7 + name: Switch to dark mode + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/JakubAndrysek + - icon: fontawesome/brands/twitter + link: https://twitter.com/KubaAndrysek + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/jakub-andrysek/ + analytics: + provider: google + property: G-8WHJ2N4SHC + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: >- + Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: >- + Thanks for your feedback! + +use_directory_urls: True +#use_directory_urls: False + +plugins: + - search + - open-in-new-tab + - mkdoxy: + enabled: !ENV [ENABLE_MKDOXY, True] + projects: + mkdoxyApi: + src_dirs: mkdoxy + full_doc: True + custom_template_dir: templates-custom + doxy_config_file: demo-projects/animal/Doxyfile + doxy_config_dict: + FILE_PATTERNS: "*.py" + EXAMPLE_PATH: "" + RECURSIVE: True + OPTIMIZE_OUTPUT_JAVA: True + JAVADOC_AUTOBRIEF: True + EXTRACT_ALL: True + animal: + src_dirs: demo-projects/animal + full_doc: True + doxy_config_dict: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + custom_api_folder: .mkdoxy + full_doc: True + debug: False + ignore_errors: False + + +markdown_extensions: + - pymdownx.highlight + - pymdownx.superfences + - def_list + - toc: + permalink: True + - pymdownx.superfences + - admonition + - pymdownx.details + - pymdownx.superfences + - markdown.extensions.md_in_html + - pymdownx.snippets: + check_paths: true + - pymdownx.blocks.admonition: + types: + - new + - settings + - note + - abstract + - info + - tip + - success + - question + - warning + - failure + - danger + - bug + - example + - quote + - pymdownx.blocks.details: + - pymdownx.blocks.html: + - pymdownx.blocks.definition: + - pymdownx.blocks.tab: + - pymdownx.tabbed: + alternate_style: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +nav: + - Home: + - "README.md" + - "Changelog": "changelog.md" + - "License": "license.md" + - "Usage": + - "usage/index.md" + - "Advanced Usage": "usage/advanced.md" + + - Snippets: + - "snippets/index.md" + - "Intelli sense and errors": "snippets/intelli_sense_and_errors.md" + - "Classes": "snippets/classes.md" + - "Source code": "snippets/source_code.md" + - "Links": "snippets/links.md" + - "Functions": "snippets/functions.md" + - "Namespaces": "snippets/namespaces.md" + - "Files": "snippets/files.md" + - MkDoxy API: + - "mkdoxyApi/index.md" + - "Links": "mkdoxyApi/links.md" + - "Classes": + - "Class List": "mkdoxyApi/annotated.md" + - "Class Index": "mkdoxyApi/classes.md" + - "Class Hierarchy": "mkdoxyApi/hierarchy.md" + - "Class Members": "mkdoxyApi/class_members.md" + - "Class Member Functions": "mkdoxyApi/class_member_functions.md" + - "Class Member Variables": "mkdoxyApi/class_member_variables.md" + - "Class Member Typedefs": "mkdoxyApi/class_member_typedefs.md" + - "Class Member Enumerations": "mkdoxyApi/class_member_enums.md" + - "Namespaces": + - "Namespace List": "mkdoxyApi/namespaces.md" + - "Namespace Members": "mkdoxyApi/namespace_members.md" + - "Namespace Member Functions": "mkdoxyApi/namespace_member_functions.md" + - "Namespace Member Variables": "mkdoxyApi/namespace_member_variables.md" + - "Namespace Member Typedefs": "mkdoxyApi/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "mkdoxyApi/namespace_member_enums.md" + - "Functions": "mkdoxyApi/functions.md" + - "Variables": "mkdoxyApi/variables.md" + - "Macros": "mkdoxyApi/macros.md" + - "Files": "mkdoxyApi/files.md" + - Demo API: + - "animal/index.md" + - "Links": "animal/links.md" + - "Classes": + - "Class List": "animal/annotated.md" + - "Class Index": "animal/classes.md" + - "Class Hierarchy": "animal/hierarchy.md" + - "Class Members": "animal/class_members.md" + - "Class Member Functions": "animal/class_member_functions.md" + - "Class Member Variables": "animal/class_member_variables.md" + - "Class Member Typedefs": "animal/class_member_typedefs.md" + - "Class Member Enumerations": "animal/class_member_enums.md" + - "Namespaces": + - "Namespace List": "animal/namespaces.md" + - "Namespace Members": "animal/namespace_members.md" + - "Namespace Member Functions": "animal/namespace_member_functions.md" + - "Namespace Member Variables": "animal/namespace_member_variables.md" + - "Namespace Member Typedefs": "animal/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "animal/namespace_member_enums.md" + - "Functions": "animal/functions.md" + - "Variables": "animal/variables.md" + - "Macros": "animal/macros.md" + - "Files": "animal/files.md" + - Advanced Demo: "https://mkdoxy-demo.kubaandrysek.cz/" diff --git a/tests/migration/data/2_old.yaml b/tests/migration/data/2_old.yaml new file mode 100644 index 00000000..596b3541 --- /dev/null +++ b/tests/migration/data/2_old.yaml @@ -0,0 +1,208 @@ +site_name: MkDoxy +site_url: https://mkdoxy.kubaandrysek.cz/ +site_author: Jakub Andrýsek +site_description: >- + MkDoxy -> MkDocs + Doxygen. Easy documentation generator with code snippets. + +# Repository +repo_name: JakubAndrysek/MkDoxy/ +repo_url: https://github.com/JakubAndrysek/MkDoxy/ + +# Copyright +copyright: Copyright © 2023 Jakub Andrýsek + +theme: + name: material + language: en + logo: assets/logo.png + favicon: assets/logo.png + features: + - navigation.tabs + - navigation.indexes + - navigation.top + + icon: + repo: fontawesome/brands/github + + palette: + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: orange + accent: orange + toggle: + icon: material/brightness-4 + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: orange + accent: orange + toggle: + icon: material/brightness-7 + name: Switch to dark mode + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/JakubAndrysek + - icon: fontawesome/brands/twitter + link: https://twitter.com/KubaAndrysek + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/jakub-andrysek/ + analytics: + provider: google + property: G-8WHJ2N4SHC + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: >- + Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: >- + Thanks for your feedback! + +use_directory_urls: True +#use_directory_urls: False + +plugins: + - search + - open-in-new-tab + - mkdoxy: + enabled: !ENV [ENABLE_MKDOXY, True] + projects: + mkdoxyApi: + src-dirs: mkdoxy + full-doc: True + template-dir: templates-custom + doxy-cfg-file: demo-projects/animal/Doxyfile + doxy-cfg: + FILE_PATTERNS: "*.py" + EXAMPLE_PATH: "" + RECURSIVE: True + OPTIMIZE_OUTPUT_JAVA: True + JAVADOC_AUTOBRIEF: True + EXTRACT_ALL: True + animal: + src-dirs: demo-projects/animal + full-doc: True + doxy-cfg: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + save-api: .mkdoxy + full-doc: True + debug: False + ignore-errors: False + + +markdown_extensions: + - pymdownx.highlight + - pymdownx.superfences + - def_list + - toc: + permalink: True + - pymdownx.superfences + - admonition + - pymdownx.details + - pymdownx.superfences + - markdown.extensions.md_in_html + - pymdownx.snippets: + check_paths: true + - pymdownx.blocks.admonition: + types: + - new + - settings + - note + - abstract + - info + - tip + - success + - question + - warning + - failure + - danger + - bug + - example + - quote + - pymdownx.blocks.details: + - pymdownx.blocks.html: + - pymdownx.blocks.definition: + - pymdownx.blocks.tab: + - pymdownx.tabbed: + alternate_style: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +nav: + - Home: + - "README.md" + - "Changelog": "changelog.md" + - "License": "license.md" + - "Usage": + - "usage/index.md" + - "Advanced Usage": "usage/advanced.md" + + - Snippets: + - "snippets/index.md" + - "Intelli sense and errors": "snippets/intelli_sense_and_errors.md" + - "Classes": "snippets/classes.md" + - "Source code": "snippets/source_code.md" + - "Links": "snippets/links.md" + - "Functions": "snippets/functions.md" + - "Namespaces": "snippets/namespaces.md" + - "Files": "snippets/files.md" + - MkDoxy API: + - "mkdoxyApi/index.md" + - "Links": "mkdoxyApi/links.md" + - "Classes": + - "Class List": "mkdoxyApi/annotated.md" + - "Class Index": "mkdoxyApi/classes.md" + - "Class Hierarchy": "mkdoxyApi/hierarchy.md" + - "Class Members": "mkdoxyApi/class_members.md" + - "Class Member Functions": "mkdoxyApi/class_member_functions.md" + - "Class Member Variables": "mkdoxyApi/class_member_variables.md" + - "Class Member Typedefs": "mkdoxyApi/class_member_typedefs.md" + - "Class Member Enumerations": "mkdoxyApi/class_member_enums.md" + - "Namespaces": + - "Namespace List": "mkdoxyApi/namespaces.md" + - "Namespace Members": "mkdoxyApi/namespace_members.md" + - "Namespace Member Functions": "mkdoxyApi/namespace_member_functions.md" + - "Namespace Member Variables": "mkdoxyApi/namespace_member_variables.md" + - "Namespace Member Typedefs": "mkdoxyApi/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "mkdoxyApi/namespace_member_enums.md" + - "Functions": "mkdoxyApi/functions.md" + - "Variables": "mkdoxyApi/variables.md" + - "Macros": "mkdoxyApi/macros.md" + - "Files": "mkdoxyApi/files.md" + - Demo API: + - "animal/index.md" + - "Links": "animal/links.md" + - "Classes": + - "Class List": "animal/annotated.md" + - "Class Index": "animal/classes.md" + - "Class Hierarchy": "animal/hierarchy.md" + - "Class Members": "animal/class_members.md" + - "Class Member Functions": "animal/class_member_functions.md" + - "Class Member Variables": "animal/class_member_variables.md" + - "Class Member Typedefs": "animal/class_member_typedefs.md" + - "Class Member Enumerations": "animal/class_member_enums.md" + - "Namespaces": + - "Namespace List": "animal/namespaces.md" + - "Namespace Members": "animal/namespace_members.md" + - "Namespace Member Functions": "animal/namespace_member_functions.md" + - "Namespace Member Variables": "animal/namespace_member_variables.md" + - "Namespace Member Typedefs": "animal/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "animal/namespace_member_enums.md" + - "Functions": "animal/functions.md" + - "Variables": "animal/variables.md" + - "Macros": "animal/macros.md" + - "Files": "animal/files.md" + - Advanced Demo: "https://mkdoxy-demo.kubaandrysek.cz/" diff --git a/tests/migration/test_migration.py b/tests/migration/test_migration.py new file mode 100644 index 00000000..3fedc96f --- /dev/null +++ b/tests/migration/test_migration.py @@ -0,0 +1,56 @@ +import shutil +from pathlib import Path +import pytest +from mkdoxy.migration import update_new_config + +# Directory containing test data files. +DATA_DIR = Path(__file__).parent / "data" + + +@pytest.fixture(params=["1", "2"]) +def migration_files(request, tmp_path: Path) -> tuple: + """ + Parameterized fixture that copies the legacy YAML file (_old.yaml) + to a temporary file and loads the expected file text from _expect.yaml. + + :returns: A tuple (old_yaml_path, expected_text, prefix) + """ + prefix = request.param + # Copy legacy file to a temporary file. + src = DATA_DIR / f"{prefix}_old.yaml" + dst = tmp_path / f"test_{prefix}.yaml" + shutil.copy(src, dst) + + # Load expected configuration text (without parsing YAML). + expected_text = (DATA_DIR / f"{prefix}_expect.yaml").read_text(encoding="utf-8") + return dst, expected_text, prefix + + +def test_migration_without_backup(migration_files): + """ + Test that migration updates the legacy configuration correctly without creating a backup. + """ + old_yaml, expected_text, prefix = migration_files + # Run migration with backup turned off. + update_new_config(old_yaml, backup=False, backup_file_name="backup.yaml") + + updated_text = old_yaml.read_text(encoding="utf-8") + assert updated_text == expected_text, f"Test case {prefix} failed: output text does not match expected." + + +def test_migration_with_backup(migration_files): + """ + Test that migration creates a backup file and updates the configuration correctly. + """ + old_yaml, expected_text, prefix = migration_files + backup_file_name = "backup.yaml" + + # Run migration with backup enabled. + update_new_config(old_yaml, backup=True, backup_file_name=backup_file_name) + + # Verify that the backup file was created. + backup_file = old_yaml.parent / backup_file_name + assert backup_file.exists(), f"Test case {prefix}: Backup file was not created." + + updated_text = old_yaml.read_text(encoding="utf-8") + assert updated_text == expected_text, f"Test case {prefix} failed: output text does not match expected."