def test_dir_unicode(self): cfg = Config( [('dir', config_options.Dir())], config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'), ) test_config = { 'dir': 'юникод' } cfg.load_dict(test_config) fails, warns = cfg.validate() self.assertEqual(len(fails), 0) self.assertEqual(len(warns), 0) self.assertIsInstance(cfg['dir'], str)
def test_dir_filesystemencoding(self): cfg = Config( [('dir', config_options.Dir())], config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'), ) test_config = { 'dir': 'Übersicht'.encode(encoding=sys.getfilesystemencoding()) } cfg.load_dict(test_config) fails, warns = cfg.validate() # str does not include byte strings so validation fails self.assertEqual(len(fails), 1) self.assertEqual(len(warns), 0)
def test_config_dir_prepended(self): base_path = os.path.abspath('.') cfg = Config( [('dir', config_options.Dir())], config_file_path=os.path.join(base_path, 'mkdocs.yml'), ) test_config = {'dir': 'foo'} cfg.load_dict(test_config) fails, warns = cfg.validate() self.assertEqual(len(fails), 0) self.assertEqual(len(warns), 0) self.assertIsInstance(cfg['dir'], str) self.assertEqual(cfg['dir'], os.path.join(base_path, 'foo'))
def test_doc_dir_in_site_dir(self): j = os.path.join option = config_options.SiteDir() docs_dir = config_options.Dir() # The parent dir is not the same on every system, so use the actual dir name parent_dir = mkdocs.__file__.split(os.sep)[-3] test_configs = ( { 'docs_dir': j('site', 'docs'), 'site_dir': 'site' }, { 'docs_dir': 'docs', 'site_dir': '.' }, { 'docs_dir': '.', 'site_dir': '.' }, { 'docs_dir': 'docs', 'site_dir': '' }, { 'docs_dir': '', 'site_dir': '' }, { 'docs_dir': j('..', parent_dir, 'docs'), 'site_dir': 'docs' }, ) for test_config in test_configs: test_config['docs_dir'] = docs_dir.validate( test_config['docs_dir']) test_config['site_dir'] = option.validate(test_config['site_dir']) self.assertRaises(config_options.ValidationError, option.post_validation, test_config, 'key')
def test_dir_bad_encoding_fails(self): cfg = Config( [('dir', config_options.Dir())], config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'), ) test_config = {'dir': 'юникод'.encode(encoding='ISO 8859-5')} cfg.load_dict(test_config) fails, warns = cfg.validate() if sys.platform.startswith('win') and not utils.PY3: # PY2 on Windows seems to be able to decode anything we give it. # But that just means less possable errors for those users so we allow it. self.assertEqual(len(fails), 0) else: self.assertEqual(len(fails), 1) self.assertEqual(len(warns), 0)
def test_doc_dir_in_site_dir(self): j = os.path.join option = config_options.SiteDir() docs_dir = config_options.Dir() test_configs = ( { 'docs_dir': j('site', 'docs'), 'site_dir': 'site' }, { 'docs_dir': 'docs', 'site_dir': '.' }, { 'docs_dir': '.', 'site_dir': '.' }, { 'docs_dir': 'docs', 'site_dir': '' }, { 'docs_dir': '', 'site_dir': '' }, { 'docs_dir': j('..', 'mkdocs', 'docs'), 'site_dir': 'docs' }, ) for test_config in test_configs: test_config['docs_dir'] = docs_dir.validate( test_config['docs_dir']) test_config['site_dir'] = option.validate(test_config['site_dir']) self.assertRaises(config_options.ValidationError, option.post_validation, test_config, 'key')
def test_site_dir_in_docs_dir(self): j = os.path.join test_configs = ( {'docs_dir': 'docs', 'site_dir': j('docs', 'site')}, {'docs_dir': '.', 'site_dir': 'site'}, {'docs_dir': '', 'site_dir': 'site'}, ) for test_config in test_configs: test_config['config_file_path'] = j(os.path.abspath('..'), 'mkdocs.yml') docs_dir = config_options.Dir() option = config_options.SiteDir() test_config['docs_dir'] = docs_dir.validate(test_config['docs_dir']) test_config['site_dir'] = option.validate(test_config['site_dir']) self.assertRaises(config_options.ValidationError, option.post_validation, test_config, 'site_dir')
class DummyPlugin(plugins.BasePlugin): config_scheme = ( ('foo', config_options.Type(str, default='default foo')), ('bar', config_options.Type(int, default=0)), ('dir', config_options.Dir(exists=False)), ) def on_pre_page(self, content, **kwargs): """ modify page content by prepending `foo` config value. """ return f'{self.config["foo"]} {content}' def on_nav(self, item, **kwargs): """ do nothing (return None) to not modify item. """ return None def on_page_read_source(self, **kwargs): """ create new source by prepending `foo` config value to 'source'. """ return f'{self.config["foo"]} source' def on_pre_build(self, **kwargs): """ do nothing (return None). """ return None
def validate_config(self, config): """ Given a config with values for site_dir and doc_dir, run site_dir post_validation. """ site_dir = config_options.SiteDir() docs_dir = config_options.Dir() fname = os.path.join(os.path.abspath('..'), 'mkdocs.yml') config['docs_dir'] = docs_dir.validate(config['docs_dir']) config['site_dir'] = site_dir.validate(config['site_dir']) schema = [ ('site_dir', site_dir), ('docs_dir', docs_dir), ] cfg = Config(schema, fname) cfg.load_dict(config) failed, warned = cfg.validate() if failed: raise config_options.ValidationError(failed) return True
def test_site_dir_in_docs_dir(self): j = os.path.join test_configs = ( {'docs_dir': 'docs', 'site_dir': j('docs', 'site')}, {'docs_dir': '.', 'site_dir': 'site'}, {'docs_dir': '', 'site_dir': 'site'}, ) for test_config in test_configs: docs_dir = config_options.Dir() option = config_options.SiteDir() test_config['docs_dir'] = docs_dir.validate(test_config['docs_dir']) test_config['site_dir'] = option.validate(test_config['site_dir']) option.post_validation(test_config, 'key') self.assertEqual(len(option.warnings), 1) self.assertEqual( option.warnings[0][:50], "The 'site_dir' should not be within the 'docs_dir'")
def test_dir_filesystemencoding(self): cfg = Config( [('dir', config_options.Dir())], config_file_path=os.path.join(os.path.abspath('.'), 'mkdocs.yml'), ) test_config = { 'dir': 'Übersicht'.encode(encoding=sys.getfilesystemencoding()) } cfg.load_dict(test_config) fails, warns = cfg.validate() if utils.PY3: # In PY3 string_types does not include byte strings so validation fails self.assertEqual(len(fails), 1) self.assertEqual(len(warns), 0) else: # In PY2 string_types includes byte strings so validation passes # This test confirms that the byte string is properly decoded self.assertEqual(len(fails), 0) self.assertEqual(len(warns), 0) self.assertIsInstance(cfg['dir'], utils.text_type)
def test_incorrect_type_type_error(self): option = config_options.Dir() self.assertRaises(config_options.ValidationError, option.validate, [])
def test_missing_dir_but_required(self): d = os.path.join("not", "a", "real", "path", "I", "hope") option = config_options.Dir(exists=True) self.assertRaises(config_options.ValidationError, option.validate, d)
def test_file(self): d = __file__ option = config_options.Dir(exists=True) self.assertRaises(config_options.ValidationError, option.validate, d)
def test_missing_dir(self): d = os.path.join("not", "a", "real", "path", "I", "hope") option = config_options.Dir() value = option.validate(d) self.assertEqual(os.path.abspath(d), value)
def test_valid_dir(self): d = os.path.dirname(__file__) option = config_options.Dir(exists=True) value = option.validate(d) self.assertEqual(d, value)
class BibTexPlugin(BasePlugin): """ Allows the use of bibtex in markdown content for MKDocs. Options: bib_file (string): path or url to a single bibtex file for entries, url example: https://api.zotero.org/*/items?format=bibtex bib_dir (string): path to a directory of bibtex files for entries bib_command (string): command to place a bibliography relevant to just that file defaults to \bibliography bib_by_default (bool): automatically appends bib_command to markdown pages by default, defaults to true full_bib_command (string): command to place a full bibliography of all references csl_file (string, optional): path or url to a CSL file, relative to mkdocs.yml. cite_inline (bool): Whether or not to render inline citations, requires CSL, defaults to false """ config_scheme = [ ("bib_file", config_options.Type(str, required=False)), ("bib_dir", config_options.Dir(exists=True, required=False)), ("bib_command", config_options.Type(str, default="\\bibliography")), ("bib_by_default", config_options.Type(bool, default=True)), ("full_bib_command", config_options.Type(str, default="\\full_bibliography")), ("csl_file", config_options.Type(str, default='')), ("cite_inline", config_options.Type(bool, default=False)), ] def __init__(self): self.bib_data = None self.all_references = OrderedDict() self.unescape_for_arithmatex = False def on_config(self, config): """ Loads bibliography on load of config """ bibfiles = [] # Set bib_file from either url or path if self.config.get("bib_file", None) is not None: is_url = validators.url(self.config["bib_file"]) # if bib_file is a valid URL, cache it with tempfile if is_url: bibfiles.append( tempfile_from_url(self.config["bib_file"], '.bib')) else: bibfiles.append(self.config["bib_file"]) elif self.config.get("bib_dir", None) is not None: bibfiles.extend(Path(self.config["bib_dir"]).glob("*.bib")) else: raise Exception( "Must supply a bibtex file or directory for bibtex files") # load bibliography data refs = {} for bibfile in bibfiles: bibdata = parse_file(bibfile) refs.update(bibdata.entries) self.bib_data = BibliographyData(entries=refs) # Set CSL from either url or path (or empty) is_url = validators.url(self.config["csl_file"]) if is_url: self.csl_file = tempfile_from_url(self.config["csl_file"], '.csl') else: self.csl_file = self.config.get("csl_file", None) # Toggle whether or not to render citations inline (Requires CSL) self.cite_inline = self.config.get("cite_inline", False) if self.cite_inline and not self.csl_file: raise Exception( "Must supply a CSL file in order to use cite_inline") return config def on_page_markdown(self, markdown, page, config, files): """ Parses the markdown for each page, extracting the bibtex references If a local reference list is requested, this will render that list where requested 1. Finds all cite keys (may include multiple citation references) 2. Convert all cite keys to citation quads: (full cite key, induvidual cite key, citation key in corresponding style, citation for induvidual cite key) 3. Insert formatted cite keys into text 4. Insert the bibliography into the markdown 5. Insert the full bibliograph into the markdown """ # 1. Grab all the cited keys in the markdown cite_keys = find_cite_blocks(markdown) # 2. Convert all the citations to text references citation_quads = self.format_citations(cite_keys) # 3. Convert cited keys to citation, # or a footnote reference if inline_cite is false. if self.cite_inline: markdown = insert_citation_keys(citation_quads, markdown, self.csl_file, self.bib_data.to_string("bibtex")) else: markdown = insert_citation_keys(citation_quads, markdown) # 4. Insert in the bibliopgrahy text into the markdown bib_command = self.config.get("bib_command", "\\bibliography") if self.config.get("bib_by_default"): markdown += f"\n{bib_command}" bibliography = format_bibliography(citation_quads) markdown = re.sub( re.escape(bib_command), bibliography, markdown, ) # 5. Build the full Bibliography and insert into the text full_bib_command = self.config.get("full_bib_command", "\\full_bibliography") markdown = re.sub( re.escape(full_bib_command), self.full_bibliography, markdown, ) return markdown def format_citations(self, cite_keys): """ Formats references into citation quads and adds them to the global registry Args: cite_keys (list): List of full cite_keys that maybe compound keys Returns: citation_quads: quad tuples of the citation inforamtion """ # Deal with arithmatex fix at some point # 1. Extract the keys from the keyset entries = OrderedDict() pairs = [[cite_block, key] for cite_block in cite_keys for key in extract_cite_keys(cite_block)] keys = list(OrderedDict.fromkeys([k for _, k in pairs]).keys()) numbers = {k: str(n + 1) for n, k in enumerate(keys)} # Remove non-existant keys from pairs pairs = [p for p in pairs if p[1] in self.bib_data.entries] # 2. Collect any unformatted reference keys for _, key in pairs: if key not in self.all_references: entries[key] = self.bib_data.entries[key] # 3. Format entries if self.csl_file: self.all_references.update(format_pandoc(entries, self.csl_file)) else: self.all_references.update(format_simple(entries)) # 4. Construct quads quads = [(cite_block, key, numbers[key], self.all_references[key]) for cite_block, key in pairs] # List the quads in order to remove duplicate entries return list(dict.fromkeys(quads)) @property def full_bibliography(self): """ Returns the full bibliography text """ bibliography = [] for number, (key, citation) in enumerate(self.all_references.items()): bibliography_text = "[^{}]: {}".format(number, citation) bibliography.append(bibliography_text) return "\n".join(bibliography)
def get_schema(): return ( # Reserved for internal use, stores the mkdocs.yml config file. ('config_file_path', config_options.Type(str)), # The title to use for the documentation ('site_name', config_options.Type(str, required=True)), # Defines the structure of the navigation. ('nav', config_options.Nav()), # TODO: remove this when the `pages` config setting is fully deprecated. ('pages', config_options.Nav()), # The full URL to where the documentation will be hosted ('site_url', config_options.URL(is_dir=True)), # A description for the documentation project that will be added to the # HTML meta tags. ('site_description', config_options.Type(str)), # The name of the author to add to the HTML meta tags ('site_author', config_options.Type(str)), # The MkDocs theme for the documentation. ('theme', config_options.Theme(default='mkdocs')), # The directory containing the documentation markdown. ('docs_dir', config_options.Dir(default='docs', exists=True)), # The directory where the site will be built to ('site_dir', config_options.SiteDir(default='site')), # A copyright notice to add to the footer of documentation. ('copyright', config_options.Type(str)), # set of values for Google analytics containing the account IO and domain, # this should look like, ['UA-27795084-5', 'mkdocs.org'] ('google_analytics', config_options.Type(list, length=2)), # The address on which to serve the live reloading docs server. ('dev_addr', config_options.IpAddress(default='127.0.0.1:8000')), # If `True`, use `<page_name>/index.hmtl` style files with hyperlinks to # the directory.If `False`, use `<page_name>.html style file with # hyperlinks to the file. # True generates nicer URLs, but False is useful if browsing the output on # a filesystem. ('use_directory_urls', config_options.Type(bool, default=True)), # Specify a link to the project source repo to be included # in the documentation pages. ('repo_url', config_options.RepoURL()), # A name to use for the link to the project source repo. # Default, If repo_url is unset then None, otherwise # "GitHub", "Bitbucket" or "GitLab" for known url or Hostname # for unknown urls. ('repo_name', config_options.Type(str)), # Specify a URI to the docs dir in the project source repo, relative to the # repo_url. When set, a link directly to the page in the source repo will # be added to the generated HTML. If repo_url is not set also, this option # is ignored. ('edit_uri', config_options.Type(str)), # Specify which css or javascript files from the docs directory should be # additionally included in the site. ('extra_css', config_options.Type(list, default=[])), ('extra_javascript', config_options.Type(list, default=[])), # Similar to the above, but each template (HTML or XML) will be build with # Jinja2 and the global context. ('extra_templates', config_options.Type(list, default=[])), # PyMarkdown extension names. ('markdown_extensions', config_options.MarkdownExtensions( builtins=['toc', 'tables', 'fenced_code'], configkey='mdx_configs', default=[])), # PyMarkdown Extension Configs. For internal use only. ('mdx_configs', config_options.Private()), # enabling strict mode causes MkDocs to stop the build when a problem is # encountered rather than display an error. ('strict', config_options.Type(bool, default=False)), # the remote branch to commit to when using gh-deploy ('remote_branch', config_options.Type( str, default='gh-pages')), # the remote name to push to when using gh-deploy ('remote_name', config_options.Type(str, default='origin')), # extra is a mapping/dictionary of data that is passed to the template. # This allows template authors to require extra configuration that not # relevant to all themes and doesn't need to be explicitly supported by # MkDocs itself. A good example here would be including the current # project version. ('extra', config_options.SubConfig()), # a list of plugins. Each item may contain a string name or a key value pair. # A key value pair should be the string name (as the key) and a dict of config # options (as the value). ('plugins', config_options.Plugins(default=['search'])), )
def test_incorrect_type_attribute_error(self): option = config_options.Dir() with self.assertRaises(config_options.ValidationError): option.validate(1)
('site_url', config_options.URL()), # A description for the documentation project that will be added to the # HTML meta tags. # The name of the keywords to add to the HTML meta tags ('site_keywords', config_options.Type(str)), # The name of the keywords to add to the HTML meta tags ('site_description', config_options.Type(str)), # The name of the author to add to the HTML meta tags ('site_author', config_options.Type(str)), # The MkDocs theme for the documentation. ('theme', config_options.Theme(default='mkdocs')), # The directory containing the documentation markdown. ('docs_dir', config_options.Dir(default='docs', exists=True)), # The directory where the site will be built to ('site_dir', config_options.SiteDir(default='site')), # A copyright notice to add to the footer of documentation. ('copyright', config_options.Type(str)), # set of values for Google analytics containing the account IO and domain, # this should look like, ['UA-27795084-5', 'mkdocs.org'] ('google_analytics', config_options.Type(list, length=2)), # The address on which to serve the live reloading docs server. ('dev_addr', config_options.IpAddress(default='127.0.0.1:8000')), # If `True`, use `<page_name>/index.hmtl` style files with hyperlinks to
('pages', config_options.Nav()), # The full URL to where the documentation will be hosted ('site_url', config_options.URL()), # A description for the documentation project that will be added to the # HTML meta tags. ('site_description', config_options.Type(utils.string_types)), # The name of the author to add to the HTML meta tags ('site_author', config_options.Type(utils.string_types)), # The MkDocs theme for the documentation. ('theme', config_options.Theme(default='mkdocs')), # The directory containing the documentation markdown. ('docs_dir', config_options.Dir(default='source', exists=True)), ('source_dir', config_options.Dir(default='source', exists=True)), # The directory where the site will be built to ('site_dir', config_options.SiteDir(default='site')), # A copyright notice to add to the footer of documentation. ('copyright', config_options.Type(utils.string_types)), # set of values for Google analytics containing the account IO and domain, # this should look like, ['UA-27795084-5', 'mkdocs.org'] ('google_analytics', config_options.Type(list, length=2)), # The address on which to serve the live reloading docs server. ('dev_addr', config_options.IpAddress(default='127.0.0.1:8000')),