From 2869ac2bb9c8f3866b39d67feff990da7a992f3d Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Mon, 13 Jul 2015 19:19:00 -0400 Subject: [PATCH] implementing feedback from @rossant --- README.md | 43 ++++++++++++++++++++------------ examples/ex4.notebook.ipynb | 6 ++--- examples/ex4.py | 2 +- ipymd/core/contents_manager.py | 10 ++++---- ipymd/core/format_manager.py | 28 +++++++++++++-------- ipymd/formats/markdown.py | 45 +++++++++++++++------------------- ipymd/formats/notebook.py | 13 +++++++--- ipymd/formats/tests/_utils.py | 3 --- ipymd/lib/markdown.py | 1 - 9 files changed, 84 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index c8d9cc9..ed3e08a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +--- +celltoolbar: Slideshow +--- + +--- +slideshow: + slide_type: slide +... + [![Build Status](https://travis-ci.org/rossant/ipymd.svg?branch=travis)](https://travis-ci.org/rossant/ipymd) [![Coverage Status](https://coveralls.io/repos/rossant/ipymd/badge.svg)](https://coveralls.io/r/rossant/ipymd) @@ -45,7 +54,7 @@ with: The JSON `.ipynb` are removed from the equation, and the conversion happens on the fly. The IPython Notebook becomes an interactive Markdown text editor! -A drawback is that you prompt numbers and images (for now). +A drawback is that you lose prompt numbers and images (for now). This is useful when you write technical documents, blog posts, books, etc. @@ -214,13 +223,11 @@ You can convert from any supported format to any supported format. This works by An **ipymd cell** is a Python dictionary with the following fields: -* `cell_type`: `markdown` or `code` +* `cell_type`: `markdown`, `code` or `notebok_metadata` (if implemented) * `input`: a string with the code input (code cell only) * `output`: a string with the text output and stdout (code cell only) * `source`: a string containing Markdown markup (markdown cell only) * `metadata`: a dictionary containing cell (or notebook) metadata -* `is_notebook`: a boolean telling whether this cell's metadata is the notebook - metadata ### Customize the Markdown format @@ -241,7 +248,8 @@ You can also implement your own format by following these instructions: * To activate this format, call this at Notebook launch time (not in a kernel!), perhaps in your `ipython_notebook_config.py`: - ```python +```python + from ipymd import format_manager format_manager().register( name='my_format', @@ -250,7 +258,7 @@ You can also implement your own format by following these instructions: file_extension='.md', # or anything else file_type='text', # or JSON ) - ``` +``` * Now you can convert contents: `ipymd.convert(contents, from_='notebook', to='my_format')` or any other combination. @@ -258,18 +266,20 @@ You can also implement your own format by following these instructions: * To further integrate your format in ipymd, create a `ipymd/formats/my_format.py` file. * Put your reader and writer class in there, as well as a top-level variable: - ```python +```python + MY_FORMAT = dict( reader=MyFormatReader, writer=MyFormatWriter, file_extension='.md', file_type='text', ) - ``` +``` * In `setup.py`, add this to `entry_points`: - ```python +```python + ... entry_points={ 'ipymd.format': [ @@ -278,10 +288,10 @@ You can also implement your own format by following these instructions: ... ] } - ``` +``` > Note that the `entry_point` name will be used by default. you may override - it, if you like, but Don't Repeat Yourself. + it, if you like, but Don't Repeat Yourself. * Add some unit tests in `ipymd/formats/tests`. * Propose a PR! @@ -293,18 +303,20 @@ Look at the existing format implementations for more details. * If you want to be able to redistribute your format without adding it to ipymd proper (i.e. in-house or experimental), implement all your code in a real python module. * Someplace easy to import, e.g. `myformat.py` or `myformat/__init__.py`, add: - ```python +```python + MY_FORMAT = dict( reader=MyFormatReader, writer=MyFormatWriter, file_extension='.md', # or anything else file_type='text', # or JSON ) - ``` +``` and this to your `setup.py`: - ```python +```python + ... entry_points={ 'ipymd.format': [ @@ -312,6 +324,7 @@ Look at the existing format implementations for more details. ], }, ... - ``` +``` + * Publish on pypi! * Your users will now be able to `pip install myformat`, then configure their Notebook to use your format with the name `my_format`. diff --git a/examples/ex4.notebook.ipynb b/examples/ex4.notebook.ipynb index 71161c5..56c998b 100644 --- a/examples/ex4.notebook.ipynb +++ b/examples/ex4.notebook.ipynb @@ -1,4 +1,5 @@ { + "nbformat": 4, "cells": [ { "cell_type": "markdown", @@ -66,9 +67,8 @@ } } ], - "nbformat_minor": 0, - "nbformat": 4, "metadata": { "title": "A Slideshow" - } + }, + "nbformat_minor": 0 } diff --git a/examples/ex4.py b/examples/ex4.py index 0e1ed7e..c770dcb 100644 --- a/examples/ex4.py +++ b/examples/ex4.py @@ -1,7 +1,7 @@ # List of ipymd cells expected for this example. output = [ - {'is_notebook': True, + {'cell_type': 'notebook_metadata', 'metadata': {'title': 'A Slideshow'}}, {'cell_type': 'markdown', diff --git a/ipymd/core/contents_manager.py b/ipymd/core/contents_manager.py index 62e71df..47f2d78 100644 --- a/ipymd/core/contents_manager.py +++ b/ipymd/core/contents_manager.py @@ -32,13 +32,13 @@ def _file_extension(os_path): class IPymdContentsManager(FileContentsManager, Configurable): format = Unicode('markdown', config=True) - # The name of the default kernel: if left blank, assume native (pythonX) - # won't store kernelspec/language_info unless forced - # this will be passed to the FormatManager, overwriting any config there + # The name of the default kernel: if left blank, assume native (pythonX), + # won't store kernelspec/language_info unless forced with verbose_metadata. + # This will be passed to the FormatManager, overwriting any config there. default_kernel_name = Unicode(config=True) - # don't strip any metadata - # this will be passed to the FormatManager, overwriting any config there + # Don't strip any metadata. + # This will be passed to the FormatManager, overwriting any config there. verbose_metadata = Bool(False, config=True) def __init__(self, *args, **kwargs): diff --git a/ipymd/core/format_manager.py b/ipymd/core/format_manager.py index a6b0653..a363d13 100644 --- a/ipymd/core/format_manager.py +++ b/ipymd/core/format_manager.py @@ -39,16 +39,16 @@ class FormatManager(LoggingConfigurable): # The name of the setup_tools entry point group to use in setup.py entry_point_group = "ipymd.format" - # The name of the default kernel: if left blank, assume native (pythonX) + # The name of the default kernel: if left blank, assume native (pythonX), # won't store kernelspec/language_info unless forced # TODO: where does this get set but by the ContentsManager? default_kernel_name = Unicode(config=True) - # don't strip any metadata + # Don't strip any metadata # TODO: where does this get set but by the ContentsManager? verbose_metadata = Bool(False, config=True) - # the singleton. there can be only one. + # The singleton. There can be only one. _instance = None def __init__(self, *args, **kwargs): @@ -254,16 +254,24 @@ def convert(self, # a list of ipymd cells. cells = contents + notebook_metadata = [cell for cell in cells + if cell["cell_type"] == "notebook_metadata"] + if writer is not None: + if notebook_metadata: + notebook_metadata = notebook_metadata[0]["metadata"] + if hasattr(writer, "write_notebook_metadata"): + writer.write_notebook_metadata(notebook_metadata) + else: + print("{} does not support notebook metadata, " + "dropping metadata: {}".format( + writer, + notebook_metadata)) + # Convert from ipymd cells to the target format. - supports_nb_metadata = hasattr(writer, "write_notebook_metadata") + for cell in cells: + writer.write(cell) - for i, cell in enumerate(cells): - if cell.get("is_notebook", None) and supports_nb_metadata: - writer.write_notebook_metadata( - self.clean_meta(cell["metadata"])) - else: - writer.write(cell) return writer.contents else: # If no writer is specified, the output is supposed to be diff --git a/ipymd/formats/markdown.py b/ipymd/formats/markdown.py index 8393eef..6f71add 100644 --- a/ipymd/formats/markdown.py +++ b/ipymd/formats/markdown.py @@ -22,9 +22,6 @@ from ..core.prompt import create_prompt -CLOSE_NOTEBOOK_METADATA = "---" -CLOSE_CELL_METADATA = "..." - #------------------------------------------------------------------------------ # Base Markdown @@ -70,9 +67,9 @@ def _meta(self, source, is_notebook=False): """Turn a YAML string into ipynb cell/notebook metadata """ if is_notebook: - return {'is_notebook': True, + return {'cell_type': 'notebook_metadata', 'metadata': source} - return {'is_meta': True, + return {'cell_type': 'cell_metadata', 'metadata': source} def _markdown_cell_from_regex(self, m): @@ -96,17 +93,17 @@ def _meta_from_regex(self, m): Both must be followed by at least one blank line (\n\n). """ - body = m.group("body") - is_notebook = m.group("sep_close") == CLOSE_NOTEBOOK_METADATA + body = m.group('body') + is_notebook = m.group('sep_close') == '---' if is_notebook: - # make it into a valid YAML object - body = body.strip()[:-3] + CLOSE_CELL_METADATA + # make it into a valid YAML object by stripping --- + body = body.strip()[:-3] + '...' try: if body: - return self._meta(yaml.safe_load(m.group("body")), is_notebook) + return self._meta(yaml.safe_load(m.group('body')), is_notebook) else: - return self._meta({"ipymd": {"empty_meta": True}}, is_notebook) + return self._meta({'ipymd': {'empty_meta': True}}, is_notebook) except Exception as err: raise Exception(body, err) @@ -124,7 +121,7 @@ def meta(self, source, is_notebook=False): if source is None: return '' - if source.get("ipymd", {}).get("empty_meta", None): + if source.get('ipymd', {}).get('empty_meta', None): return '---\n\n' if not source: @@ -138,7 +135,8 @@ def meta(self, source, is_notebook=False): default_flow_style=False)) if is_notebook: - meta = meta[:-5] + "---\n\n" + # Replace the trailing `...\n\n` + meta = meta[:-5] + '---\n\n' return meta @@ -181,23 +179,20 @@ class MarkdownReader(BaseMarkdownReader): def __init__(self, prompt=None): super(MarkdownReader, self).__init__() self._prompt = create_prompt(prompt) + self._notebook_metadata = {} def read(self, text, rules=None): - cells_and_meta = super(MarkdownReader, self).read(text, rules) + raw_cells = super(MarkdownReader, self).read(text, rules) cells = [] - for i, cell_or_meta in enumerate(cells_and_meta): - if cell_or_meta.get("is_notebook", None): - if cells: - raise ValueError("Notebook metadata must appear first") - cells.append(cell_or_meta) - elif ( - cell_or_meta.get("is_meta", None) and cell_or_meta["metadata"] - ): - cells_and_meta[i + 1].update( - metadata=cell_or_meta["metadata"]) + last_index = len(raw_cells) - 1 + + for i, cell in enumerate(raw_cells): + if cell['cell_type'] == 'cell_metadata': + if i + 1 <= last_index: + raw_cells[i + 1].update(metadata=cell['metadata']) else: - cells.append(cell_or_meta) + cells.append(cell) return cells diff --git a/ipymd/formats/notebook.py b/ipymd/formats/notebook.py index 458909a..1686934 100644 --- a/ipymd/formats/notebook.py +++ b/ipymd/formats/notebook.py @@ -66,12 +66,14 @@ class NotebookReader(object): ignore_meta = ["collapsed", "trusted"] + def __init__(self): + self._notebook_metadata = {} + def read(self, nb): assert nb['nbformat'] >= 4 - yield { - "is_notebook": True, - "metadata": nb["metadata"] - } + + self._notebook_metadata = nb['metadata'] + for cell in nb['cells']: ipymd_cell = {} metadata = self.clean_meta(cell) @@ -88,6 +90,9 @@ def read(self, nb): continue yield ipymd_cell + def read_notebook_metadata(self): + return self._notebook_metadata + def clean_meta(self, cell): metadata = cell.get('metadata', {}) for key in self.ignore_meta: diff --git a/ipymd/formats/tests/_utils.py b/ipymd/formats/tests/_utils.py index 95d49f1..bd137c4 100644 --- a/ipymd/formats/tests/_utils.py +++ b/ipymd/formats/tests/_utils.py @@ -56,9 +56,6 @@ def _test_reader(basename, format, ignore_notebook_meta=True): """Return converted and expected ipymd cells of a given example.""" contents = _read_test_file(basename, format) converted = convert(contents, from_=format) - if ignore_notebook_meta: - converted = [cell for cell in converted - if not cell.get("is_notebook", None)] expected = _exec_test_file(basename) return converted, expected diff --git a/ipymd/lib/markdown.py b/ipymd/lib/markdown.py index 33c35b3..2d7ea24 100644 --- a/ipymd/lib/markdown.py +++ b/ipymd/lib/markdown.py @@ -338,7 +338,6 @@ def parse_text(self, m): self.renderer.text(text) def parse_meta(self, m): - # raise Exception(m.groupdict()) if not m.group("alias") and not m.group("body"): self.renderer.text("META SPLIT") elif m.group("alias") and not m.group("body"):