def _shard_version(cls, path: str):
     file_path = os.path.join(path, "shard.yml")
     if os.path.isfile(file_path):
         with open(file_path, "rb") as f:
             m = re.search(rb"^version: *([\S+]+)",
                           f.read(),
                           flags=re.MULTILINE)
         if not m:
             raise PluginError(f"`version:` not found in {file_path!r}")
         return m[1].decode()
     if not path:
         raise PluginError(f"'shard.yml' not found anywhere above {path!r}")
     return cls._shard_version(os.path.dirname(path))
Exemple #2
0
    def on_files(self, files, config):
        if not self.config['version_selector']:
            return files

        try:
            theme_dir = get_theme_dir(config['theme'].name)
        except ValueError:
            return files

        for path, prop in [('css', 'css'), ('js', 'javascript')]:
            cfg_value = self.config[prop + '_dir']
            srcdir = os.path.join(theme_dir, path)
            destdir = os.path.join(config['site_dir'], cfg_value)

            extra_kind = 'extra_' + prop
            norm_extras = [os.path.normpath(i) for i in config[extra_kind]]
            for f in os.listdir(srcdir):
                relative_dest = os.path.join(cfg_value, f)
                if relative_dest in norm_extras:
                    raise PluginError(
                        '{!r} is already included in {!r}'.format(
                            relative_dest, extra_kind))

                files.append(File(f, srcdir, destdir, False))
                config[extra_kind].append(relative_dest)
        return files
    def on_post_page(self, output_content: str, page: Page,
                     config: Config) -> None:
        if not self.config['enabled']:
            return

        use_directory_urls = config.data["use_directory_urls"]

        # Optimization: only parse links and headings
        # li, sup are used for footnotes
        strainer = SoupStrainer(
            ('a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'sup'))

        content = output_content if self.config[
            'validate_rendered_template'] else page.content
        soup = BeautifulSoup(content, 'lxml', parse_only=strainer)

        all_element_ids = set(tag['id'] for tag in soup.select('[id]'))
        all_element_ids.add('')  # Empty anchor is commonly used, but not real
        for a in soup.find_all('a', href=True):
            url = a['href']

            url_status = self.get_url_status(url, page.file.src_path,
                                             all_element_ids, self.files,
                                             use_directory_urls)

            if self.bad_url(url_status) is True:
                error = f'invalid url - {url} [{url_status}] [{page.file.src_path}]'

                is_error = self.is_error(self.config, url, url_status)
                if self.config['raise_error'] and is_error:
                    raise PluginError(error)
                elif is_error:
                    print(error)
Exemple #4
0
    def _process_block(
            self,
            identifier: str,
            yaml_block: str,
            heading_level: int = 0) -> Tuple[str, Sequence[Element]]:
        """Process an autodoc block.

        Arguments:
            identifier: The identifier of the object to collect and render.
            yaml_block: The YAML configuration.
            heading_level: Suggested level of the the heading to insert (0 to ignore).

        Raises:
            PluginError: When something wrong happened during collection.
            TemplateNotFound: When a template used for rendering could not be found.

        Returns:
            Rendered HTML and the list of heading elements encoutered.
        """
        config = yaml.safe_load(yaml_block) or {}
        handler_name = self._handlers.get_handler_name(config)

        log.debug(f"Using handler '{handler_name}'")
        handler_config = self._handlers.get_handler_config(handler_name)
        handler = self._handlers.get_handler(handler_name, handler_config)

        selection, rendering = get_item_configs(handler_config, config)
        if heading_level:
            rendering = ChainMap(
                rendering, {"heading_level": heading_level})  # like setdefault

        log.debug("Collecting data")
        try:
            data: CollectorItem = handler.collector.collect(
                identifier, selection)
        except CollectionError as exception:
            log.error(str(exception))
            if PluginError is SystemExit:  # When MkDocs 1.2 is sufficiently common, this can be dropped.
                log.error(
                    f"Error reading page '{self._autorefs.current_page}':")
            raise PluginError(
                f"Could not collect '{identifier}'") from exception

        if not self._updated_env:
            log.debug("Updating renderer's env")
            handler.renderer._update_env(
                self.md, self._config)  # noqa: W0212 (protected member OK)
            self._updated_env = True

        log.debug("Rendering templates")
        try:
            rendered = handler.renderer.render(data, rendering)
        except TemplateNotFound as exc:
            theme_name = self._config["theme_name"]
            log.error(
                f"Template '{exc.name}' not found for '{handler_name}' handler and theme '{theme_name}'.",
            )
            raise

        return (rendered, handler.renderer.get_headings())
Exemple #5
0
def read_source(self, config):
    self.eva = None
    try:
        with open(self.file.abs_src_path,
                  "r",
                  encoding="utf-8-sig",
                  errors="strict") as f:
            source = f.read()
    except OSError:
        log.error(f"File not found: {self.file.src_path}")
        raise
    except ValueError:
        log.error(f"Encoding error reading file: {self.file.src_path}")
        raise

    m = YAML_RE.match(source)
    if m:
        try:
            data = yaml.load(m.group(1), SafeLoader)
            if not isinstance(data, dict):
                data = {}
        except Exception as exc:
            raise BuildError(f"Page's YAML metadata is malformed: {exc}")

        try:
            self.eva = config["meta_model_class"](**data)
        except ValidationError as exc:
            raise PluginError(
                f"Deserializing {self} page's meta failed with the following errors: {exc}"
            )

    self.markdown = source
    self.title = self.eva.title
Exemple #6
0
 def crystal_src(self) -> str:
     out = subprocess.check_output(["crystal", "env", "CRYSTAL_PATH"],
                                   text=True).rstrip()
     for path in out.split(os.pathsep):
         if os.path.isfile(os.path.join(path, "prelude.cr")):
             return os.path.relpath(path)
     raise PluginError(
         f"Crystal sources not found anywhere in CRYSTAL_PATH={out!r}")
Exemple #7
0
 def _shard_version(cls, path: str) -> str:
     file_path = _find_above(path, "shard.yml")
     with open(file_path, "rb") as f:
         m = re.search(rb"^version: *([\S+]+)",
                       f.read(),
                       flags=re.MULTILINE)
     if not m:
         raise PluginError(f"`version:` not found in {file_path!r}")
     return m[1].decode()
Exemple #8
0
def _find_above(path: str, filename: str) -> str:
    orig_path = path
    while path:
        file_path = os.path.join(path, filename)
        if os.path.isfile(file_path):
            return file_path
        path = os.path.dirname(path)
    raise PluginError(
        f"{filename!r} not found anywhere above {os.path.abspath(orig_path)!r}"
    )
 def substitute(self, location: DocLocation) -> str:
     data = {
         "file": location.filename[len(self.src_path):],
         "line": location.line
     }
     try:
         return self.dest_url.format_map(collections.ChainMap(
             data, self))  # type: ignore
     except KeyError as e:
         raise PluginError(
             f"The source_locations template {self.dest_url!r} did not resolve correctly: {e}"
         )
Exemple #10
0
    def on_files(self, files: Files, config: Config) -> Files:
        self._dir = tempfile.TemporaryDirectory(prefix="mkdocs_gen_files_")

        with editor.FilesEditor(files, config, self._dir.name) as ed:
            for file_name in self.config["scripts"]:
                try:
                    runpy.run_path(file_name)
                except SystemExit as e:
                    if e.code:
                        raise PluginError(f"Script {file_name!r} caused {e!r}")

        self._edit_paths = dict(ed.edit_paths)
        return ed.files
Exemple #11
0
 def root(self) -> "DocRoot":
     """The top-level namespace, represented as a fake module."""
     try:
         with self._proc:
             root = inventory.read(self._proc.stdout)
         root.__class__ = DocRoot
         root.source_locations = self._source_locations
         return root
     finally:
         if self._proc.returncode:
             cmd = " ".join(shlex.quote(arg) for arg in self._proc.args)
             raise PluginError(
                 f"Command `{cmd}` exited with status {self._proc.returncode}"
             )
 def root(self) -> "DocRoot":
     """The top-level namespace, represented as a fake module."""
     try:
         with self._proc:
             data = json.load(self._proc.stdout)
         data["program"]["full_name"] = ""
         root = DocRoot(data["program"], None, None)
         root.source_locations = self._source_locations
         return root
     finally:
         if self._proc.returncode:
             cmd = " ".join(shlex.quote(arg) for arg in self._proc.args)
             raise PluginError(
                 f"Command `{cmd}` exited with status {self._proc.returncode}"
             )
Exemple #13
0
    def on_config(self, config):
        self.context = {}
        config["version"] = self.config["version"]
        self.session = Session(db.engine)
        db.create_db_and_tables()
        self.pages = []
        self.env = config["theme"].get_env()
        self.env.filters["b64encode"] = escapeb64
        self.env.filters["bom_json"] = self.bom_json
        self.env.filters["md_table"] = self.md_table
        self.env.filters["yes_no"] = self.yes_no
        self.env.filters["slugify"] = slugify

        try:
            config["meta_model_class"] = import_string(self.config["meta_model_class"])
        except ImportError as exc:
            raise PluginError(
                f"Meta Model Class {self.config['meta_model_class']} could not be imported. {exc}"
            ) from exc

        return config
Exemple #14
0
class BuildTests(PathAssertionMixin, unittest.TestCase):

    def assert_mock_called_once(self, mock):
        """assert that the mock was called only once.

        The `mock.assert_called_once()` method was added in PY36.
        TODO: Remove this when PY35 support is dropped.
        """
        try:
            mock.assert_called_once()
        except AttributeError:
            if not mock.call_count == 1:
                msg = ("Expected '%s' to have been called once. Called %s times." %
                       (mock._mock_name or 'mock', self.call_count))
                raise AssertionError(msg)

    def _get_env_with_null_translations(self, config):
        env = config['theme'].get_env()
        env.add_extension('jinja2.ext.i18n')
        env.install_null_translations()
        return env

    # Test build.get_context

    def test_context_base_url_homepage(self):
        nav_cfg = [
            {'Home': 'index.md'}
        ]
        cfg = load_config(nav=nav_cfg, use_directory_urls=False)
        files = Files([
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ])
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[0])
        self.assertEqual(context['base_url'], '.')

    def test_context_base_url_homepage_use_directory_urls(self):
        nav_cfg = [
            {'Home': 'index.md'}
        ]
        cfg = load_config(nav=nav_cfg)
        files = Files([
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ])
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[0])
        self.assertEqual(context['base_url'], '.')

    def test_context_base_url_nested_page(self):
        nav_cfg = [
            {'Home': 'index.md'},
            {'Nested': 'foo/bar.md'}
        ]
        cfg = load_config(nav=nav_cfg, use_directory_urls=False)
        files = Files([
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
            File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        ])
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[1])
        self.assertEqual(context['base_url'], '..')

    def test_context_base_url_nested_page_use_directory_urls(self):
        nav_cfg = [
            {'Home': 'index.md'},
            {'Nested': 'foo/bar.md'}
        ]
        cfg = load_config(nav=nav_cfg)
        files = Files([
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
            File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        ])
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[1])
        self.assertEqual(context['base_url'], '../..')

    def test_context_base_url_relative_no_page(self):
        cfg = load_config(use_directory_urls=False)
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..')
        self.assertEqual(context['base_url'], '..')

    def test_context_base_url_relative_no_page_use_directory_urls(self):
        cfg = load_config()
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..')
        self.assertEqual(context['base_url'], '..')

    def test_context_base_url_absolute_no_page(self):
        cfg = load_config(use_directory_urls=False)
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/')
        self.assertEqual(context['base_url'], '/')

    def test_context_base_url__absolute_no_page_use_directory_urls(self):
        cfg = load_config()
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/')
        self.assertEqual(context['base_url'], '/')

    def test_context_base_url_absolute_nested_no_page(self):
        cfg = load_config(use_directory_urls=False)
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/foo/')
        self.assertEqual(context['base_url'], '/foo/')

    def test_context_base_url__absolute_nested_no_page_use_directory_urls(self):
        cfg = load_config()
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/foo/')
        self.assertEqual(context['base_url'], '/foo/')

    def test_context_extra_css_js_from_homepage(self):
        nav_cfg = [
            {'Home': 'index.md'}
        ]
        cfg = load_config(
            nav=nav_cfg,
            extra_css=['style.css'],
            extra_javascript=['script.js'],
            use_directory_urls=False
        )
        files = Files([
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ])
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[0])
        self.assertEqual(context['extra_css'], ['style.css'])
        self.assertEqual(context['extra_javascript'], ['script.js'])

    def test_context_extra_css_js_from_nested_page(self):
        nav_cfg = [
            {'Home': 'index.md'},
            {'Nested': 'foo/bar.md'}
        ]
        cfg = load_config(
            nav=nav_cfg,
            extra_css=['style.css'],
            extra_javascript=['script.js'],
            use_directory_urls=False
        )
        files = Files([
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
            File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        ])
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[1])
        self.assertEqual(context['extra_css'], ['../style.css'])
        self.assertEqual(context['extra_javascript'], ['../script.js'])

    def test_context_extra_css_js_from_nested_page_use_directory_urls(self):
        nav_cfg = [
            {'Home': 'index.md'},
            {'Nested': 'foo/bar.md'}
        ]
        cfg = load_config(
            nav=nav_cfg,
            extra_css=['style.css'],
            extra_javascript=['script.js']
        )
        files = Files([
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
            File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        ])
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[1])
        self.assertEqual(context['extra_css'], ['../../style.css'])
        self.assertEqual(context['extra_javascript'], ['../../script.js'])

    def test_context_extra_css_js_no_page(self):
        cfg = load_config(extra_css=['style.css'], extra_javascript=['script.js'])
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..')
        self.assertEqual(context['extra_css'], ['../style.css'])
        self.assertEqual(context['extra_javascript'], ['../script.js'])

    def test_extra_context(self):
        cfg = load_config(extra={'a': 1})
        context = build.get_context(mock.Mock(), mock.Mock(), cfg)
        self.assertEqual(context['config']['extra']['a'], 1)

    # Test build._build_theme_template

    @mock.patch('mkdocs.utils.write_file')
    @mock.patch('mkdocs.commands.build._build_template', return_value='some content')
    def test_build_theme_template(self, mock_build_template, mock_write_file):
        cfg = load_config()
        env = cfg['theme'].get_env()
        build._build_theme_template('main.html', env, mock.Mock(), cfg, mock.Mock())
        self.assert_mock_called_once(mock_write_file)
        self.assert_mock_called_once(mock_build_template)

    @mock.patch('mkdocs.utils.write_file')
    @mock.patch('mkdocs.commands.build._build_template', return_value='some content')
    @mock.patch('gzip.GzipFile')
    def test_build_sitemap_template(self, mock_gzip_gzipfile, mock_build_template, mock_write_file):
        cfg = load_config()
        env = cfg['theme'].get_env()
        build._build_theme_template('sitemap.xml', env, mock.Mock(), cfg, mock.Mock())
        self.assert_mock_called_once(mock_write_file)
        self.assert_mock_called_once(mock_build_template)
        self.assert_mock_called_once(mock_gzip_gzipfile)

    @mock.patch('mkdocs.utils.write_file')
    @mock.patch('mkdocs.commands.build._build_template', return_value='')
    def test_skip_missing_theme_template(self, mock_build_template, mock_write_file):
        cfg = load_config()
        env = cfg['theme'].get_env()
        with self.assertLogs('mkdocs', level='WARN') as cm:
            build._build_theme_template('missing.html', env, mock.Mock(), cfg, mock.Mock())
        self.assertEqual(
            cm.output,
            ["WARNING:mkdocs.commands.build:Template skipped: 'missing.html' not found in theme directories."]
        )
        mock_write_file.assert_not_called()
        mock_build_template.assert_not_called()

    @mock.patch('mkdocs.utils.write_file')
    @mock.patch('mkdocs.commands.build._build_template', return_value='')
    def test_skip_theme_template_empty_output(self, mock_build_template, mock_write_file):
        cfg = load_config()
        env = cfg['theme'].get_env()
        with self.assertLogs('mkdocs', level='INFO') as cm:
            build._build_theme_template('main.html', env, mock.Mock(), cfg, mock.Mock())
        self.assertEqual(
            cm.output,
            ["INFO:mkdocs.commands.build:Template skipped: 'main.html' generated empty output."]
        )
        mock_write_file.assert_not_called()
        self.assert_mock_called_once(mock_build_template)

    # Test build._build_extra_template

    @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data='template content'))
    def test_build_extra_template(self):
        cfg = load_config()
        files = Files([
            File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ])
        build._build_extra_template('foo.html', files, cfg, mock.Mock())

    @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data='template content'))
    def test_skip_missing_extra_template(self):
        cfg = load_config()
        files = Files([
            File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ])
        with self.assertLogs('mkdocs', level='INFO') as cm:
            build._build_extra_template('missing.html', files, cfg, mock.Mock())
        self.assertEqual(
            cm.output,
            ["WARNING:mkdocs.commands.build:Template skipped: 'missing.html' not found in docs_dir."]
        )

    @mock.patch('mkdocs.commands.build.open', side_effect=OSError('Error message.'))
    def test_skip_ioerror_extra_template(self, mock_open):
        cfg = load_config()
        files = Files([
            File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ])
        with self.assertLogs('mkdocs', level='INFO') as cm:
            build._build_extra_template('foo.html', files, cfg, mock.Mock())
        self.assertEqual(
            cm.output,
            ["WARNING:mkdocs.commands.build:Error reading template 'foo.html': Error message."]
        )

    @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data=''))
    def test_skip_extra_template_empty_output(self):
        cfg = load_config()
        files = Files([
            File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ])
        with self.assertLogs('mkdocs', level='INFO') as cm:
            build._build_extra_template('foo.html', files, cfg, mock.Mock())
        self.assertEqual(
            cm.output,
            ["INFO:mkdocs.commands.build:Template skipped: 'foo.html' generated empty output."]
        )

    # Test build._populate_page

    @tempdir(files={'index.md': 'page content'})
    def test_populate_page(self, docs_dir):
        cfg = load_config(docs_dir=docs_dir)
        file = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        page = Page('Foo', file, cfg)
        build._populate_page(page, cfg, Files([file]))
        self.assertEqual(page.content, '<p>page content</p>')

    @tempdir(files={'testing.html': '<p>page content</p>'})
    def test_populate_page_dirty_modified(self, site_dir):
        cfg = load_config(site_dir=site_dir)
        file = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        page = Page('Foo', file, cfg)
        build._populate_page(page, cfg, Files([file]), dirty=True)
        self.assertTrue(page.markdown.startswith('# Welcome to MkDocs'))
        self.assertTrue(page.content.startswith('<h1 id="welcome-to-mkdocs">Welcome to MkDocs</h1>'))

    @tempdir(files={'index.md': 'page content'})
    @tempdir(files={'index.html': '<p>page content</p>'})
    def test_populate_page_dirty_not_modified(self, site_dir, docs_dir):
        cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
        file = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        page = Page('Foo', file, cfg)
        build._populate_page(page, cfg, Files([file]), dirty=True)
        # Content is empty as file read was skipped
        self.assertEqual(page.markdown, None)
        self.assertEqual(page.content, None)

    @tempdir(files={'index.md': 'new page content'})
    @mock.patch('mkdocs.structure.pages.open', side_effect=OSError('Error message.'))
    def test_populate_page_read_error(self, docs_dir, mock_open):
        cfg = load_config(docs_dir=docs_dir)
        file = File('missing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        page = Page('Foo', file, cfg)
        with self.assertLogs('mkdocs', level='ERROR') as cm:
            self.assertRaises(OSError, build._populate_page, page, cfg, Files([file]))
        self.assertEqual(
            cm.output, [
                'ERROR:mkdocs.structure.pages:File not found: missing.md',
                "ERROR:mkdocs.commands.build:Error reading page 'missing.md': Error message."
            ]
        )
        self.assert_mock_called_once(mock_open)

    @tempdir(files={'index.md': 'page content'})
    @mock.patch('mkdocs.plugins.PluginCollection.run_event', side_effect=PluginError('Error message.'))
    def test_populate_page_read_plugin_error(self, docs_dir, mock_open):
        cfg = load_config(docs_dir=docs_dir)
        file = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        page = Page('Foo', file, cfg)
        with self.assertLogs('mkdocs', level='ERROR') as cm:
            self.assertRaises(PluginError, build._populate_page, page, cfg, Files([file]))
        self.assertEqual(
            cm.output, [
                "ERROR:mkdocs.commands.build:Error reading page 'index.md':"
            ]
        )
        self.assert_mock_called_once(mock_open)

    # Test build._build_page

    @tempdir()
    def test_build_page(self, site_dir):
        cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
        files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
        nav = get_navigation(files, cfg)
        page = files.documentation_pages()[0].page
        # Fake populate page
        page.title = 'Title'
        page.markdown = 'page content'
        page.content = '<p>page content</p>'
        build._build_page(page, cfg, files, nav, self._get_env_with_null_translations(cfg))
        self.assertPathIsFile(site_dir, 'index.html')

    # TODO: fix this. It seems that jinja2 chokes on the mock object. Not sure how to resolve.
    # @tempdir()
    # @mock.patch('jinja2.environment.Template')
    # def test_build_page_empty(self, site_dir, mock_template):
    #     mock_template.render = mock.Mock(return_value='')
    #     cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
    #     files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
    #     nav = get_navigation(files, cfg)
    #     page = files.documentation_pages()[0].page
    #     # Fake populate page
    #     page.title = ''
    #     page.markdown = ''
    #     page.content = ''
    #     with self.assertLogs('mkdocs', level='INFO') as cm:
    #         build._build_page(page, cfg, files, nav, cfg['theme'].get_env())
    #     self.assertEqual(
    #         cm.output,
    #         ["INFO:mkdocs.commands.build:Page skipped: 'index.md'. Generated empty output."]
    #     )
    #     self.assert_mock_called_once(mock_template.render)
    #     self.assertPathNotFile(site_dir, 'index.html')

    @tempdir(files={'index.md': 'page content'})
    @tempdir(files={'index.html': '<p>page content</p>'})
    @mock.patch('mkdocs.utils.write_file')
    def test_build_page_dirty_modified(self, site_dir, docs_dir, mock_write_file):
        cfg = load_config(docs_dir=docs_dir, site_dir=site_dir, nav=['index.md'], plugins=[])
        files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
        nav = get_navigation(files, cfg)
        page = files.documentation_pages()[0].page
        # Fake populate page
        page.title = 'Title'
        page.markdown = 'new page content'
        page.content = '<p>new page content</p>'
        build._build_page(page, cfg, files, nav, self._get_env_with_null_translations(cfg), dirty=True)
        mock_write_file.assert_not_called()

    @tempdir(files={'testing.html': '<p>page content</p>'})
    @mock.patch('mkdocs.utils.write_file')
    def test_build_page_dirty_not_modified(self, site_dir, mock_write_file):
        cfg = load_config(site_dir=site_dir, nav=['testing.md'], plugins=[])
        files = Files([File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
        nav = get_navigation(files, cfg)
        page = files.documentation_pages()[0].page
        # Fake populate page
        page.title = 'Title'
        page.markdown = 'page content'
        page.content = '<p>page content</p>'
        build._build_page(page, cfg, files, nav, self._get_env_with_null_translations(cfg), dirty=True)
        self.assert_mock_called_once(mock_write_file)

    @tempdir()
    def test_build_page_custom_template(self, site_dir):
        cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
        files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
        nav = get_navigation(files, cfg)
        page = files.documentation_pages()[0].page
        # Fake populate page
        page.title = 'Title'
        page.meta = {'template': '404.html'}
        page.markdown = 'page content'
        page.content = '<p>page content</p>'
        build._build_page(page, cfg, files, nav, self._get_env_with_null_translations(cfg))
        self.assertPathIsFile(site_dir, 'index.html')

    @tempdir()
    @mock.patch('mkdocs.utils.write_file', side_effect=OSError('Error message.'))
    def test_build_page_error(self, site_dir, mock_write_file):
        cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
        files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
        nav = get_navigation(files, cfg)
        page = files.documentation_pages()[0].page
        # Fake populate page
        page.title = 'Title'
        page.markdown = 'page content'
        page.content = '<p>page content</p>'
        with self.assertLogs('mkdocs', level='ERROR') as cm:
            self.assertRaises(
                    OSError,
                    build._build_page,
                    page,
                    cfg,
                    files,
                    nav,
                    self._get_env_with_null_translations(cfg)
            )
        self.assertEqual(
            cm.output,
            ["ERROR:mkdocs.commands.build:Error building page 'index.md': Error message."]
        )
        self.assert_mock_called_once(mock_write_file)

    @tempdir()
    @mock.patch('mkdocs.plugins.PluginCollection.run_event', side_effect=PluginError('Error message.'))
    def test_build_page_plugin_error(self, site_dir, mock_write_file):
        cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
        files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
        nav = get_navigation(files, cfg)
        page = files.documentation_pages()[0].page
        # Fake populate page
        page.title = 'Title'
        page.markdown = 'page content'
        page.content = '<p>page content</p>'
        with self.assertLogs('mkdocs', level='ERROR') as cm:
            self.assertRaises(PluginError, build._build_page, page, cfg, files, nav, cfg['theme'].get_env())
        self.assertEqual(
            cm.output,
            ["ERROR:mkdocs.commands.build:Error building page 'index.md':"]
        )
        self.assert_mock_called_once(mock_write_file)

    # Test build.build

    @tempdir(files={
        'index.md': 'page content',
        'empty.md': '',
        'img.jpg': '',
        'static.html': 'content',
        '.hidden': 'content',
        '.git/hidden': 'content'
    })
    @tempdir()
    def test_copying_media(self, site_dir, docs_dir):
        cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
        build.build(cfg)

        # Verify that only non-empty md file (coverted to html), static HTML file and image are copied.
        self.assertPathIsFile(site_dir, 'index.html')
        self.assertPathIsFile(site_dir, 'img.jpg')
        self.assertPathIsFile(site_dir, 'static.html')
        self.assertPathNotExists(site_dir, 'empty.md')
        self.assertPathNotExists(site_dir, '.hidden')
        self.assertPathNotExists(site_dir, '.git/hidden')

    @tempdir(files={'index.md': 'page content'})
    @tempdir()
    def test_copy_theme_files(self, site_dir, docs_dir):
        cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
        build.build(cfg)

        # Verify only theme media are copied, not templates, Python or localization files.
        self.assertPathIsFile(site_dir, 'index.html')
        self.assertPathIsFile(site_dir, '404.html')
        self.assertPathIsDir(site_dir, 'js')
        self.assertPathIsDir(site_dir, 'css')
        self.assertPathIsDir(site_dir, 'img')
        self.assertPathIsDir(site_dir, 'fonts')
        self.assertPathNotExists(site_dir, '__init__.py')
        self.assertPathNotExists(site_dir, '__init__.pyc')
        self.assertPathNotExists(site_dir, 'base.html')
        self.assertPathNotExists(site_dir, 'content.html')
        self.assertPathNotExists(site_dir, 'main.html')
        self.assertPathNotExists(site_dir, 'locales')

    # Test build.site_directory_contains_stale_files

    @tempdir(files=['index.html'])
    def test_site_dir_contains_stale_files(self, site_dir):
        self.assertTrue(build.site_directory_contains_stale_files(site_dir))

    @tempdir()
    def test_not_site_dir_contains_stale_files(self, site_dir):
        self.assertFalse(build.site_directory_contains_stale_files(site_dir))
Exemple #15
0
 def on_page_content(self, html, **kwargs):
     if self.error_on == 'page_content':
         raise PluginError('page content error')
     return html
    def on_post_build(self, config, **kwargs):
        """
        The post_build event does not alter any variables. Use this event to call post-build scripts.

        See https://www.mkdocs.org/user-guide/plugins/#on_post_build.
        """
        if not self.config.get("enabled"):
            return

        if len(self.context) == 0:
            msg = "Could not find a template context.\n"
            msg += "Report an issue at https://github.com/timvink/mkdocs-print-site-plugin\n"
            msg += f"And mention the template you're using: {get_theme_name(config)}"
            raise PluginError(msg)

        # Add print-site.js
        js_output_base_path = os.path.join(config["site_dir"], "js")
        js_file_path = os.path.join(js_output_base_path, "print-site.js")
        copy_file(os.path.join(os.path.join(HERE, "js"), "print-site.js"),
                  js_file_path)

        if self.config.get("include_css"):
            # Add print-site.css
            css_output_base_path = os.path.join(config["site_dir"], "css")
            css_file_path = os.path.join(css_output_base_path,
                                         "print-site.css")
            copy_file(
                os.path.join(os.path.join(HERE, "css"), "print-site.css"),
                css_file_path)

            # Add enumeration css
            for f in self.enum_css_files:
                f = f.replace("/", os.sep)
                css_file_path = os.path.join(config["site_dir"], f)
                copy_file(os.path.join(HERE, f), css_file_path)

            # Add theme CSS file
            css_file = "print-site-%s.css" % get_theme_name(config)
            if css_file in os.listdir(os.path.join(HERE, "css")):
                css_file_path = os.path.join(css_output_base_path, css_file)
                copy_file(os.path.join(os.path.join(HERE, "css"), css_file),
                          css_file_path)

        # Combine the HTML of all pages present in the navigation
        self.print_page.content = self.renderer.write_combined()
        # Generate a TOC sidebar for HTML version of print page
        self.print_page.toc = self.renderer.get_toc_sidebar()

        # Get the info for MkDocs to be able to apply a theme template on our print page
        env = config["theme"].get_env()
        # env.list_templates()
        template = env.get_template("main.html")
        self.context["page"] = self.print_page
        # Render the theme template for the print page
        html = template.render(self.context)

        # Remove lazy loading attributes from images
        # https://regex101.com/r/HVpKPs/1
        html = re.sub(r"(\<img.+)(loading=\"lazy\")", r"\1", html)

        # Compatiblity with mkdocs-chart-plugin
        # As this plugin adds some javascript to every page
        # It should be included in the print site also
        if config.get("plugins", {}).get("charts"):
            html = (config.get("plugins",
                               {}).get("charts").add_javascript_variables(
                                   html, self.print_page, config))

        # Compatibility with https://github.com/g-provost/lightgallery-markdown
        # This plugin insert link hrefs with double dashes, f.e.
        # <link href="//assets/css/somecss.css">
        # Details https://github.com/timvink/mkdocs-print-site-plugin/issues/68
        htmls = html.split("</head>")
        base_url = "../" if config.get("use_directory_urls") else ""
        htmls[0] = htmls[0].replace("href=\"//", f"href=\"{base_url}")
        htmls[0] = htmls[0].replace("src=\"//", f"src=\"{base_url}")
        html = "</head>".join(htmls)

        # Determine calls to required javascript functions
        js_calls = "remove_material_navigation();"
        js_calls += "remove_mkdocs_theme_navigation();"
        if self.config.get("add_table_of_contents"):
            js_calls += "generate_toc();"

        # Inject JS into print page
        print_site_js = ("""
        <script type="text/javascript">
        document.addEventListener('DOMContentLoaded', function () {
            %s
        })
        </script>
        """ % js_calls)
        html = html.replace("</head>", print_site_js + "</head>")

        # Write the print_page file to the output folder
        write_file(
            html.encode("utf-8", errors="xmlcharrefreplace"),
            self.print_page.file.abs_dest_path,
        )