Example #1
0
def validate_build(testproject_path, plugin_config: dict = {}):
    """
    Validates a build from a testproject

    Args:
        testproject_path (Path): Path to test project
    """
    assert os.path.exists(str(testproject_path / "site"))

    # Make sure index file exists
    index_file = testproject_path / "site/index.html"
    assert index_file.exists(), "%s does not exist" % index_file

    # Make sure with markdown tag has valid
    # git revision date tag
    page_with_tag = testproject_path / "site/page_with_tag/index.html"
    contents = page_with_tag.read_text(encoding="utf8")
    assert re.search(r"Markdown tag\:\s[<span>|\w].+", contents)

    repo = Util(str(testproject_path / "docs"))
    date_formats = repo.get_revision_date_for_file(
        path=str(testproject_path / "docs/page_with_tag.md"),
        locale=plugin_config.get("locale"),
        fallback_to_build_date=plugin_config.get("fallback_to_build_date"),
    )

    searches = [re.search(x, contents) for x in date_formats.values()]
    assert any(searches), "No correct date formats output was found"
def test_date_formats():
    u = Util()
    assert u._date_formats(1582397529) == {
        "date": "February 22, 2020",
        "datetime": "February 22, 2020 18:52:09",
        "iso_date": "2020-02-22",
        "iso_datetime": "2020-02-22 18:52:09",
        "timeago": '<span class="timeago" datetime="2020-02-22T18:52:09+00:00" locale="en"></span>',
    }
Example #3
0
def test_date_formats():
    u = Util()
    assert u._date_formats(1582397529) == {
        "date":
        "February 22, 2020",
        "datetime":
        "February 22, 2020 18:52:09",
        "iso_date":
        "2020-02-22",
        "iso_datetime":
        "2020-02-22 18:52:09",
        "timeago":
        "<span class='timeago' datetime='1582397529000' locale='en'></span>",
    }
Example #4
0
def validate_build(testproject_path, plugin_config: dict = {}):
    """
    Validates a build from a testproject

    Args:
        testproject_path (Path): Path to test project
    """
    assert os.path.exists(str(testproject_path / "site"))

    # Make sure index file exists
    index_file = testproject_path / "site/index.html"
    assert index_file.exists(), "%s does not exist" % index_file

    # Make sure with markdown tag has valid
    # git revision date tag
    if not plugin_config.get('enabled'):
        return

    page_with_tag = testproject_path / "site/page_with_tag/index.html"
    contents = page_with_tag.read_text(encoding="utf8")
    assert re.search(r"renders as\:\s[<span>|\w].+", contents)

    repo = Util(config=plugin_config)
    date_formats = repo.get_date_formats_for_timestamp(
        commit_timestamp=repo.get_git_commit_timestamp(
            path=str(testproject_path / "docs/page_with_tag.md"),
            is_first_commit=False,
        ),
        locale=plugin_config['locale'],
        add_spans=True,
    )

    searches = [x in contents for x in date_formats.values()]
    assert any(searches), "No correct revision date formats output was found"

    if plugin_config.get("enable_creation_date"):
        commit_timestamp = repo.get_git_commit_timestamp(
            path=str(testproject_path / "docs/page_with_tag.md"),
            is_first_commit=True,
        )
        assert commit_timestamp == 1500854705
        date_formats = repo.get_date_formats_for_timestamp(
            commit_timestamp=commit_timestamp,
            locale=plugin_config['locale'],
            add_spans=True,
        )

        searches = [x in contents for x in date_formats.values()]
        assert any(
            searches), "No correct creation date formats output was found"
 def __init__(self):
     self.util = Util()
class GitRevisionDateLocalizedPlugin(BasePlugin):
    config_scheme = (
        ("fallback_to_build_date", config_options.Type(bool, default=False)),
        ("locale", config_options.Type(str, default=None)),
        ("type", config_options.Type(str, default="date")),
    )

    def __init__(self):
        self.util = Util()

    def on_config(self, config: config_options.Config) -> dict:
        """
        Determine which locale to use.

        The config event is the first event called on build and
        is run immediately after the user configuration is loaded and validated.
        Any alterations to the config should be made here.
        https://www.mkdocs.org/user-guide/plugins/#on_config

        Args:
            config (dict): global configuration object

        Returns:
            dict: global configuration object
        """

        # Get locale settings - might be added in future mkdocs versions
        # see: https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/issues/24
        mkdocs_locale = config.get("locale", None)

        # Get locale from plugin configuration
        plugin_locale = self.config.get("locale", None)

        # theme locale
        if "theme" in config and "locale" in config.get("theme"):
            custom_theme = config.get("theme")
            theme_locale = custom_theme._vars.get("locale")
            logging.debug(
                "Locale '%s' extracted from the custom theme: '%s'"
                % (theme_locale, custom_theme.name)
            )
        elif "theme" in config and "language" in config.get("theme"):
            custom_theme = config.get("theme")
            theme_locale = custom_theme._vars.get("language")
            logging.debug(
                "Locale '%s' extracted from the custom theme: '%s'"
                % (theme_locale, custom_theme.name)
            )

        else:
            theme_locale = None
            logging.debug(
                "No locale found in theme configuration (or no custom theme set)"
            )

        # First prio: plugin locale
        if plugin_locale:
            locale_set = plugin_locale
            logging.debug("Using locale from plugin configuration: %s" % locale_set)
        # Second prio: theme locale
        elif theme_locale:
            locale_set = theme_locale
            logging.debug(
                "Locale not set in plugin. Fallback to theme configuration: %s"
                % locale_set
            )
        # Third prio is mkdocs locale (which might be added in the future)
        elif mkdocs_locale:
            locale_set = mkdocs_locale
            logging.debug("Using locale from mkdocs configuration: %s" % locale_set)
        else:
            locale_set = "en"
            logging.debug("No locale set. Fallback to: %s" % locale_set)

        # set locale also in plugin configuration
        self.config["locale"] = locale_set

        return config

    def on_post_page(self, output_content: str, **kwargs) -> str:
        """
        Add timeago.js as a CDN to the HTML page.
        The CDN with latest version timeago.js can be found on
        https://cdnjs.com/libraries/timeago.js

        The `post_template` event is called after the template is rendered,
        but before it is written to disc and can be used to alter the output
        of the page. If an empty string is returned, the page is skipped
        and nothing is written to disc.

        https://www.mkdocs.org/user-guide/plugins/#on_post_page

        Args:
            output_content (str): output of rendered template as string

        Returns:
            str: output of rendered template as string
        """

        if self.config.get("type") != "timeago":
            return output_content

        extra_js = """
          <script src="https://cdnjs.cloudflare.com/ajax/libs/timeago.js/4.0.0-beta.2/timeago.min.js"></script>
          <script src="https://cdnjs.cloudflare.com/ajax/libs/timeago.js/4.0.0-beta.2/timeago.locales.min.js"></script>
          <script>
            var nodes = document.querySelectorAll('.timeago');
            var locale = nodes[0].getAttribute('locale');
            timeago.render(nodes, locale);
          </script>
        """
        idx = output_content.index("</body>")
        return output_content[:idx] + extra_js + output_content[idx:]

    def on_page_markdown(
        self, markdown: str, page: Page, config: config_options.Config, files
    ) -> str:
        """
        Replace jinja2 tags in markdown and templates with the localized dates

        The page_markdown event is called after the page's markdown is loaded
        from file and can be used to alter the Markdown source text.
        The meta- data has been stripped off and is available as page.meta
        at this point.

        https://www.mkdocs.org/user-guide/plugins/#on_page_markdown

        Args:
            markdown (str): Markdown source text of page as string
            page: mkdocs.nav.Page instance
            config: global configuration object
            site_navigation: global navigation object

        Returns:
            str: Markdown source text of page as string
        """

        revision_dates = self.util.get_revision_date_for_file(
            path=page.file.abs_src_path,
            locale=self.config.get("locale", "en"),
            fallback_to_build_date=self.config.get("fallback_to_build_date"),
        )
        revision_date = revision_dates[self.config["type"]]
        page.meta["git_revision_date_localized"] = revision_date
        return re.sub(
            r"\{\{\s*[page\.meta\.]*git_revision_date_localized\s*\}\}",
            revision_date,
            markdown,
            flags=re.IGNORECASE,
        )
Example #7
0
    def on_config(self, config: config_options.Config) -> dict:
        """
        Determine which locale to use.

        The config event is the first event called on build and
        is run immediately after the user configuration is loaded and validated.
        Any alterations to the config should be made here.
        https://www.mkdocs.org/user-guide/plugins/#on_config

        Args:
            config (dict): global configuration object

        Returns:
            dict: global configuration object
        """
        self.util = Util(path=config["docs_dir"])

        # Get locale settings - might be added in future mkdocs versions
        # see: https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/issues/24
        mkdocs_locale = config.get("locale", None)

        # Get locale from plugin configuration
        plugin_locale = self.config.get("locale", None)

        # theme locale
        if "theme" in config and "locale" in config.get("theme"):
            custom_theme = config.get("theme")
            theme_locale = custom_theme._vars.get("locale")
            logging.debug("Locale '%s' extracted from the custom theme: '%s'" %
                          (theme_locale, custom_theme.name))
        elif "theme" in config and "language" in config.get("theme"):
            custom_theme = config.get("theme")
            theme_locale = custom_theme._vars.get("language")
            logging.debug("Locale '%s' extracted from the custom theme: '%s'" %
                          (theme_locale, custom_theme.name))

        else:
            theme_locale = None
            logging.debug(
                "No locale found in theme configuration (or no custom theme set)"
            )

        # First prio: plugin locale
        if plugin_locale:
            locale_set = plugin_locale
            logging.debug("Using locale from plugin configuration: %s" %
                          locale_set)
        # Second prio: theme locale
        elif theme_locale:
            locale_set = theme_locale
            logging.debug(
                "Locale not set in plugin. Fallback to theme configuration: %s"
                % locale_set)
        # Third prio is mkdocs locale (which might be added in the future)
        elif mkdocs_locale:
            locale_set = mkdocs_locale
            logging.debug("Using locale from mkdocs configuration: %s" %
                          locale_set)
        else:
            locale_set = "en"
            logging.debug("No locale set. Fallback to: %s" % locale_set)

        # set locale also in plugin configuration
        self.config["locale"] = locale_set

        return config
Example #8
0
    def on_config(self, config: config_options.Config, **kwargs) -> Dict[str, Any]:
        """
        Determine which locale to use.

        The config event is the first event called on build and
        is run immediately after the user configuration is loaded and validated.
        Any alterations to the config should be made here.
        https://www.mkdocs.org/user-guide/plugins/#on_config

        Args:
            config (dict): global configuration object

        Returns:
            dict: global configuration object
        """
        if not self.config.get('enabled'):
            return config
        
        self.util = Util(config=self.config)

        # Save last commit timestamp for entire site
        self.last_site_revision_timestamp = self.util.get_git_commit_timestamp(
            config.get('docs_dir')
        )

        # Get locale settings - might be added in future mkdocs versions
        # see: https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/issues/24
        mkdocs_locale = config.get("locale", None)

        # Get locale from plugin configuration
        plugin_locale = self.config.get("locale", None)

        # theme locale
        if "theme" in config and "locale" in config.get("theme"):
            custom_theme = config.get("theme")
            theme_locale = custom_theme._vars.get("locale")
            logging.debug(
                "Locale '%s' extracted from the custom theme: '%s'"
                % (theme_locale, custom_theme.name)
            )
        elif "theme" in config and "language" in config.get("theme"):
            custom_theme = config.get("theme")
            theme_locale = custom_theme._vars.get("language")
            logging.debug(
                "Locale '%s' extracted from the custom theme: '%s'"
                % (theme_locale, custom_theme.name)
            )

        else:
            theme_locale = None
            logging.debug(
                "No locale found in theme configuration (or no custom theme set)"
            )

        # First prio: plugin locale
        if plugin_locale:
            locale_set = plugin_locale
            logging.debug("Using locale from plugin configuration: %s" % locale_set)
        # Second prio: theme locale
        elif theme_locale:
            locale_set = theme_locale
            logging.debug(
                "Locale not set in plugin. Fallback to theme configuration: %s"
                % locale_set
            )
        # Third prio is mkdocs locale (which might be added in the future)
        elif mkdocs_locale:
            locale_set = mkdocs_locale
            logging.debug("Using locale from mkdocs configuration: %s" % locale_set)
        else:
            locale_set = "en"
            logging.debug("No locale set. Fallback to: %s" % locale_set)

        # set locale also in plugin configuration
        self.config["locale"] = locale_set

        # Add pointers to support files for timeago.js
        if self.config.get("type") == "timeago":
            config["extra_javascript"] = ["js/timeago_mkdocs_material.js"] + config[
                "extra_javascript"
            ]
            config["extra_javascript"] = ["js/timeago.min.js"] + config[
                "extra_javascript"
            ]
            config["extra_css"] = ["css/timeago.css"] + config["extra_css"]

        return config
Example #9
0
class GitRevisionDateLocalizedPlugin(BasePlugin):
    """
    Mkdocs plugin to add revision date from Git.

    See https://www.mkdocs.org/user-guide/plugins
    """

    config_scheme = (
        ("fallback_to_build_date", config_options.Type(bool, default=False)),
        ("locale", config_options.Type(str, default=None)),
        ("type", config_options.Type(str, default="date")),
        ("timezone", config_options.Type(str, default="UTC")),
        ("exclude", config_options.Type(list, default=[])),
        ("enable_creation_date", config_options.Type(bool, default=False)),
        ("enabled", config_options.Type(bool, default=True)),
    )

    def on_config(self, config: config_options.Config, **kwargs) -> Dict[str, Any]:
        """
        Determine which locale to use.

        The config event is the first event called on build and
        is run immediately after the user configuration is loaded and validated.
        Any alterations to the config should be made here.
        https://www.mkdocs.org/user-guide/plugins/#on_config

        Args:
            config (dict): global configuration object

        Returns:
            dict: global configuration object
        """
        if not self.config.get('enabled'):
            return config
        
        self.util = Util(config=self.config)

        # Save last commit timestamp for entire site
        self.last_site_revision_timestamp = self.util.get_git_commit_timestamp(
            config.get('docs_dir')
        )

        # Get locale settings - might be added in future mkdocs versions
        # see: https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/issues/24
        mkdocs_locale = config.get("locale", None)

        # Get locale from plugin configuration
        plugin_locale = self.config.get("locale", None)

        # theme locale
        if "theme" in config and "locale" in config.get("theme"):
            custom_theme = config.get("theme")
            theme_locale = custom_theme._vars.get("locale")
            logging.debug(
                "Locale '%s' extracted from the custom theme: '%s'"
                % (theme_locale, custom_theme.name)
            )
        elif "theme" in config and "language" in config.get("theme"):
            custom_theme = config.get("theme")
            theme_locale = custom_theme._vars.get("language")
            logging.debug(
                "Locale '%s' extracted from the custom theme: '%s'"
                % (theme_locale, custom_theme.name)
            )

        else:
            theme_locale = None
            logging.debug(
                "No locale found in theme configuration (or no custom theme set)"
            )

        # First prio: plugin locale
        if plugin_locale:
            locale_set = plugin_locale
            logging.debug("Using locale from plugin configuration: %s" % locale_set)
        # Second prio: theme locale
        elif theme_locale:
            locale_set = theme_locale
            logging.debug(
                "Locale not set in plugin. Fallback to theme configuration: %s"
                % locale_set
            )
        # Third prio is mkdocs locale (which might be added in the future)
        elif mkdocs_locale:
            locale_set = mkdocs_locale
            logging.debug("Using locale from mkdocs configuration: %s" % locale_set)
        else:
            locale_set = "en"
            logging.debug("No locale set. Fallback to: %s" % locale_set)

        # set locale also in plugin configuration
        self.config["locale"] = locale_set

        # Add pointers to support files for timeago.js
        if self.config.get("type") == "timeago":
            config["extra_javascript"] = ["js/timeago_mkdocs_material.js"] + config[
                "extra_javascript"
            ]
            config["extra_javascript"] = ["js/timeago.min.js"] + config[
                "extra_javascript"
            ]
            config["extra_css"] = ["css/timeago.css"] + config["extra_css"]

        return config

    def on_page_markdown(
        self, markdown: str, page: Page, config: config_options.Config, files, **kwargs
    ) -> str:
        """
        Replace jinja2 tags in markdown and templates with the localized dates.

        The page_markdown event is called after the page's markdown is loaded
        from file and can be used to alter the Markdown source text.
        The meta- data has been stripped off and is available as page.meta
        at this point.

        https://www.mkdocs.org/user-guide/plugins/#on_page_markdown

        Args:
            markdown (str): Markdown source text of page as string
            page: mkdocs.nav.Page instance
            config: global configuration object
            site_navigation: global navigation object

        Returns:
            str: Markdown source text of page as string
        """
        if not self.config.get('enabled'):
            return markdown

        # Exclude pages specified in config
        excluded_pages = self.config.get("exclude", [])
        if exclude(page.file.src_path, excluded_pages):
            logging.debug("Excluding page " + page.file.src_path)
            return markdown

        # Retrieve git commit timestamp
        last_revision_timestamp = self.util.get_git_commit_timestamp(
                path=page.file.abs_src_path,
                is_first_commit=False,
        )

        # Last revision date
        revision_dates = self.util.get_date_formats_for_timestamp(last_revision_timestamp)
        revision_date = revision_dates[self.config["type"]]

        # timeago output is dynamic, which breaks when you print a page
        # This ensures fallback to type "iso_date"
        # controlled via CSS (see on_post_build() event)
        if self.config["type"] == "timeago":
            revision_date += revision_dates["iso_date"]

        # Add to page meta information, for developers
        # Include variants without the CSS <span> elements (raw date strings)
        page.meta["git_revision_date_localized"] = revision_date
        revision_dates_raw = self.util.get_date_formats_for_timestamp(last_revision_timestamp, add_spans=False)
        for date_type, date_string in revision_dates_raw.items():
            page.meta["git_revision_date_localized_raw_%s" % date_type] = date_string

        # Replace any occurances in markdown page
        markdown = re.sub(
            r"\{\{\s*git_revision_date_localized\s*\}\}",
            revision_date,
            markdown,
            flags=re.IGNORECASE,
        )

        # If creation date not enabled, return markdown
        # This is for speed: prevents another `git log` operation each file
        if not self.config.get("enable_creation_date"):
            return markdown
    
        # Retrieve git commit timestamp
        first_revision_timestamp = self.util.get_git_commit_timestamp(
            path=page.file.abs_src_path,
            is_first_commit=True,
        )

        # Creation date formats
        creation_dates = self.util.get_date_formats_for_timestamp(first_revision_timestamp)
        creation_date = creation_dates[self.config["type"]]

        # timeago output is dynamic, which breaks when you print a page
        # This ensures fallback to type "iso_date"
        # controlled via CSS (see on_post_build() event)
        if self.config["type"] == "timeago":
            creation_date += creation_dates["iso_date"]

        # Add to page meta information, for developers
        # Include variants without the CSS <span> elements (raw date strings)
        page.meta["git_creation_date_localized"] = creation_date
        creation_dates_raw = self.util.get_date_formats_for_timestamp(first_revision_timestamp, add_spans=False)
        for date_type, date_string in creation_dates_raw.items():
            page.meta["git_creation_date_localized_raw_%s" % date_type] = date_string

        # Replace any occurances in markdown page
        markdown = re.sub(
            r"\{\{\s*git_creation_date_localized\s*\}\}",
            creation_date,
            markdown,
            flags=re.IGNORECASE,
        )

        # Finally,
        # Also add site last updated information, for developers
        site_dates = self.util.get_date_formats_for_timestamp(self.last_site_revision_timestamp)
        site_date = site_dates[self.config["type"]]
        if self.config["type"] == "timeago":
            site_date += site_dates["iso_date"]
        page.meta["git_site_revision_date_localized"] = site_date
        site_dates_raw = self.util.get_date_formats_for_timestamp(self.last_site_revision_timestamp, add_spans=False)
        for date_type, date_string in site_dates_raw.items():
            page.meta["git_site_revision_date_localized_raw_%s" % date_type] = date_string

        return markdown

    def on_post_build(self, config: Dict[str, Any], **kwargs) -> None:
        """
        Run on post build.

        Adds the timeago assets to the build.
        """
        # Add timeago files:
        if self.config.get("type") == "timeago" and self.config.get('enabled'):
            files = [
                "js/timeago.min.js",
                "js/timeago_mkdocs_material.js",
                "css/timeago.css",
            ]
            for file in files:
                dest_file_path = os.path.join(config["site_dir"], file)
                src_file_path = os.path.join(HERE, file)
                assert os.path.exists(src_file_path)
                copy_file(src_file_path, dest_file_path)
Example #10
0
    def on_config(self, config: config_options.Config,
                  **kwargs) -> Dict[str, Any]:
        """
        Determine which locale to use.

        The config event is the first event called on build and
        is run immediately after the user configuration is loaded and validated.
        Any alterations to the config should be made here.
        https://www.mkdocs.org/user-guide/plugins/#on_config

        Args:
            config (dict): global configuration object

        Returns:
            dict: global configuration object
        """
        if not self.config.get('enabled'):
            return config

        assert self.config['type'] in [
            "date", "datetime", "iso_date", "iso_datetime", "timeago", "custom"
        ]

        self.util = Util(config=self.config)

        # Save last commit timestamp for entire site
        self.last_site_revision_timestamp = self.util.get_git_commit_timestamp(
            config.get('docs_dir'))

        # Get locale from plugin configuration
        plugin_locale = self.config.get("locale", None)

        # theme locale
        if "theme" in config and "locale" in config.get("theme"):
            custom_theme = config.get("theme")
            theme_locale = custom_theme._vars.get("locale")
            logging.debug("Locale '%s' extracted from the custom theme: '%s'" %
                          (theme_locale, custom_theme.name))
        elif "theme" in config and "language" in config.get("theme"):
            custom_theme = config.get("theme")
            theme_locale = custom_theme._vars.get("language")
            logging.debug("Locale '%s' extracted from the custom theme: '%s'" %
                          (theme_locale, custom_theme.name))

        else:
            theme_locale = None
            logging.debug(
                "No locale found in theme configuration (or no custom theme set)"
            )

        # First prio: plugin locale
        if plugin_locale:
            locale_set = plugin_locale
            logging.debug("Using locale from plugin configuration: %s" %
                          locale_set)
        # Second prio: theme locale
        elif theme_locale:
            locale_set = theme_locale
            logging.debug(
                "Locale not set in plugin. Fallback to theme configuration: %s"
                % locale_set)
        # Lastly, fallback is English
        else:
            locale_set = "en"
            logging.debug("No locale set. Fallback to: %s" % locale_set)

        # Validate locale
        locale_set = str(locale_set)
        assert len(
            locale_set
        ) == 2, "locale must be a 2 letter code, see https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes"

        # set locale also in plugin configuration
        self.config["locale"] = locale_set

        # Add pointers to support files for timeago.js
        if self.config.get("type") == "timeago":
            config["extra_javascript"] = ["js/timeago_mkdocs_material.js"
                                          ] + config["extra_javascript"]
            config["extra_javascript"] = ["js/timeago.min.js"
                                          ] + config["extra_javascript"]
            config["extra_css"] = ["css/timeago.css"] + config["extra_css"]

        # Compatibility with mkdocs-static-i18n
        plugins = [*OrderedDict(config["plugins"])]
        if "i18n" in plugins:
            if plugins.index("git-revision-date-localized") < plugins.index(
                    "i18n"):
                msg = "[git-revision-date-localized] should be defined after the i18n plugin in your mkdocs.yml file. "
                msg += "This is because i18n adds a 'locale' variable to markdown pages that this plugin supports."
                raise ConfigurationError(msg)

        return config
Example #11
0
class GitRevisionDateLocalizedPlugin(BasePlugin):
    """
    Mkdocs plugin to add revision date from Git.

    See https://www.mkdocs.org/user-guide/plugins
    """

    config_scheme = (
        ("fallback_to_build_date", config_options.Type(bool, default=False)),
        ("locale", config_options.Type(str, default=None)),
        ("type", config_options.Type(str, default="date")),
        ("custom_format", config_options.Type(str, default="%d. %B %Y")),
        ("timezone", config_options.Type(str, default="UTC")),
        ("exclude", config_options.Type(list, default=[])),
        ("enable_creation_date", config_options.Type(bool, default=False)),
        ("enabled", config_options.Type(bool, default=True)),
    )

    def on_config(self, config: config_options.Config,
                  **kwargs) -> Dict[str, Any]:
        """
        Determine which locale to use.

        The config event is the first event called on build and
        is run immediately after the user configuration is loaded and validated.
        Any alterations to the config should be made here.
        https://www.mkdocs.org/user-guide/plugins/#on_config

        Args:
            config (dict): global configuration object

        Returns:
            dict: global configuration object
        """
        if not self.config.get('enabled'):
            return config

        assert self.config['type'] in [
            "date", "datetime", "iso_date", "iso_datetime", "timeago", "custom"
        ]

        self.util = Util(config=self.config)

        # Save last commit timestamp for entire site
        self.last_site_revision_timestamp = self.util.get_git_commit_timestamp(
            config.get('docs_dir'))

        # Get locale from plugin configuration
        plugin_locale = self.config.get("locale", None)

        # theme locale
        if "theme" in config and "locale" in config.get("theme"):
            custom_theme = config.get("theme")
            theme_locale = custom_theme._vars.get("locale")
            logging.debug("Locale '%s' extracted from the custom theme: '%s'" %
                          (theme_locale, custom_theme.name))
        elif "theme" in config and "language" in config.get("theme"):
            custom_theme = config.get("theme")
            theme_locale = custom_theme._vars.get("language")
            logging.debug("Locale '%s' extracted from the custom theme: '%s'" %
                          (theme_locale, custom_theme.name))

        else:
            theme_locale = None
            logging.debug(
                "No locale found in theme configuration (or no custom theme set)"
            )

        # First prio: plugin locale
        if plugin_locale:
            locale_set = plugin_locale
            logging.debug("Using locale from plugin configuration: %s" %
                          locale_set)
        # Second prio: theme locale
        elif theme_locale:
            locale_set = theme_locale
            logging.debug(
                "Locale not set in plugin. Fallback to theme configuration: %s"
                % locale_set)
        # Lastly, fallback is English
        else:
            locale_set = "en"
            logging.debug("No locale set. Fallback to: %s" % locale_set)

        # Validate locale
        locale_set = str(locale_set)
        assert len(
            locale_set
        ) == 2, "locale must be a 2 letter code, see https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes"

        # set locale also in plugin configuration
        self.config["locale"] = locale_set

        # Add pointers to support files for timeago.js
        if self.config.get("type") == "timeago":
            config["extra_javascript"] = ["js/timeago_mkdocs_material.js"
                                          ] + config["extra_javascript"]
            config["extra_javascript"] = ["js/timeago.min.js"
                                          ] + config["extra_javascript"]
            config["extra_css"] = ["css/timeago.css"] + config["extra_css"]

        # Compatibility with mkdocs-static-i18n
        plugins = [*OrderedDict(config["plugins"])]
        if "i18n" in plugins:
            if plugins.index("git-revision-date-localized") < plugins.index(
                    "i18n"):
                msg = "[git-revision-date-localized] should be defined after the i18n plugin in your mkdocs.yml file. "
                msg += "This is because i18n adds a 'locale' variable to markdown pages that this plugin supports."
                raise ConfigurationError(msg)

        return config

    def on_page_markdown(self, markdown: str, page: Page,
                         config: config_options.Config, files,
                         **kwargs) -> str:
        """
        Replace jinja2 tags in markdown and templates with the localized dates.

        The page_markdown event is called after the page's markdown is loaded
        from file and can be used to alter the Markdown source text.
        The meta- data has been stripped off and is available as page.meta
        at this point.

        https://www.mkdocs.org/user-guide/plugins/#on_page_markdown

        Args:
            markdown (str): Markdown source text of page as string
            page: mkdocs.nav.Page instance
            config: global configuration object
            site_navigation: global navigation object

        Returns:
            str: Markdown source text of page as string
        """
        if not self.config.get('enabled'):
            return markdown

        # Exclude pages specified in config
        excluded_pages = self.config.get("exclude", [])
        if exclude(page.file.src_path, excluded_pages):
            logging.debug("Excluding page " + page.file.src_path)
            return markdown

        # Find the locale

        # First prio is use mkdocs-static-i18n locale if set
        try:
            locale = page.locale

        except AttributeError:
            locale = None

        # Second prio is a frontmatter variable 'locale' set in the markdown
        if not locale:
            if "locale" in page.meta:
                locale = page.meta['locale']

        # Finally, if no page locale set, we take the locale determined on_config()
        if not locale:
            locale = self.config.get("locale")

        # MkDocs supports 2-letter and 5-letter locales
        # https://www.mkdocs.org/user-guide/localizing-your-theme/#supported-locales
        # We need the 2 letter variant
        if len(locale) == 5:
            locale = locale[:2]
        assert len(
            locale
        ) == 2, "locale must be a 2 letter code, see https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes"

        # Retrieve git commit timestamp
        last_revision_timestamp = self.util.get_git_commit_timestamp(
            path=page.file.abs_src_path,
            is_first_commit=False,
        )

        # Last revision date
        revision_dates = self.util.get_date_formats_for_timestamp(
            last_revision_timestamp, locale=locale, add_spans=True)
        revision_date = revision_dates[self.config["type"]]

        # timeago output is dynamic, which breaks when you print a page
        # This ensures fallback to type "iso_date"
        # controlled via CSS (see on_post_build() event)
        if self.config["type"] == "timeago":
            revision_date += revision_dates["iso_date"]

        # Add to page meta information, for developers
        # Include variants without the CSS <span> elements (raw date strings)
        page.meta["git_revision_date_localized"] = revision_date
        revision_dates_raw = self.util.get_date_formats_for_timestamp(
            last_revision_timestamp, locale=locale, add_spans=False)
        for date_type, date_string in revision_dates_raw.items():
            page.meta["git_revision_date_localized_raw_%s" %
                      date_type] = date_string

        # Replace any occurances in markdown page
        markdown = re.sub(
            r"\{\{\s*git_revision_date_localized\s*\}\}",
            revision_date,
            markdown,
            flags=re.IGNORECASE,
        )

        # Also add site last updated information, for developers
        site_dates = self.util.get_date_formats_for_timestamp(
            self.last_site_revision_timestamp, locale=locale, add_spans=True)
        site_date = site_dates[self.config["type"]]
        if self.config["type"] == "timeago":
            site_date += site_dates["iso_date"]
        page.meta["git_site_revision_date_localized"] = site_date
        site_dates_raw = self.util.get_date_formats_for_timestamp(
            self.last_site_revision_timestamp, locale=locale, add_spans=False)
        for date_type, date_string in site_dates_raw.items():
            page.meta["git_site_revision_date_localized_raw_%s" %
                      date_type] = date_string

        # Replace any occurances in markdown page
        markdown = re.sub(
            r"\{\{\s*git_site_revision_date_localized\s*\}\}",
            site_date,
            markdown,
            flags=re.IGNORECASE,
        )

        # If creation date not enabled, return markdown
        # This is for speed: prevents another `git log` operation each file
        if not self.config.get("enable_creation_date"):
            return markdown

        # Retrieve git commit timestamp
        first_revision_timestamp = self.util.get_git_commit_timestamp(
            path=page.file.abs_src_path,
            is_first_commit=True,
        )

        # Creation date formats
        creation_dates = self.util.get_date_formats_for_timestamp(
            first_revision_timestamp, locale=locale, add_spans=True)
        creation_date = creation_dates[self.config["type"]]

        # timeago output is dynamic, which breaks when you print a page
        # This ensures fallback to type "iso_date"
        # controlled via CSS (see on_post_build() event)
        if self.config["type"] == "timeago":
            creation_date += creation_dates["iso_date"]

        # Add to page meta information, for developers
        # Include variants without the CSS <span> elements (raw date strings)
        page.meta["git_creation_date_localized"] = creation_date
        creation_dates_raw = self.util.get_date_formats_for_timestamp(
            first_revision_timestamp, locale=locale, add_spans=False)
        for date_type, date_string in creation_dates_raw.items():
            page.meta["git_creation_date_localized_raw_%s" %
                      date_type] = date_string

        # Replace any occurances in markdown page
        markdown = re.sub(
            r"\{\{\s*git_creation_date_localized\s*\}\}",
            creation_date,
            markdown,
            flags=re.IGNORECASE,
        )

        return markdown

    def on_post_build(self, config: Dict[str, Any], **kwargs) -> None:
        """
        Run on post build.

        Adds the timeago assets to the build.
        """
        # Add timeago files:
        if self.config.get("type") == "timeago" and self.config.get('enabled'):
            files = [
                "js/timeago.min.js",
                "js/timeago_mkdocs_material.js",
                "css/timeago.css",
            ]
            for file in files:
                dest_file_path = os.path.join(config["site_dir"], file)
                src_file_path = os.path.join(HERE, file)
                assert os.path.exists(src_file_path)
                copy_file(src_file_path, dest_file_path)
Example #12
0
def test_tags_are_replaced(tmp_path, mkdocs_file):
    """
    Make sure the {{ }} tags are replaced properly.
    """
    testproject_path = setup_clean_mkdocs_folder(
        mkdocs_yml_path=f"tests/fixtures/{mkdocs_file}", output_path=tmp_path)
    setup_commit_history(testproject_path)
    result = build_docs_setup(testproject_path)
    assert result.exit_code == 0, "'mkdocs build' command failed"

    plugin_config = get_plugin_config_from_mkdocs(
        str(testproject_path / "mkdocs.yml"))
    tags_file = testproject_path / "site/page_with_tag/index.html"
    contents = tags_file.read_text(encoding="utf8")

    # validate the build
    validate_build(testproject_path, plugin_config=plugin_config)

    if plugin_config.get("enabled") == False:
        return True

    if plugin_config.get("type") == "timeago":
        pytest.skip("Not necessary to test the JS library")

    # Make sure count_commits() works
    # We created 8 commits in setup_commit_history()
    with working_directory(testproject_path):
        u = Util()
        assert commit_count(u._get_repo("docs/page_with_tag.md")) == 8

    # the revision date was in 'setup_commit_history' was set to 1642911026 (Sun Jan 23 2022 04:10:26 GMT+0000)
    # Assert {{ git_revision_date_localized }} is replaced
    date_formats_revision_date = Util()._date_formats(
        1642911026,
        locale=plugin_config.get("locale"),
        time_zone=plugin_config.get("timezone"),
        custom_format=plugin_config.get("custom_format"))
    for k, v in date_formats_revision_date.items():
        assert v is not None

    date = date_formats_revision_date.get(plugin_config.get('type'))
    assert re.search(rf"{date}\<\/span.+", contents)

    # The last site revision was set in setup_commit_history to 1643911026 (Thu Feb 03 2022 17:57:06 GMT+0000)
    # Assert {{ git_site_revision_date_localized }} is replaced
    date_formats_revision_date = Util()._date_formats(
        1643911026,
        locale=plugin_config.get("locale"),
        time_zone=plugin_config.get("timezone"),
        custom_format=plugin_config.get("custom_format"))
    for k, v in date_formats_revision_date.items():
        assert v is not None
    date = date_formats_revision_date.get(plugin_config.get('type'))
    assert re.search(rf"{date}\<\/span.+", contents)

    # Note {{ git_creation_date_localized }} is only replaced when configured in the config
    if plugin_config.get("enable_creation_date"):
        # The creation of page_with_tag.md was set in setup_commit_history to 1500854705 ( Mon Jul 24 2017 00:05:05 GMT+0000 )
        date_formats_revision_date = Util()._date_formats(
            1500854705,
            locale=plugin_config.get("locale"),
            time_zone=plugin_config.get("timezone"),
            custom_format=plugin_config.get("custom_format"))
        for k, v in date_formats_revision_date.items():
            assert v is not None
        date = date_formats_revision_date.get(plugin_config.get('type'))
        assert re.search(rf"{date}\<\/span.+", contents)