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))
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)
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())
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
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}")
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()
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}" )
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
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}" )
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
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))
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, )