| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- import logging
- import os
- import gzip
- from urllib.parse import urlparse
- from jinja2.exceptions import TemplateNotFound
- import jinja2
- from mkdocs import utils
- from mkdocs.structure.files import get_files
- from mkdocs.structure.nav import get_navigation
- import mkdocs
- class DuplicateFilter:
- ''' Avoid logging duplicate messages. '''
- def __init__(self):
- self.msgs = set()
- def filter(self, record):
- rv = record.msg not in self.msgs
- self.msgs.add(record.msg)
- return rv
- log = logging.getLogger(__name__)
- log.addFilter(DuplicateFilter())
- log.addFilter(utils.warning_filter)
- def get_context(nav, files, config, page=None, base_url=''):
- """
- Return the template context for a given page or template.
- """
- if page is not None:
- base_url = utils.get_relative_url('.', page.url)
- extra_javascript = utils.create_media_urls(config['extra_javascript'], page, base_url)
- extra_css = utils.create_media_urls(config['extra_css'], page, base_url)
- return {
- 'nav': nav,
- 'pages': files.documentation_pages(),
- 'base_url': base_url,
- 'extra_css': extra_css,
- 'extra_javascript': extra_javascript,
- 'mkdocs_version': mkdocs.__version__,
- 'build_date_utc': utils.get_build_datetime(),
- 'config': config,
- 'page': page,
- }
- def _build_template(name, template, files, config, nav):
- """
- Return rendered output for given template as a string.
- """
- # Run `pre_template` plugin events.
- template = config['plugins'].run_event(
- 'pre_template', template, template_name=name, config=config
- )
- if utils.is_error_template(name):
- # Force absolute URLs in the nav of error pages and account for the
- # possability that the docs root might be different than the server root.
- # See https://github.com/mkdocs/mkdocs/issues/77.
- # However, if site_url is not set, assume the docs root and server root
- # are the same. See https://github.com/mkdocs/mkdocs/issues/1598.
- base_url = urlparse(config['site_url'] or '/').path
- else:
- base_url = utils.get_relative_url('.', name)
- context = get_context(nav, files, config, base_url=base_url)
- # Run `template_context` plugin events.
- context = config['plugins'].run_event(
- 'template_context', context, template_name=name, config=config
- )
- output = template.render(context)
- # Run `post_template` plugin events.
- output = config['plugins'].run_event(
- 'post_template', output, template_name=name, config=config
- )
- return output
- def _build_theme_template(template_name, env, files, config, nav):
- """ Build a template using the theme environment. """
- log.debug("Building theme template: {}".format(template_name))
- try:
- template = env.get_template(template_name)
- except TemplateNotFound:
- log.warning("Template skipped: '{}' not found in theme directories.".format(template_name))
- return
- output = _build_template(template_name, template, files, config, nav)
- if output.strip():
- output_path = os.path.join(config['site_dir'], template_name)
- utils.write_file(output.encode('utf-8'), output_path)
- if template_name == 'sitemap.xml':
- log.debug("Gzipping template: %s", template_name)
- gz_filename = '{}.gz'.format(output_path)
- with open(gz_filename, 'wb') as f:
- timestamp = utils.get_build_timestamp()
- with gzip.GzipFile(fileobj=f, filename=gz_filename, mode='wb', mtime=timestamp) as gz_buf:
- gz_buf.write(output.encode('utf-8'))
- else:
- log.info("Template skipped: '{}' generated empty output.".format(template_name))
- def _build_extra_template(template_name, files, config, nav):
- """ Build user templates which are not part of the theme. """
- log.debug("Building extra template: {}".format(template_name))
- file = files.get_file_from_path(template_name)
- if file is None:
- log.warning("Template skipped: '{}' not found in docs_dir.".format(template_name))
- return
- try:
- with open(file.abs_src_path, 'r', encoding='utf-8', errors='strict') as f:
- template = jinja2.Template(f.read())
- except Exception as e:
- log.warning("Error reading template '{}': {}".format(template_name, e))
- return
- output = _build_template(template_name, template, files, config, nav)
- if output.strip():
- utils.write_file(output.encode('utf-8'), file.abs_dest_path)
- else:
- log.info("Template skipped: '{}' generated empty output.".format(template_name))
- def _populate_page(page, config, files, dirty=False):
- """ Read page content from docs_dir and render Markdown. """
- try:
- # When --dirty is used, only read the page if the file has been modified since the
- # previous build of the output.
- if dirty and not page.file.is_modified():
- return
- # Run the `pre_page` plugin event
- page = config['plugins'].run_event(
- 'pre_page', page, config=config, files=files
- )
- page.read_source(config)
- # Run `page_markdown` plugin events.
- page.markdown = config['plugins'].run_event(
- 'page_markdown', page.markdown, page=page, config=config, files=files
- )
- page.render(config, files)
- # Run `page_content` plugin events.
- page.content = config['plugins'].run_event(
- 'page_content', page.content, page=page, config=config, files=files
- )
- except Exception as e:
- log.error("Error reading page '{}': {}".format(page.file.src_path, e))
- raise
- def _build_page(page, config, files, nav, env, dirty=False):
- """ Pass a Page to theme template and write output to site_dir. """
- try:
- # When --dirty is used, only build the page if the file has been modified since the
- # previous build of the output.
- if dirty and not page.file.is_modified():
- return
- log.debug("Building page {}".format(page.file.src_path))
- # Activate page. Signals to theme that this is the current page.
- page.active = True
- context = get_context(nav, files, config, page)
- # Allow 'template:' override in md source files.
- if 'template' in page.meta:
- template = env.get_template(page.meta['template'])
- else:
- template = env.get_template('main.html')
- # Run `page_context` plugin events.
- context = config['plugins'].run_event(
- 'page_context', context, page=page, config=config, nav=nav
- )
- # Render the template.
- output = template.render(context)
- # Run `post_page` plugin events.
- output = config['plugins'].run_event(
- 'post_page', output, page=page, config=config
- )
- # Write the output file.
- if output.strip():
- utils.write_file(output.encode('utf-8', errors='xmlcharrefreplace'), page.file.abs_dest_path)
- else:
- log.info("Page skipped: '{}'. Generated empty output.".format(page.file.src_path))
- # Deactivate page
- page.active = False
- except Exception as e:
- log.error("Error building page '{}': {}".format(page.file.src_path, e))
- raise
- def build(config, live_server=False, dirty=False):
- """ Perform a full site build. """
- from time import time
- start = time()
- # Run `config` plugin events.
- config = config['plugins'].run_event('config', config)
- # Run `pre_build` plugin events.
- config['plugins'].run_event('pre_build', config=config)
- if not dirty:
- log.info("Cleaning site directory")
- utils.clean_directory(config['site_dir'])
- else: # pragma: no cover
- # Warn user about problems that may occur with --dirty option
- log.warning("A 'dirty' build is being performed, this will likely lead to inaccurate navigation and other"
- " links within your site. This option is designed for site development purposes only.")
- if not live_server: # pragma: no cover
- log.info("Building documentation to directory: %s", config['site_dir'])
- if dirty and site_directory_contains_stale_files(config['site_dir']):
- log.info("The directory contains stale files. Use --clean to remove them.")
- # First gather all data from all files/pages to ensure all data is consistent across all pages.
- files = get_files(config)
- env = config['theme'].get_env()
- files.add_files_from_theme(env, config)
- # Run `files` plugin events.
- files = config['plugins'].run_event('files', files, config=config)
- nav = get_navigation(files, config)
- # Run `nav` plugin events.
- nav = config['plugins'].run_event('nav', nav, config=config, files=files)
- log.debug("Reading markdown pages.")
- for file in files.documentation_pages():
- log.debug("Reading: " + file.src_path)
- _populate_page(file.page, config, files, dirty)
- # Run `env` plugin events.
- env = config['plugins'].run_event(
- 'env', env, config=config, files=files
- )
- # Start writing files to site_dir now that all data is gathered. Note that order matters. Files
- # with lower precedence get written first so that files with higher precedence can overwrite them.
- log.debug("Copying static assets.")
- files.copy_static_files(dirty=dirty)
- for template in config['theme'].static_templates:
- _build_theme_template(template, env, files, config, nav)
- for template in config['extra_templates']:
- _build_extra_template(template, files, config, nav)
- log.debug("Building markdown pages.")
- for file in files.documentation_pages():
- _build_page(file.page, config, files, nav, env, dirty)
- # Run `post_build` plugin events.
- config['plugins'].run_event('post_build', config=config)
- if config['strict'] and utils.warning_filter.count:
- raise SystemExit('\nExited with {} warnings in strict mode.'.format(utils.warning_filter.count))
- log.info('Documentation built in %.2f seconds', time() - start)
- def site_directory_contains_stale_files(site_directory):
- """ Check if the site directory contains stale files from a previous build. """
- return True if os.path.exists(site_directory) and os.listdir(site_directory) else False
|