Skip to content
Snippets Groups Projects
plugins.py 32.66 KiB
#!/usr/bin/env python3
"""Set of methods to manage mkdocs configuration dynamically.

Set of methods for
[mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/) Set of
which :

- Define macros usable in jinja template
- Update `env.conf`, i.e. mkdocs configuration based on variables files
- Dynamically update `env` dictionary and `nav` entries to support subrepo
  either internal subrepo, using
  [mkdocs-monorepo-plugin](https://github.com/backstage/mkdocs-monorepo-plugin),
  or external subrepo by using `online_url` of such subrepo.

This script allow to make content of `mkdocs.yml` to be templated, i.e. using
the same `mkdocs.yml` file for multiple repos and use variables files in
`docs/_data/`
"""

# pylint: disable=R0801

# JSON encoder and decoder
# https://docs.python.org/3/library/json.html
import json

# Logging facility
# https://docs.python.org/3/library/logging.html
import logging

# Miscellaneous operating system interfaces
# https://docs.python.org/3/library/os.html
import os

# Regular expression operations
# https://docs.python.org/3/library/re.html
import re

# System-specific parameters and functions
# https://docs.python.org/3/library/sys.html
import sys

# Time access and conversions
# https://docs.python.org/3/library/time.html
import time

# Python Git Library
# https://pypi.org/project/GitPython/
import git

# Python implementation of Markdown
# https://pypi.org/project/markdown/
import markdown

# YAML parser and emitter for Python
# https://pypi.org/project/PyYAML/
import yaml

# Python lib/cli for JSON/YAML schema validation
# https://pypi.org/project/pykwalify/
from pykwalify.core import Core as yamlschema

# pylint: disable=W0105
# - W0105: String statement has no effect
LOG = logging.getLogger(__name__)
"""The logger facilty."""
ERR_CLR = "\033[31m"
"""String coloring error output in red."""
INFO_CLR = "\033[32m"
"""String coloring error output in green."""
RESET_CLR = "\033[0m"
"""String reseting coloring output."""


def add_internal_to_nav(
    env: dict,
    nav: dict,
    repo_dict: dict,
    repo_parent: list,
    nav_parent: list = None,
) -> None:
    """Add internal subrepo to `nav` key of mkdocs.yml for monorepo.

    This method recursively parse `nav_parent` arguments to know where to
    include the internal subrepo into `nav` key.

    Once determined, add the subrepo as a entry to the `nav` key, with the
    format required by
    [mkdocs-monorepo-plugin](https://github.com/backstage/mkdocs-monorepo-plugin).

    Args:
        env : Environment dictionary provided by
            [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/)
        nav : Navigation dictionary (subpart of it if called
            recursively)
        repo_dict : Repo dictionary from `subrepo.yml` file in `docs/_data/`
        repo_parent : List of keys storing parent keys of the current
            `repo_dict` from `subrepo.yml` file in `docs/_data`
        nav_parent : List of keys storing parents `nav_entry` keys of the
            current `repo_dict` from `subrepo.yml` file in `docs/_data`
    """
    if nav_parent:
        for i_nav in nav:
            if nav_parent[0] in i_nav:
                for i_key in i_nav:
                    add_internal_to_nav(
                        env,
                        i_nav[i_key],
                        repo_dict,
                        repo_parent,
                        nav_parent[1:],
                    )
    else:
        mkdocs_path = env.project_dir
        for i_parent in repo_parent:
            mkdocs_path = os.path.join(mkdocs_path, i_parent)
        mkdocs_path = os.path.join(mkdocs_path, repo_dict["name"])
        if "subpath" in repo_dict:
            mkdocs_path = os.path.join(mkdocs_path, repo_dict["subpath"])
        mkdocs_path = os.path.join(mkdocs_path, "mkdocs.yml")
        nav.append({repo_dict["nav_entry"]: f"!include {mkdocs_path}"})


def add_external_to_nav(
    env: dict, nav: dict, repo_dict: dict, repo_parent: list, nav_parent: list
) -> None:
    """Add external subrepo to `nav` key of mkdocs.yml.

    This method recursively parse `nav_parent` arguments to know where to
    include the external subrepo into `nav` key.

    Once determined, add the subrepo as a entry to the `nav` key, with the
    `online_url` key of the current subrepo defined with `repo_dict` in file
    `subrepo.yml` in `docs/_data`.

    Args:
        env : Environment dictionary provided by
            [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/)
        nav : Navigation dictionary (subpart of it if called recursively)
        repo_dict : Repo dictionary from `subrepo.yml` file in `docs/_data/`
        repo_parent : List of keys storing parent keys of the current
            `repo_dict` from `subrepo.yml` file in `docs/_data`
        nav_parent : List of keys storing parents `nav_entry` keys of the
            current `repo_dict` from `subrepo.yml` file in `docs/_data`
    """
    if nav_parent:
        for i_nav in nav:
            if nav_parent[0] in i_nav:
                for i_key in i_nav:
                    add_external_to_nav(
                        env,
                        i_nav[i_key],
                        repo_dict,
                        repo_parent,
                        nav_parent[1:],
                    )
    elif repo_dict["online_url"].startswith('/'):
        nav.append({
            repo_dict["nav_entry"]: repo_dict["online_url"].replace('/','../',1)
        })
    else:
        nav.append({repo_dict["nav_entry"]: repo_dict["online_url"]})


def add_nav_entry(nav: list, nav_parent: list = None) -> None:
    """Create missing entry into `nav` key of `env.conf`.

    Recursively parse list `nav_parent` and create missing entry into key `nav`
    of mkdocs.yml.

    Args:
        nav : Navigation dictionary (subpart of it if called recursively)
        nav_parent : List of keys storing parents `nav_entry` keys
    """
    entry = dict()

    for i_nav in nav:
        if nav_parent[0] in i_nav:
            entry = i_nav

    if not entry:
        entry = {nav_parent[0]: []}
        nav.append(entry)

    if len(nav_parent[1:]) == 0:
        return
    add_nav_entry(entry[nav_parent[0]], nav_parent[1:])


def update_nav(
    env: dict,
    repo_dict: dict,
    repo_parent: list = None,
    nav_parent: list = None,
    first_iteration=False,
) -> None:
    """Meta method which dynamically update the `nav` key of `env.conf`.

    Recursively parse `repo_dict` (provided from `subrepo.yml` file in
    `docs/_data`), depending on the content of the keys, method will:

    - Update the list of `nav_parent` and `repo_parent`,
    - Call [add_nav_entry][plugins.add_nav_entry] to add missing entry to `nav`
      key of `mkdocs.yml`,
    - Call [add_external_to_nav][plugins.add_external_to_nav] to add external
      subrepo to `nav` key of `mkdocs.yml`,
    - Call [add_internal_to_nav][plugins.add_internal_to_nav] to add internal
      subrepo to `nav` key of `mkdocs.yml`,
    - Recursively call itself.

    Args:
        env : Environment dictionary provided by
            [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/)
        repo_dict : Repo dictionary from `subrepo.yml` file in `docs/_data/`
        repo_parent : List of keys storing parent keys of the current
            `repo_dict` from `subrepo.yml` file in `docs/_data`
        nav_parent : List of keys storing parents `nav_entry` keys of the
            current `repo_dict` from `subrepo.yml` file in `docs/_data`
        first_iteration : Simple boolean to know if it is the first recursive
            call of the method.
    """
    for i_key in repo_dict:
        if not nav_parent or first_iteration:
            nav_parent = list()

        if not repo_parent or first_iteration:
            repo_parent = list()

        if i_key == "nav_entry":
            nav_parent.append(repo_dict["nav_entry"])
        elif i_key == "internal":
            for i_repo in repo_dict["internal"]:
                add_nav_entry(env.conf["nav"], nav_parent)
                add_internal_to_nav(
                    env, env.conf["nav"], i_repo, repo_parent, nav_parent
                )
        elif i_key == "external":
            for i_repo in repo_dict["external"]:
                add_nav_entry(env.conf["nav"], nav_parent)
                add_external_to_nav(
                    env, env.conf["nav"], i_repo, repo_parent, nav_parent
                )
        else:
            repo_parent.append(i_key)
            update_nav(env, repo_dict[i_key], repo_parent, nav_parent)


def get_repo_slug(env: dict, git_repo: git.Repo) -> str:
    """Compute the slug of the `git_repo` and ensure repo dictionary is defined.

    Compute the slug of the repo provided as `git_repo` based on the origin
    remote. If no remo, then will use the folder name.

    Then ensure the repo dictionary is defined in `docs/_data/`. If not, print
    an error and exit.

    Else, update value of `env.variables["git"]` and return the `repo_slug`.

    Arguments:
        env : Environment dictionary provided by
            [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/)
        git_repo: Git python object of the current repo.

    Returns:
        Posix path from `os.path` python library.
    """
    if git_repo.remotes:
        repo_slug = (
            git_repo.remotes.origin.url.rsplit("/")[-1]
            .split(".git")[0]
            .replace(".", "_")
        )
    else:
        repo_slug = os.path.basename(env.project_dir)

    if repo_slug not in env.variables:
        LOG.error(
            "%s[macros] - Dictionary %s is not defined.%s",
            ERR_CLR,
            repo_slug,
            RESET_CLR,
        )
        LOG.error(
            "%s[macros] - Ensure you copy docs/_data/templates/repo.tpl.yaml "
            "to docs/_data/%s.yaml.%s",
            ERR_CLR,
            repo_slug,
            RESET_CLR,
        )
        LOG.error(
            "%s[macros] - And you setup dictionary %s in docs/_data/%s.yaml.%s",
            ERR_CLR,
            repo_slug,
            repo_slug,
            RESET_CLR,
        )
        sys.exit(1)

    env.variables["git"]["repo_slug"] = repo_slug
    return repo_slug


def set_site_name(env: dict, repo_slug: str) -> None:
    """Update content of the `site_name` key in `env.conf`.

    Update the value of `site_name` keys for mkdocs documentation based on (in
    precedence order):

    - Value of `site_name` in `mkdocs.yml`,
    - Value of `site_name` in `env.variables`, from `docs/_data/vars.yml`,
    - Value of `name` in `env.variables[repo_slug]` from `docs/_data/repo.yml`.


    If `site_name` key is not defined in `mkdocs.yml` then look to
    `docs/_data/vars.yml`, if defined, else look to the the current repo
    dictionary to set value of `site_name`.

    Arguments:
        env: Mkdocs macro plugin environment dictionary.
        repo_slug: Repo slug or name of the repo folder.
    """
    if "site_name" not in env.conf or not env.conf["site_name"]:
        if "site_name" in env.variables:
            env.conf["site_name"] = env.variables["site_name"]
        else:
            env.conf["site_name"] = env.variables[repo_slug]["name"]


def set_site_desc(env: dict, repo_slug: str) -> None:
    """Update content of the `site_desc` key in `env.conf`.

    Update the value of `site_desc` keys for mkdocs configuration based on (in
    precedence order):

    - Value of `site_desc` in `mkdocs.yml`,
    - Value of `site_desc` in `env.variables`, from `docs/_data/vars.yml`,
    - Value of `desc` in `env.variables[repo_slug]` from `docs/_data/repo.yml`.

    Arguments:
        env: Mkdocs macro plugin environment dictionary.
        repo_slug: Repo slug or name of the repo folder.
    """
    if "site_desc" not in env.conf:
        if "site_desc" in env.variables:
            env.conf["site_desc"] = env.variables["site_desc"]
        else:
            env.conf["site_desc"] = env.variables[repo_slug]["desc"]


def set_site_url(env: dict, repo_slug: str) -> None:
    """Update content of the `site_url` key in `env.conf`.

    Update the value of `site_url` key for mkdocs documentation based on (in
    precedence order):

    - Value of `site_url` in `mkdocs.yml`,
    - Value of `site_url` in `env.variables`, from `docs/_data/vars.yml`,
    - Value of `site_base_url` in `env.variables`, from `docs/_data/vars.yml`,
      concatenate with `env.variables[repo_slug]["url_slug_with_namespace"]`
      from `docs/_data/repo.yml`.

    Arguments:
        env: Mkdocs macro plugin environment dictionary.
        repo_slug: Repo slug or name of the repo folder.
    """
    if "site_url" not in env.conf:
        if "site_url" in env.variables:
            env.conf["site_url"] = env.variables["site_url"]
        elif "site_base_url" in env.variables:
            site_url = (
                env.variables["site_base_url"]
                + env.variables[repo_slug]["url_slug_with_namespace"]
            )
            env.conf["site_url"] = site_url


def set_copyright(env: dict, git_repo: git.Repo) -> None:
    """Update content of the `copyright` key in `env.conf`.

    Update the value of `copyright` key for mkdocs documentation based on (in
    precedence order):

    - Value of `copyright` in `mkdocs.yml`,
    - Value of `copyright` in `env.variables`, from `docs/_data/vars.yml`, then,
      using this value:
        - Value of the year of the first commit of the repo holding the
          documentation and current year,
        - Value of the current year only,

    If no `copyright` key defined, neither in `mkdocs.yml`, nor in
    `docs/_data/vars.yml`, then not copyright will be set.

    Arguments:
        env: Mkdocs macro plugin environment dictionary.
        git_repo: Git python object of the current repo.
    """
    if (
        "copyright" not in env.conf or not env.conf["copyright"]
    ) and "copyright" in env.variables:
        if git_repo.branches and git_repo.branches.master:
            first_date = git_repo.commit(
                git_repo.branches.master.log()[0].newhexsha
            ).committed_date
            first_year = time.strftime("%Y", time.gmtime(first_date))
        else:
            first_year = time.strftime("%Y", time.localtime())
        curr_year = time.strftime("%Y", time.localtime())

        if first_year == curr_year:
            env.variables[
                "date_copyright"
            ] = f"Copyright © {curr_year}"
        else:
            env.variables[
                "date_copyright"
            ] = f"Copyright © {curr_year} - {curr_year}"

        env.conf[
            "copyright"
        ] = f"{env.variables['date_copyright']} {env.variables['copyright']}"



def set_repo_name(env: dict, repo_slug: str) -> None:
    """Update content of the `repo_name` key in `env.conf`.

    Update the value of `repo_name` key for mkdocs documentation based on (in
    precedence order):

    - Value of `repo_name` in `mkdocs.yml`,
    - Value of `repo_name` in `env.variables`, from `docs/_data/vars.yml`,
    - Value of `name` in `env.variables[repo_slug]`, from `docs/_data/repo.yml`,
      then, depending on its value:
      - If value is `!!git_platform`, then value of `repo_name` will be set to
        the value of `env.variables['git_platform']['name']`, from
        `docs/_data/vars.yml`
      - Else, value is key `name` of `env.variables[repo_slug]

    Arguments:
        env: Mkdocs macro plugin environment dictionary.
        repo_slug: Repo slug or name of the repo folder.
    """
    if "repo_name" not in env.conf or not env.conf["repo_name"]:
        if "repo_name" in env.variables:
            env.conf["repo_name"] = env.variables["repo_name"]
        elif "name" in env.variables[repo_slug]:
            if env.variables[repo_slug]["name"] == "!!git_platform":
                env.conf["repo_name"] = env.variables["git_platform"]["name"]
            else:
                env.conf["repo_name"] = env.variables[repo_slug]["name"]


def set_repo_url(env: dict, repo_slug: str) -> None:
    """Update content of the `repo_url` key in `env.conf`.

    Update the value of `repo_url` key for mkdocs documentation based on (in
    precedence order):

    - Value of `repo_url` in `mkdocs.yml`,
    - Value of `repo_url` in `env.variables`, from `docs/_data/vars.yml`,
    - Concatenation of the `url` of `env.variables['git_platform']`, from
      `docs/_data/vars.yml` and value `git_slug_with_namespace` in
      `env.variables[repo_slug]`, from `docs/_data/repo.yml`.

    Arguments:
        env: Mkdocs macro plugin environment dictionary.
        repo_slug: Repo slug or name of the repo folder.
    """
    if "repo_url" not in env.conf or not env.conf["repo_url"]:
        if "repo_url" in env.variables:
            env.conf["repo_url"] = env.variables["repo_url"]
        elif "repo_url" in env.conf:
            env.conf["repo_url"] = (
                f"{env.variables['git_platform']['url']}"
                + f"{env.variables[repo_slug]['git_slug_with_namespace']}"
            )


def set_nav(env: dict) -> None:
    """Update content of the `nav` key in `env.conf`.

    Update the value of `nav` key for mkdocs documentation based on (in
    precedence order):

    - Value of `nav` in `vars.yml` or `extra.yml` in `docs/_data/`, allowing
      overloading of nav for forked repo.
    - Value of `nav` in `mkdocs.yml`

    Arguments:
        env: Mkdocs macro plugin environment dictionary.
    """
    if "nav" in env.variables and env.variables["nav"]:
        env.conf["nav"] = env.variables["nav"]


def update_theme(env: dict, repo_slug: str) -> None:
    """Update content of the `theme` key in `env.conf`.

    If `theme` key is defined in `docs/_data/vars.yml`, this override the
    content of the default `theme` key in mkdocs documentation.

    Arguments:
        env: Mkdocs macro plugin environment dictionary.
        repo_slug: Repo slug or name of the repo folder.
    """
    if "theme" in env.variables:
        for i_key in env.variables["theme"]:
            env.conf["theme"][i_key] = env.variables["theme"][i_key]

    if "logo" not in env.conf["theme"] or not env.conf["theme"]["logo"]:
        if "logo" in env.variables[repo_slug]:
            env.conf["theme"]["logo"] = env.variables[repo_slug]["logo"]
        else:
            env.conf["theme"]["logo"] = os.path.join(
                "assets", "img", "meta", f"{repo_slug}_logo.png"
            )

    if not env.conf["theme"]["icon"]:
        env.conf["theme"]["icon"] = {}

    if "icon" not in env.conf["theme"] or not env.conf["theme"]["icon"]:
        env.conf["theme"]["icon"]["repo"] = env.variables["git_platform"][
            "icon"
        ]

    if "favicon" not in env.conf["theme"] or not env.conf["theme"]["favicon"]:
        if "favicon" in env.variables[repo_slug]:
            env.conf["theme"]["favicon"] = env.variables[repo_slug]["favicon"]
        elif "logo" in env.variables[repo_slug]:
            env.conf["theme"]["favicon"] = env.variables[repo_slug]["logo"]
    else:
        env.conf["theme"]["favicon"] = os.path.join(
            "assets", "img", "meta", f"{repo_slug}_logo.png"
        )


def set_config(env: dict) -> None:
    """Dynamically update mkdocs configuration.

    Based on the `repo_slug` (or folder name) load variables in
    `docs/_data/vars.yml`, in `docs/_data/repo.yml` and update content of mkdocs
    documentation accordingly.

    Especially, if `docs/_data/subrepo.yaml` exists and define valid subrepos,
    clone these subrepo and dynamically add them to the `nav` key of the mkdocs
    configuration.

    Arguments:
        env: Mkdocs macro plugin environment dictionary.
    """
    git_repo = git.Repo(search_parent_directories=True)
    repo_slug = get_repo_slug(env, git_repo)

    set_site_name(env, repo_slug)
    set_site_desc(env, repo_slug)
    set_site_url(env, repo_slug)
    set_copyright(env, git_repo)
    set_repo_name(env, repo_slug)
    set_repo_url(env, repo_slug)
    set_nav(env)
    update_theme(env, repo_slug)

    if "subrepo" in env.variables:
        if (
            env.variables["internal_subdoc"]
            and "monorepo" in env.conf["plugins"]
        ):
            env.conf["plugins"].pop("monorepo")
        else:
            update_nav(env, env.variables["subrepo"], first_iteration=True)


def load_yaml_file(path: str, filename: str) -> None:
    """Ensure a YAML file is valid again a schema and return its content.

    Depending on the name of the YAML file, compare its content to a schema to
    validate its content. If content is not valid, an error will be raised.
    Otherwise, its content will be returned.

    If filename is `extra.yml` or `extra.yaml`, load content of the file
    unconditionnally.

    Arguments:
        path: Base path where YAML files are.
        filename: Name of the YAML file to load.
    """
    source_file = os.path.join(path, filename)
    schema_file = os.path.join(path, "schema")
    data_type = ""

    if filename not in ("extra.yaml", "extra.yml"):
        if filename in ("subrepo.yaml", "subrepo.yml"):
            schema_file = os.path.join(schema_file, "subrepo.schema.yaml")
        elif filename in ("vars.yaml", "vars.yml"):
            schema_file = os.path.join(schema_file, "vars.schema.yaml")
        elif filename not in ("extra.yaml", "extra.yml"):
            schema_file = os.path.join(schema_file, "repo.schema.yaml")
            data_type = "repo"
        schema = yamlschema(source_file=source_file, schema_files=[schema_file])
        schema.validate(raise_exception=True)
        data_content = schema.source
    else:
        with open(source_file) as file:
            data_content = yaml.safe_load(file)

    return data_content, data_type


# pylint: disable=R0913
# - R0913: Too many arguments
def update_subrepo_logo_src(
    env: dict,
    curr_repo: dict,
    repo_name: str,
    subrepo_dict: dict,
    path: str,
    external: bool,
) -> None:
    """Update the content of the key `logo` and `src_path` of subrepo.

    Update value of keys `logo` and `src_path` of cloned subrepo, i.e. value
    from file `docs/_data/repo.yaml` in the cloned subrepo, relative to the main
    repo holding the documentation.

    Args:
        env : Environment dictionary provided by
            [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/)
        curr_repo : Repo dictionary from `repo.yml` file in `docs/_data/` in the
            cloned subrepo,
        repo_name: Name of the repo,
        subrepo_dict: Dictionary of the repo as defined in file `subrepo.yaml`
            in `docs/_data`,
        path: Absolute path of the location of the cloned subrepo,
        external: Boolean to know if current repo is an external subrepo.
    """
    logo_subpath = ""
    src_subpath = ""
    if external:
        logo_subpath = os.path.join(subrepo_dict["online_url"])

    src_subpath = os.path.join(
        path.replace(f"{env.project_dir}/", ""), repo_name
    )

    if "logo" not in curr_repo:
        curr_repo["logo"] = os.path.join(
            logo_subpath, "assets", "img", "meta", f"{repo_name}_logo.png"
        )
    if "src_path" in curr_repo:
        for i_src in curr_repo["src_path"]:
            i_src = os.path.join(src_subpath, i_src)
            env.conf["plugins"]["mkdocstrings"].config.data["handlers"][
                "python"
            ]["setup_commands"].append(f"sys.path.append('{i_src}')")


def update_subrepo_info(
    env: dict, subrepo_list: dict, path: str, external: bool = False
) -> dict:
    """Clone subrepo, load repo information and update values if needed.

    Recursively clone or pull repo defined from subpart of
    `env.variables['subrepo'], load repo information from this cloned or pulled
    repo, i.e. load file `docs/_data/repo.yaml` in the subrepo, and update
    needed keys.

    Args:
        env : Environment dictionary provided by
            [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/)
        subrepo_list: List of dictionary storing subrepo dict,
        path: Absolute path of the location of the cloned subrepo,
        external: Boolean to know if current repo is an external subrepo.

    Return:
        A updating dictionary storing subrepo information
    """
    return_dict = dict()
    for i_repo in subrepo_list:
        subrepo_root = os.path.join(path, i_repo["name"])

        if os.path.isdir(subrepo_root):
            print(
                f"{INFO_CLR}INFO [macros] - Pulling repo {i_repo['name']}{RESET_CLR}"
            )
            git_subrepo = git.Repo(subrepo_root)
            git_subrepo.remotes.origin.pull()
        else:
            print(
                f"{INFO_CLR}INFO [macros] - Cloning repo {i_repo['name']}{RESET_CLR}"
            )
            git.Repo.clone_from(i_repo["git_url"], subrepo_root)

        if "subpath" in i_repo:
            data_dir = os.path.join(
                subrepo_root, i_repo["subpath"], "docs", "_data"
            )
        else:
            data_dir = os.path.join(subrepo_root, "docs", "_data")

        data_file = os.path.join(data_dir, f"{i_repo['name']}.yaml")
        data, _ = load_yaml_file(data_dir, data_file)
        for i_repo_info in data:
            curr_repo = data[i_repo_info]
            update_subrepo_logo_src(
                env, curr_repo, i_repo_info, i_repo, path, external
            )
        return_dict.update(data)
    return return_dict


def update_subrepo(
    env: dict, subrepo_dict: dict, path: str, external: bool
) -> dict:
    """Recursively parse `env.variables['subrepo']`.

    Recursively parse dictionary `env.variables['subrepo']`, from file
    `docs/_data/subrepo.yaml`. Depending on the key:

    - `nav_entry`: Do a recursion of this method,
    - `external` or `internal`: Parse the list to update subrepo information

    Args:
        env : Environment dictionary provided by
            [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/)
        subrepo_dict: Dictionary storing subrepo,
        path: Absolute path of the location of the cloned subrepo,
        external: Boolean to know if current repo is an external subrepo.

    Returns:
        An updated dictionary of repo informations.
    """
    return_dict = dict()
    for i_key in subrepo_dict:
        if isinstance(subrepo_dict[i_key], list):
            if i_key == "external":
                external = True
            elif i_key == "internal":
                env.variables["internal_subdoc"] = True
            return_dict.update(
                update_subrepo_info(env, subrepo_dict[i_key], path, external)
            )
        elif i_key not in ["nav_entry"]:
            return_dict.update(
                update_subrepo(
                    env,
                    subrepo_dict[i_key],
                    os.path.join(path, i_key),
                    external,
                )
            )
    return return_dict


def update_logo_src_repo(
    env: dict, curr_repo: dict, repo_name: str, path: str = None
) -> None:
    """Update the content of the key `logo` and `src_path` of current repo.

    Update value of keys `logo` and `src_path` of current repo holding the
    documentation.

    Args:
        env : Environment dictionary provided by
            [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/)
        curr_repo : Repo dictionary from `repo.yml` file in `docs/_data/` in the
            cloned subrepo,
        repo_name: Name of the repo,
        path: Absolute path of the location of the current repo.
    """
    subpath = ""
    if path:
        subpath = os.path.join(path.replace(env.project_dir, ""), repo_name)

    if "logo" not in curr_repo:
        curr_repo["logo"] = os.path.join(
            subpath, "assets", "img", "meta", f"{repo_name}_logo.png"
        )
    if "src_path" in curr_repo:
        for i_src in curr_repo["src_path"]:
            i_src = os.path.join(subpath, i_src)
            env.conf["plugins"]["mkdocstrings"].config.data["handlers"][
                "python"
            ]["setup_commands"].append(f"sys.path.append('{i_src}')")


def load_var_file(env: dict) -> None:
    """Load variables files in `docs/_data/`.

    Load every yaml files in `docs/_data/`, if one of the file define the
    current repo, then update keys `logo` and `src_path` for the current repo.

    Arguments:
        env : Environment dictionary provided by
            [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/)
    """
    var_dir = os.path.join(env.project_dir, "docs", "_data")

    for i_file in os.listdir(var_dir):
        if i_file.endswith((".yml", ".yaml")):
            data, data_type = load_yaml_file(var_dir, i_file)
            for i_key in data:
                if data_type == "repo":
                    update_logo_src_repo(env, data[i_key], i_key)
                env.variables[i_key] = data[i_key]


def update_version(env: dict) -> None:
    """Parse every tags of the repo to build a `docs/versions.json`.

    To emulate mike version support for gitlab, this method will parse every
    tags of the current repo holding the current documentation to create a file
    `versions.json` which will be put in folder `docs`.

    This is mainly used for the CI to build a documentation per repo tags.

    Arguments:
        env : Environment dictionary provided by
            [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/)
    """
    if (
        "version" not in env.variables
        or "provider" not in env.variables["version"]
        or env.variables["version"]["provider"] != "mike"
    ):
        return
    git_repo = git.Repo(search_parent_directories=True)
    mike_version = list()
    last_major = -1
    last_minor = -1
    last_patch = -1
    for i_tag in git_repo.tags:
        i_tag = yaml.dump(i_tag.path)
        i_tag = re.sub(".*v", "", i_tag).split(".")
        major = int(i_tag[0])
        minor = int(i_tag[1])
        patch = int(i_tag[2])
        if major > last_major:
            if last_major >= 0:
                mike_version.append(
                    {
                        "version": f"{last_major}.{last_minor}",
                        "title": f"{last_major}.{last_minor}.{last_patch}",
                        "aliases": [],
                    }
                )
            last_major = major
            last_minor = -1
        if minor > last_minor:
            if last_minor >= 0:
                mike_version.append(
                    {
                        "version": f"{last_major}.{last_minor}",
                        "title": f"{last_major}.{last_minor}.{last_patch}",
                        "aliases": [],
                    }
                )
            last_minor = minor
            last_patch = -1
        if patch > last_patch:
            last_patch = patch
    mike_version.append(
        {
            "version": f"{last_major}.{last_minor}",
            "title": f"{last_major}.{last_minor}.{last_patch}",
            "aliases": ["latest"],
        }
    )
    mike_version.reverse()
    with open(
        os.path.join(env.project_dir, "docs", "versions.json"), "w"
    ) as version_file:
        json.dump(mike_version, version_file, indent=2)


def define_env(env: dict) -> None:
    # pylint: disable=C0301
    # - C0301: Line to long
    """Hook for mkdocs-macros-plugins defining variables, macros and filters.

    This is the hook for defining variables, macros and filters

    - variables: the dictionary that contains the environment variables
    - macro: a decorator function, to declare a macro.

    See
    [https://mkdocs-macros-plugin.readthedocs.io/en/latest/](https://mkdocs-macros-plugin.readthedocs.io/en/latest/)

    This hooks also start the initialization of the dynamic configuration of
    mkdocs.

    Arguments:
        env: Mkdocs macro plugin environment dictionary.
    """
    load_var_file(env)

    if "subrepo" in env.variables:
        env.variables["internal_subdoc"] = False
        env.variables.update(
            update_subrepo(
                env, env.variables["subrepo"], env.project_dir, False
            )
        )

    set_config(env)

    update_version(env)

    @env.macro
    # pylint: disable=W0612
    # -  W0612: Unused variable (unused-variable)
    def subs(var: str) -> dict:
        """Return the content of the dictionary defined by var.

        Arguments:
            var: Key in env.variables to return.

        Returns:
            The value of `env.variables[var]` if it exists, else return None.
        """
        if var in env.variables:
            return env.variables[var]
        return None

    @env.macro
    # pylint: disable=W0612
    # -  W0612: Unused variable (unused-variable)
    def to_html(var: str) -> dict:
        """Convert the content of the markdown string into HTML.

        Arguments:
            var: Markdown string which need to be converted to HTML

        Returns:
            The content of the markdown converted to HTML
        """
        return markdown.markdown(var)


# -----------------------------------------------------------------------------
# VIM MODELINE
# vim: fdm=indent
# -----------------------------------------------------------------------------