# -*- coding: utf-8 -*- """Simple Markdown writer for reStructuredText. See: * https://daringfireball.net/projects/markdown/syntax * https://guides.github.com/pdfs/markdown-cheatsheet-online.pdf ********************** Copyright and Licenses ********************** nb2plots ======== The nb2plots package, including all examples, code snippets and attached documentation is covered by the 2-clause BSD license. Copyright (c) 2015-2018, Matthew Brett All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 3rd party code and data ======================= Some code distributed within the nb2plots sources was developed by other projects. This code is distributed under their respective licenses that are listed below. Matplotlib plot extension ------------------------- The ``nbplots`` module is an edited version of ``matplotlib/lib/matplotlib/sphinxext/plot_directive.py``. Among the major changes are the following: * rename of 'plot' directive to 'nbplot'; * rename of 'plot_*' configuration options to 'nbplot_*' equivalent; * nbplot removes the option to point to Python script file rather than using the code in the directive; * nbplot keeps the namespace across directives in the same page, by default; * include-source is True by default; * nbplot can include multiple "parts" that can be selected via the "render-parts" and "run-parts" options; * do not use cached output when rebuilding page to avoid errors from stale output; * fix bug including empty parens when ``nofigs`` not specified (also fixed upstream); * empty ``setup_module`` to avoid breakage of nose doctests; * fix of plot doctest. The following license covers the plot directive code: LICENSE AGREEMENT FOR MATPLOTLIB 1.2.0 -------------------------------------- 1. This LICENSE AGREEMENT is between John D. Hunter ("JDH"), and the Individual or Organization ("Licensee") accessing and otherwise using matplotlib software in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, JDH hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use matplotlib 1.2.0 alone or in any derivative version, provided, however, that JDH's License Agreement and JDH's notice of copyright, i.e., "Copyright (c) 2002-2011 John D. Hunter; All Rights Reserved" are retained in matplotlib 1.2.0 alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates matplotlib 1.2.0 or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to matplotlib 1.2.0. 4. JDH is making matplotlib 1.2.0 available to Licensee on an "AS IS" basis. JDH MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, JDH MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB 1.2.0 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. JDH SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB 1.2.0 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING MATPLOTLIB 1.2.0, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between JDH and Licensee. This License Agreement does not grant permission to use JDH trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using matplotlib 1.2.0, Licensee agrees to be bound by the terms and conditions of this License Agreement. rst2md ------ The files nb2plots/doctree2md.py and scripts/rst2md derive from the "markdown.py" and "rst2md.py" files from "rst2md" project by Chris Wrench: https://github.com/cgwrench/rst2md ``rst2md`` has the following license: Copyright (c) 2012, C. G. Wrench All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ from __future__ import unicode_literals import re from textwrap import dedent import posixpath __docformat__ = 'reStructuredText' from docutils import frontend, nodes, writers, languages from collections import OrderedDict class IndentLevel(object): """Class to hold text being written for a certain indentation level. For example, all text in list_elements need to be indented. A list_element creates one of these indentation levels, and all text contained in the list_element gets written to this IndentLevel. When we leave the list_element, we ``write`` the text with suitable prefixes to the next level down, which might be the base of the document (document body) or another indentation level, if this is - for example - a nested list. In most respects, IndentLevel behaves like a list. """ def __init__(self, base, prefix, first_prefix=None): self.base = base # The list to which we eventually write self.prefix = prefix # Text prepended to lines # Text prepended to first list self.first_prefix = prefix if first_prefix is None else first_prefix # Our own list to which we append before doing a ``write`` self.content = [] def append(self, new): self.content.append(new) def __getitem__(self, index): return self.content[index] def __len__(self): return len(self.content) def __bool__(self): return len(self) != 0 def write(self): """Add ``self.contents`` with current ``prefix`` and ``first_prefix`` Add processed ``self.contents`` to ``self.base``. The first line has ``first_prefix`` prepended, further lines have ``prefix`` prepended. Empty (all whitespace) lines get written as bare carriage returns, to avoid ugly extra whitespace. """ string = ''.join(self.content) lines = string.splitlines(True) if len(lines) == 0: return texts = [self.first_prefix + lines[0]] for line in lines[1:]: if line.strip() == '': # avoid prefix for empty lines texts.append('\n') else: texts.append(self.prefix + line) self.base.append(''.join(texts)) def _make_method(to_add): """Make a method that adds `to_add` We need this function so that `to_add` is a fresh and unique variable at the time the method is defined. """ def method(self, node): self.add(to_add) return method def add_pref_suff(pref_suff_map): """Decorator adds visit, depart methods for prefix/suffix pairs.""" def dec(cls): # Need _make_method to ensure new variable picked up for each iteration # of the loop. The defined method picks up this new variable in its # scope. for key, (prefix, suffix) in pref_suff_map.items(): setattr(cls, 'visit_' + key, _make_method(prefix)) setattr(cls, 'depart_' + key, _make_method(suffix)) return cls return dec def add_pass_thru(pass_thrus): """Decorator adds explicit pass-through visit and depart methods.""" def meth(self, node): pass def dec(cls): for element_name in pass_thrus: for meth_prefix in ('visit_', 'depart_'): meth_name = meth_prefix + element_name if hasattr(cls, meth_name): raise ValueError( 'method name {} already defined'.format(meth_name) ) setattr(cls, meth_name, meth) return cls return dec # Characters that should be escaped in Markdown ESCAPE_RE = re.compile(r'([\\*`])') # Doctree elements for which Markdown element is PREF_SUFF_ELEMENTS = { 'emphasis': ('*', '*'), # Could also use ('_', '_') 'strong': ('**', '**'), # Could also use ('__', '__') 'literal': ('`', '`'), 'subscript': ('', ''), 'superscript': ('', ''), } # Doctree elements explicitly passed through without extra markup PASS_THRU_ELEMENTS = ( 'document', 'container', 'target', 'inline', 'definition_list', 'definition_list_item', 'term', 'field_list', 'field_list_item', 'field', 'field_name', 'mpl_hint', 'pending_xref', 'compound', 'line', 'line_block', ) @add_pass_thru(PASS_THRU_ELEMENTS) @add_pref_suff(PREF_SUFF_ELEMENTS) class Translator(nodes.NodeVisitor): std_indent = ' ' def __init__(self, document, builder=None): nodes.NodeVisitor.__init__(self, document) self.builder = builder self.settings = settings = document.settings lcode = settings.language_code self.language = languages.get_language(lcode, document.reporter) # Not-None here indicates Markdown should use HTTP for internal and # download links. self.markdown_http_base = ( builder.markdown_http_base if builder else None ) # Warn only once per writer about unsupported elements self._warned = set() # Lookup table to get section list from name self._lists = OrderedDict((('head', []), ('body', []), ('foot', []))) # Reset attributes modified by reading self.reset() # Attribute shortcuts self.head, self.body, self.foot = self._lists.values() def reset(self): """Initialize object for fresh read.""" for part in self._lists.values(): part[:] = [] # Current section heading level during writing self.section_level = 0 # FIFO list of list prefixes, while writing nested lists. Each element # corresponds to one level of nesting. Thus ['1. ', '1. ', '* '] would # occur when writing items of an unordered list, that is nested within # an ordered list, that in turn is nested in another ordered list. self.list_prefixes = [] # FIFO list of indentation levels. When we are writing a block of text # that should be indented, we create a new indentation level. We only # write the text when we leave the indentation level, so we can insert # the correct prefix for every line. self.indent_levels = [] # Flag indicating we are processing docinfo items self._in_docinfo = False # Flag for whether to escape characters self._escape_text = True def astext(self): """Return the final formatted document as a string.""" parts = [''.join(lines).strip() for lines in self._lists.values()] parts = [part + '\n\n' for part in parts if part] return ''.join(parts).strip() + '\n' def ensure_eol(self): """Ensure the last line in current base is terminated by new line.""" out = self.get_current_output() if out and out[-1] and out[-1][-1] != '\n': out.append('\n') def get_current_output(self, section='body'): """Get list or IndentLevel to which we are currently writing.""" return ( self.indent_levels[-1] if self.indent_levels else self._lists[section] ) def add(self, string, section='body'): """Add `string` to `section` or current output. Parameters ---------- string : str String to add to output document section : {'body', 'head', 'foot'}, optional Section of document that generated text should be appended to, if not already appending to an indent level. If already appending to an indent level, method will ignore `section`. Use :meth:`add_section` to append to a section unconditionally. """ self.get_current_output(section).append(string) def add_section(self, string, section='body'): """Add `string` to `section` regardless of current output. Can be useful when forcing write to header or footer. Parameters ---------- string : str String to add to output document section : {'body', 'head', 'foot'}, optional Section of document that generated text should be appended to. """ self._lists[section].append(string) def start_level(self, prefix, first_prefix=None, section='body'): """Create a new IndentLevel with `prefix` and `first_prefix`""" base = ( self.indent_levels[-1].content if self.indent_levels else self._lists[section] ) level = IndentLevel(base, prefix, first_prefix) self.indent_levels.append(level) def finish_level(self): """Remove most recent IndentLevel and write contents.""" level = self.indent_levels.pop() level.write() def escape_chars(self, txt): # Escape (some) characters with special meaning for Markdown return ESCAPE_RE.sub(r'\\\1', txt) def visit_Text(self, node): text = node.astext().replace('\r\n', '\n') if self._escape_text: text = self.escape_chars(text) self.add(text) def depart_Text(self, node): pass def visit_comment(self, node): txt = node.astext() if txt.strip(): self.add('\n') raise nodes.SkipNode def visit_docinfo(self, node): self._in_docinfo = True def depart_docinfo(self, node): self._in_docinfo = False def process_docinfo_item(self, node): """Called explicitly from methods in this class.""" self.add_section('% {}\n'.format(node.astext()), section='head') raise nodes.SkipNode def visit_definition(self, node): self.add('\n\n') self.start_level(' ') def depart_definition(self, node): self.finish_level() visit_field_body = visit_definition depart_field_body = depart_definition def visit_paragraph(self, node): pass def depart_paragraph(self, node): self.ensure_eol() self.add('\n') def visit_math_block(self, node): # docutils math block self._escape_text = False self.add('$$\n') def depart_math_block(self, node): self._escape_text = True self.ensure_eol() self.add('$$\n\n') def visit_displaymath(self, node): # sphinx math blocks become displaymath self.add('$$\n{}\n$$\n\n'.format(node['latex'])) raise nodes.SkipNode def visit_math(self, node): # sphinx math node has 'latex' attribute, docutils does not if 'latex' in node: # sphinx math node self.add('${}$'.format(node['latex'])) raise nodes.SkipNode # docutils math node self._escape_text = False self.add('$') def depart_math(self, node): # sphinx node skipped in visit, only docutils gets here self._escape_text = True self.add('$') def visit_literal_block(self, node): self._escape_text = False code_type = node['classes'][1] if 'code' in node['classes'] else '' if node.get('force_highlighting', False) and 'language' in node: code_type = node['language'] self.add('```' + code_type + '\n') def depart_literal_block(self, node): self._escape_text = True self.ensure_eol() self.add('```\n\n') def visit_doctest_block(self, node): self._escape_text = False self.add('```python\n') depart_doctest_block = depart_literal_block def visit_block_quote(self, node): self.start_level('> ') def depart_block_quote(self, node): self.finish_level() def visit_section(self, node): self.section_level += 1 def depart_section(self, node): self.section_level -= 1 def visit_enumerated_list(self, node): self.list_prefixes.append('1. ') def depart_enumerated_list(self, node): self.list_prefixes.pop() def visit_bullet_list(self, node): self.list_prefixes.append('* ') depart_bullet_list = depart_enumerated_list def visit_list_item(self, node): first_prefix = self.list_prefixes[-1] prefix = ' ' * len(first_prefix) self.start_level(prefix, first_prefix) def depart_list_item(self, node): self.finish_level() def visit_problematic(self, node): self.add('\n\n```\n{}\n```\n\n'.format(node.astext())) raise nodes.SkipNode def visit_system_message(self, node): if node['level'] < self.document.reporter.report_level: # Level is too low to display raise nodes.SkipNode line = ', line %s' % node['line'] if node.hasattr('line') else '' self.add( '```\nSystem Message: {}:{}\n\n{}\n```\n\n'.format( node['source'], line, node.astext() ) ) raise nodes.SkipNode def visit_title(self, node): self.add((self.section_level + 1) * '#' + ' ') def depart_title(self, node): self.ensure_eol() self.add('\n') def visit_subtitle(self, node): self.add((self.section_level + 2) * '#' + ' ') depart_subtitle = depart_title def visit_transition(self, node): # Simply replace a transition by a horizontal rule. # Could use three or more '*', '_' or '-'. self.add('\n---\n\n') raise nodes.SkipNode def _refuri2http(self, node): # Replace 'refuri' in reference with HTTP address, if possible # None for no possible address url = node.get('refuri') if not node.get('internal'): return url # If HTTP page build URL known, make link relative to that. if not self.markdown_http_base: return None this_doc = self.builder.current_docname if url in (None, ''): # Reference to this doc url = self.builder.get_target_uri(this_doc) else: # URL is relative to the current docname. this_dir = posixpath.dirname(this_doc) if this_dir: url = posixpath.normpath('{}/{}'.format(this_dir, url)) url = '{}/{}'.format(self.markdown_http_base, url) if 'refid' in node: url += '#' + node['refid'] return url def visit_reference(self, node): # If no target possible, pass through. url = self._refuri2http(node) if url is None: return self.add('[') for child in node.children: child.walkabout(self) self.add(']({})'.format(url)) raise nodes.SkipNode def depart_reference(self, node): pass def visit_nbplot_epilogue(self, node): raise nodes.SkipNode def visit_nbplot_not_rendered(self, node): raise nodes.SkipNode def visit_code_links(self, node): raise nodes.SkipNode def visit_index(self, node): # Drop index entries raise nodes.SkipNode def visit_substitution_definition(self, node): # Drop substitution definitions - the doctree already contains the text # with substitutions applied. raise nodes.SkipNode def visit_only(self, node): if node['expr'] == 'markdown': self.add(dedent(node.astext()) + '\n') raise nodes.SkipNode def visit_runrole_reference(self, node): raise nodes.SkipNode def visit_download_reference(self, node): # If not resolving internal links, or there is no filename specified, # pass through. filename = node.get('filename') if None in (self.markdown_http_base, filename): return target_url = '{}/_downloads/{}'.format( self.markdown_http_base, filename ) self.add('[{}]({})'.format(node.astext(), target_url)) raise nodes.SkipNode def depart_download_reference(self, node): pass visit_compact_paragraph = visit_paragraph depart_compact_paragraph = depart_paragraph def visit_nbplot_container(self, node): pass def depart_nbplot_container(self, node): pass def unknown_visit(self, node): """Warn once per instance for unsupported nodes. Intercept docinfo items if in docinfo block. """ if self._in_docinfo: self.process_docinfo_item(node) return # We really don't know this node type, warn once per node type node_type = node.__class__.__name__ if node_type not in self._warned: self.document.reporter.warning( 'The ' + node_type + ' element not yet supported in Markdown.' ) self._warned.add(node_type) raise nodes.SkipNode class Writer(writers.Writer): supported = ('markdown', ) """Formats this writer supports.""" output = None """Final translated form of `document`.""" # Add configuration settings for additional Markdown flavours here. settings_spec = ( 'Markdown-Specific Options', None, ( ( 'Extended Markdown syntax.', ['--extended-markdown'], { 'default': 0, 'action': 'store_true', 'validator': frontend.validate_boolean } ), ( 'Strict Markdown syntax. Default: true', ['--strict-markdown'], { 'default': 1, 'action': 'store_true', 'validator': frontend.validate_boolean } ), ) ) translator_class = Translator def __init__(self, builder=None): writers.Writer.__init__(self) self.builder = builder def translate(self): visitor = self.translator_class(self.document, self.builder) self.document.walkabout(visitor) self.output = visitor.astext()