In [ ]:
# coding: utf-8

# The main source for the `nbd` jupyter application and its notebook creation tools.  The nbformat namespace was renamed to simplify the creation of new notebooks.  When someone is using `nbd` they should expect to make one python configuration file that is executed from the command line with 
# 
#         jupyter nbd --config config.py

# In[1]:


__all__ = 'markdown', 'notebook', 'code', 'raw', 'output', 'reads'


# In[2]:


from nbconvert.nbconvertapp import NbConvertApp
from traitlets import Any  # These should be properly assigned
import typing as t

class Docs(NbConvertApp):    
    loaders = Any(default_value=tuple(), help="""""").tag(config=True)
    post = Any(default_value=None, help="""""").tag(config=True)
    report = Any(default_value=None, help="""""").tag(config=True)
                
    def convert_notebooks(self):
        super().convert_notebooks() or self.report and [
            name and NbConvertApp.convert_single_notebook(
                    self, name, io.StringIO(writes(nb))) for name, nb in self.report()]
   
    def init_single_notebook_resources(self, notebook_filename):
        resources = super().init_single_notebook_resources(notebook_filename)
        resources['name'] = resources['unique_key'] = notebook_filename
        return resources    

main = launch_new_instance = Docs.launch_instance


# Continuing on attributes are appened to the `Docs` class as they become necessary.

# ## Callable Exporter

# In[3]:


from nbformat import reads, io, writes, NotebookNode

def identity(path, resources: dict=None, **kw) -> t.Tuple[NotebookNode, dict]:
    """callable to export ipynb files"""
    return reads(path.read_text(), 4), resources

class FuncExporter(__import__('nbconvert').exporters.html.HTMLExporter):
    callable = Any(identity)
    def from_filename(self, file_name: str, resources: dict=None, **kw) -> t.Tuple[str, dict]:
        html, resources = self.from_notebook_node(
            *self.callable(Path(file_name), resources, **kw), **kw)
        return html, resources


# Update the exporter each time a new notebook is accessed.

# In[4]:


def convert_single_notebook(self, name, buffer=None):    
    path = Path(name)
    for exts, exporter in reversed([*RULES, *self.loaders]):  # There is a better way to have default loaders
        if path.suffix[1:] in exts:
            self.exporter = (
                isinstance(exporter, __import__('nbconvert').exporters.base.Exporter)
                and exporter or FuncExporter(callable=exporter))
            return super(Docs, self).convert_single_notebook(name, buffer)
    else:
        self.log.warning("{} was not converted; there is no rule for the {} suffix.".format(str(path), path.suffix))

Docs.convert_single_notebook = convert_single_notebook


# ## The Loaders.
# 
# The default loader is notebooks only.  

# In[112]:


def minimal_style(callable: t.Callable[[str, dict], NotebookNode]
                 ) -> t.Callable[[str, dict], t.Tuple[NotebookNode, dict]]:
    """A minimal style wrapper other file types"""
    def _return_nb(path, resources=None, **kw):
        return notebook(cells=[
            markdown("""# [{}]({})""".format(str(path), str(path)+'.html')),
            callable(path.read_text())]), resources
    return _return_nb


# Notebooks are created using objects in the `nbformat` package; rename these objects to have shorter namespaces.

# ### Default loaders

# In[6]:


from nbformat.v4 import (
    new_code_cell     as code, 
    new_markdown_cell as markdown, 
    new_notebook      as notebook, 
    new_raw_cell      as raw, 
    new_output        as output)

RULES = [
    (('ipynb',), identity), 
    (('py', 'pyi'), minimal_style(code)), 
    (('md', 'markdown'), minimal_style(markdown)), 
    (('txt',), minimal_style(code))]


# ## Post Processing
# 
# The standard post processor recieves just the notebook name, this one recieves the exported text, resources, and name.

# In[7]:


class CallablePostProcessor(__import__('nbconvert').postprocessors.PostProcessorBase):
    """Call an arbitrary function after it has been writen to disk."""
    callable = Any().tag(config=True)    
    def postprocess(self, result: t.Tuple[str, dict, str]) -> None:
        html, resources, notebook_name = result
        self.callable(html, resources, notebook_name)


# In[8]:


def init_postprocessor(self):
    if self.post: self.postprocessor = CallablePostProcessor(callable=self.post)
    else:  super(Docs, self).init_postprocessor()

Docs.init_postprocessor = init_postprocessor


# ## Writing 
# 
# The post processor above requires a patched files writer.

# In[9]:


class FilesWriter(__import__('nbconvert').writers.FilesWriter):
    """Use the same name so configuration works the same.  This patch assures the parent directory exists and returns the output, resources, and name.
    """
    def write(self, output: str, resources: dict, notebook_name: str) -> t.Tuple[str, dict, str]:
        """Returns a tuple of the output html, resources, and file destination. These
        values are available to the post processor."""
        (lambda path: path.parent.exists() or path.parent.mkdir())(
            self.build_directory / Path(notebook_name))
        return output, resources, super().write(output, resources, notebook_name)


# Change the default directory path to docs to be consistent with github pages.

# In[10]:


FilesWriter.build_directory.default_value = 'docs'
Docs.writer_class.default_value = 'nbd.FilesWriter'


# > A Most Basic Index that lists the notebooks.

# In[111]:


def index(data):
    def _index(html, resources, destination):
        """"""
        data.cells.append(markdown("""# [{0}]({1})\n\n---\n\n""".format(
            resources.get('name'),resources.get('name') + destination.split(resources.get('name'), 1)[-1])))
    return _index


# If imports are needed in scope then don't import them.

# In[ ]:


from pathlib2 import Path