Example #1
0
def load_config(filename='mkdocs.yml', options=None):
    options = options or {}
    if 'config' in options:
        filename = options.pop('config')
    if not os.path.exists(filename):
        raise ConfigurationError("Config file '%s' does not exist." % filename)
    with open(filename, 'r') as fp:
        user_config = yaml.load(fp)
        if not isinstance(user_config, dict):
            raise ConfigurationError("The mkdocs.yml file is invalid. See http://www.mkdocs.org/user-guide/configuration/ for more information.")
    user_config.update(options)
    return validate_config(user_config)
Example #2
0
    def on_config(self, config: Config, **kwargs) -> Config:
        """
        Hook for the [`on_config` event](https://www.mkdocs.org/user-guide/plugins/#on_config).

        In this hook, we instantiate our [`MkdocstringsExtension`][mkdocstrings.extension.MkdocstringsExtension]
        and add it to the list of Markdown extensions used by `mkdocs`.

        We pass this plugin's configuration dictionary to the extension when instantiating it (it will need it
        later when processing markdown to get handlers and their global configurations).
        """
        if not config["site_url"]:
            raise ConfigurationError(
                "mkdocstrings.plugin: configuration item 'site_url' is required for cross-references"
            )

        log.debug("mkdocstrings.plugin: Adding extension to the list")

        theme_name = None
        if config["theme"].name is None:
            theme_name = os.path.dirname(config["theme"].dirs[0])
        else:
            theme_name = config["theme"].name

        extension_config = {
            "theme_name": theme_name,
            "mdx": config["markdown_extensions"],
            "mdx_configs": config["mdx_configs"],
            "mkdocstrings": self.config,
        }

        self.mkdocstrings_extension = MkdocstringsExtension(
            config=extension_config)
        config["markdown_extensions"].append(self.mkdocstrings_extension)
        return config
Example #3
0
def load_config(filename='mkdocs.yml', options=None):
    options = options or {}
    if 'config' in options:
        filename = options['config']
    if not os.path.exists(filename):
        raise ConfigurationError("Config file '%s' does not exist." % filename)
    with open(filename, 'r') as fp:
        user_config = yaml.load(fp)
    user_config.update(options)
    return validate_config(user_config)
Example #4
0
    def on_pre_build(self, config, **kwargs):
        """Validates plugin user configuration input

        Args:
            config (dict): plugin configuration
        """
        if self.config.get("toc_depth", 0) > 6:
            raise ConfigurationError(
                "toc_depth is set to %s, but max is 6. Update plugin settings in mkdocs.yml."
                % self.config.get("toc_depth"))
Example #5
0
def load_config(config_file=None, **kwargs):

    options = dict((k, v) for k, v in kwargs.items() if v is not None)

    if config_file is None:
        config_file = 'mkdocs.yml'
        if os.path.exists(config_file):
            config_file = open(config_file, 'rb')
        else:
            raise ConfigurationError(
                "Config file '{0}' does not exist.".format(config_file))

    options['config'] = config_file

    user_config = utils.yaml_load(config_file)

    if not isinstance(user_config, dict):
        raise ConfigurationError(
            "The mkdocs.yml file is invalid. For more information see: "
            "http://www.mkdocs.org/user-guide/configuration/ ({0})".format(
                user_config))

    user_config.update(options)
    return validate_config(user_config)
    def on_config(self, config):
        """

        See https://www.mkdocs.org/user-guide/plugins/#on_config
        Args:
            config

        Returns:
            Config
        """

        plugins = [p for p in config.get("plugins")]

        for post_load_plugin in ["macros", "markdownextradata"]:
            if post_load_plugin in plugins:
                if plugins.index("table-reader") > plugins.index(post_load_plugin):
                    raise ConfigurationError(f"[table-reader]: Incompatible plugin order: Define 'table-reader' before '{post_load_plugin}' in your mkdocs.yml.")
    def on_config(self, config):
        """

        See https://www.mkdocs.org/user-guide/plugins/#on_config
        Args:
            config

        Returns:
            Config
        """

        plugins = [p for p in config.get("plugins")]

        if "macros" in plugins:
            if plugins.index("table-reader") > plugins.index("macros"):
                raise ConfigurationError(
                    "[table-reader]: Incompatible plugin order: Define 'table-reader' before 'macros' in your mkdocs.yml."
                )
Example #8
0
def pages_compat_shim(original_pages):
    """
    Support legacy pages configuration

    Re-write the pages config fron MkDocs <=0.12 to match the
    new nested structure added in 0.13.

    Given a pages configuration in the old style of:

        pages:
        - ['index.md', 'Home']
        - ['user-guide/writing-your-docs.md', 'User Guide']
        - ['user-guide/styling-your-docs.md', 'User Guide']
        - ['about/license.md', 'About', 'License']
        - ['about/release-notes.md', 'About']
        - ['help/contributing.md', 'Help', 'Contributing']
        - ['support.md']
        - ['cli.md', 'CLI Guide']

    Rewrite it to look like:

        pages:
        - Home: index.md
        - User Guide:
            - user-guide/writing-your-docs.md
            - user-guide/styling-your-docs.md
        - About:
            - License: about/license.md
            - about/release-notes.md
        - Help:
            - Contributing: about/contributing.md
        - support.md
        - CLI Guide: cli.md

    TODO: Remove in 1.0
    """

    log.warning("The pages config in the mkdocs.yml uses the deprecated "
                "structure. This will be removed in the next release of "
                "MkDocs. See for details on updating: "
                "http://www.mkdocs.org/about/release-notes/")

    new_pages = []

    for config_line in original_pages:

        if isinstance(config_line, six.string_types):
            config_line = [
                config_line,
            ]

        if len(config_line) not in (1, 2, 3):
            msg = ("Line in 'page' config contained {0} items. In Line {1}. "
                   "Expected 1, 2 or 3 strings.".format(
                       config_line, len(config_line)))
            raise ConfigurationError(msg)

        # First we need to pad out the config line as it could contain
        # 1-3 items.
        path, category, title = (list(config_line) + [None, None])[:3]

        if len(new_pages) > 0:
            # Get the previous top-level page so we can see if the category
            # matches up with the one we have now.
            prev_cat, subpages = next(iter(new_pages[-1].items()))
        else:
            # We are on the first page
            prev_cat, subpages = None, []

        # If the category is different, add a new top level category. If the
        # previous category is None, the it's another top level one too.
        if prev_cat is None or prev_cat != category:
            subpages = []
            new_pages.append({category: subpages})

        # Add the current page to the determined category.
        subpages.append({title: path})

    # We need to do a bit of cleaning up to match the new structure. In the
    # above example, pages can either be `- file.md` or `- Title: file.md`.
    # For pages without a title we currently have `- None: file.md` - so we
    # need to remove those Nones by changing from a dict to just a string with
    # the path.
    for i, category in enumerate(new_pages):

        # Categories are a dictionary with one key as the name and the value
        # is a list of pages. So, grab that from the dict.
        category, pages = next(iter(category.items()))

        # If we only have one page, then we can assume it is a top level
        # category and no further nesting is required unless that single page
        # has a title itself,
        if len(pages) == 1:
            title, path = pages.pop().popitem()
            # If we have a title, it should be a sub page
            if title is not None:
                pages.append({title: path})
            # if we have a category, but no title it should be a top-level page
            elif category is not None:
                new_pages[i] = {category: path}
            # if we have no category or title, it must be a top level page with
            # an atomatic title.
            else:
                new_pages[i] = path
        else:
            # We have more than one page, so the category is valid. We just
            # need to iterate through and convert any {None: path} dicts to
            # be just the path string.
            for j, page in enumerate(pages):
                title, path = page.popitem()
                if title:
                    pages[j] = {title: path}
                else:
                    pages[j] = path

    return new_pages
Example #9
0
def validate_config(user_config):
    config = DEFAULT_CONFIG.copy()

    theme_in_config = 'theme' in user_config

    config.update(user_config)

    if not config['site_name']:
        raise ConfigurationError("Config must contain 'site_name' setting.")

    # Validate that the docs_dir and site_dir don't contain the
    # other as this will lead to copying back and forth on each
    # and eventually make a deep nested mess.
    abs_site_dir = os.path.abspath(config['site_dir'])
    abs_docs_dir = os.path.abspath(config['docs_dir'])
    if abs_docs_dir.startswith(abs_site_dir):
        raise ConfigurationError(
            "The 'docs_dir' can't be within the 'site_dir'.")
    elif abs_site_dir.startswith(abs_docs_dir):
        raise ConfigurationError(
            "The 'site_dir' can't be within the 'docs_dir'.")

    # If not specified, then the 'pages' config simply includes all
    # markdown files in the docs dir, without generating any header items
    # for them.
    pages = []
    extra_css = []
    extra_javascript = []
    for (dirpath, dirnames, filenames) in os.walk(config['docs_dir']):
        for filename in sorted(filenames):
            fullpath = os.path.join(dirpath, filename)
            relpath = os.path.relpath(fullpath, config['docs_dir'])

            if utils.is_markdown_file(filename):
                # index pages should always be the first listed page.
                if os.path.splitext(relpath)[0] == 'index':
                    pages.insert(0, relpath)
                else:
                    pages.append(relpath)
            elif utils.is_css_file(filename):
                extra_css.append(relpath)
            elif utils.is_javascript_file(filename):
                extra_javascript.append(relpath)

    if config['pages'] is None:
        config['pages'] = pages

    if config['extra_css'] is None:
        config['extra_css'] = extra_css

    if config['extra_javascript'] is None:
        config['extra_javascript'] = extra_javascript

    package_dir = os.path.dirname(__file__)
    theme_dir = [os.path.join(package_dir, 'themes', config['theme'])]

    if config['theme_dir'] is not None:
        # If the user has given us a custom theme but not a
        # builtin theme name then we don't want to merge them.
        if not theme_in_config:
            theme_dir = []
        theme_dir.insert(0, config['theme_dir'])

    config['theme_dir'] = theme_dir

    if config['repo_url'] is not None and config['repo_name'] is None:
        repo_host = urlparse(config['repo_url']).netloc.lower()
        if repo_host == 'github.com':
            config['repo_name'] = 'GitHub'
        elif repo_host == 'bitbucket.org':
            config['repo_name'] = 'Bitbucket'
        else:
            config['repo_name'] = repo_host.split('.')[0].title()

    if config['include_next_prev'] is None:
        config['include_next_prev'] = len(config['pages']) > 1

    if config['include_nav'] is None:
        config['include_nav'] = len(config['pages']) > 1

    # To Do:

    # The docs dir must exist.
    # The theme dir must exist.
    # Ensure 'theme' is one of 'mkdocs', 'readthedocs', 'custom'
    # A homepage 'index' must exist.
    # The theme 'base.html' file must exist.
    # Cannot set repo_name without setting repo_url.
    # Cannot set 'include_next_prev: true' when only one page exists.
    # Cannot set 'include_nav: true' when only one page exists.
    # Error if any config keys provided that are not in the DEFAULT_CONFIG.

    return config
Example #10
0
def validate_config(user_config):
    config = DEFAULT_CONFIG.copy()

    theme_in_config = 'theme' in user_config

    config.update(user_config)

    if not config['site_name']:
        raise ConfigurationError("Config must contain 'site_name' setting.")

    # Validate that the docs_dir and site_dir don't contain the
    # other as this will lead to copying back and forth on each
    # and eventually make a deep nested mess.
    abs_site_dir = os.path.abspath(config['site_dir'])
    abs_docs_dir = os.path.abspath(config['docs_dir'])
    if abs_docs_dir.startswith(abs_site_dir):
        raise ConfigurationError(
            "The 'docs_dir' can't be within the 'site_dir'.")
    elif abs_site_dir.startswith(abs_docs_dir):
        raise ConfigurationError(
            "The 'site_dir' can't be within the 'docs_dir'.")

    # If not specified, then the 'pages' config simply includes all
    # markdown files in the docs dir, without generating any header items
    # for them.
    pages = []
    extra_css = []
    extra_javascript = []
    for (dirpath, _, filenames) in os.walk(config['docs_dir']):
        for filename in sorted(filenames):
            fullpath = os.path.join(dirpath, filename)
            relpath = os.path.normpath(
                os.path.relpath(fullpath, config['docs_dir']))

            if utils.is_markdown_file(filename):
                # index pages should always be the first listed page.
                if os.path.splitext(relpath)[0] == 'index':
                    pages.insert(0, relpath)
                else:
                    pages.append(relpath)
            elif utils.is_css_file(filename):
                extra_css.append(relpath)
            elif utils.is_javascript_file(filename):
                extra_javascript.append(relpath)

    if config['pages'] is None:
        config['pages'] = pages
    else:
        """
        If the user has provided the pages config, then iterate through and
        check for Windows style paths. If they are found, output a warning
        and continue.
        """
        for page_config in config['pages']:
            if isinstance(page_config, str):
                path = page_config
            elif len(page_config) in (1, 2, 3):
                path = page_config[0]

            if ntpath.sep in path:
                log.warning("The config path contains Windows style paths (\\ "
                            " backward slash) and will have comparability "
                            "issues if it is used on another platform.")
                break

    if config['extra_css'] is None:
        config['extra_css'] = extra_css

    if config['extra_javascript'] is None:
        config['extra_javascript'] = extra_javascript

    package_dir = os.path.dirname(__file__)
    theme_dir = [
        os.path.join(package_dir, 'themes', config['theme']),
    ]

    if config['theme_dir'] is not None:
        # If the user has given us a custom theme but not a
        # builtin theme name then we don't want to merge them.
        if not theme_in_config:
            theme_dir = []
        theme_dir.insert(0, config['theme_dir'])

    config['theme_dir'] = theme_dir

    # Add the search assets to the theme_dir, this means that
    # they will then we copied into the output directory but can
    # be overwritten by themes if needed.
    config['theme_dir'].append(os.path.join(package_dir, 'assets', 'search'))

    if config['repo_url'] is not None and config['repo_name'] is None:
        repo_host = urlparse(config['repo_url']).netloc.lower()
        if repo_host == 'github.com':
            config['repo_name'] = 'GitHub'
        elif repo_host == 'bitbucket.org':
            config['repo_name'] = 'Bitbucket'
        else:
            config['repo_name'] = repo_host.split('.')[0].title()

    if config['include_next_prev'] is None:
        config['include_next_prev'] = len(config['pages']) > 1

    if config['include_nav'] is None:
        config['include_nav'] = len(config['pages']) > 1

    # To Do:

    # The docs dir must exist.
    # The theme dir must exist.
    # Ensure 'theme' is one of 'mkdocs', 'readthedocs', 'custom'
    # A homepage 'index' must exist.
    # The theme 'base.html' file must exist.
    # Cannot set repo_name without setting repo_url.
    # Cannot set 'include_next_prev: true' when only one page exists.
    # Cannot set 'include_nav: true' when only one page exists.
    # Error if any config keys provided that are not in the DEFAULT_CONFIG.

    return config
Example #11
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 #12
0
 def check_position(plugin, plugins):
     if plugin in plugins:
         if plugins.index("enumerate-headings") < plugins.index(plugin):
             raise ConfigurationError(
                 "[enumerate-headings-plugin] enumerate-headings should be defined after the %s plugin in your mkdocs.yml file"
                 % plugin)