├── ipymd ├── core │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_core.py │ │ ├── test_scripts.py │ │ └── test_prompt.py │ ├── scripts.py │ ├── contents_manager.py │ ├── prompt.py │ └── format_manager.py ├── ext │ └── __init__.py ├── lib │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_python.py │ │ ├── test_base_lexer.py │ │ ├── test_opendocument.py │ │ └── test_markdown.py │ ├── python.py │ ├── base_lexer.py │ └── markdown.py ├── utils │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_tempdir.py │ │ └── test_utils.py │ ├── tempdir.py │ └── utils.py ├── formats │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_utils.py │ │ ├── test_atlas.py │ │ ├── test_notebook.py │ │ ├── _utils.py │ │ ├── test_opendocument.py │ │ ├── test_markdown.py │ │ └── test_python.py │ ├── opendocument.py │ ├── atlas.py │ ├── notebook.py │ ├── python.py │ └── markdown.py └── __init__.py ├── setup.cfg ├── examples ├── ex1.opendocument.odt ├── ex2.opendocument.odt ├── ex1.python.py ├── ex1.markdown.md ├── ex1.atlas.md ├── ex3.markdown.md ├── ex1.py ├── ex4.markdown.md ├── ex3.py ├── ex4.py ├── ex2.python.py ├── ex2.markdown.md ├── ex1.notebook.ipynb ├── ex3.notebook.ipynb ├── ex4.notebook.ipynb ├── ex2.atlas.md ├── ex2.py └── ex2.notebook.ipynb ├── requirements-dev.txt ├── .coveragerc ├── MANIFEST.in ├── .gitignore ├── Makefile ├── .travis.yml ├── LICENSE.txt ├── setup.py └── README.md /ipymd/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ipymd/ext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ipymd/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ipymd/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ipymd/core/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ipymd/formats/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ipymd/lib/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ipymd/formats/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ipymd/utils/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = --cov-report term-missing --cov ipymd 3 | -------------------------------------------------------------------------------- /examples/ex1.opendocument.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossant/ipymd/HEAD/examples/ex1.opendocument.odt -------------------------------------------------------------------------------- /examples/ex2.opendocument.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossant/ipymd/HEAD/examples/ex2.opendocument.odt -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | coveralls 2 | flake8 3 | https://github.com/eea/odfpy/archive/master.zip 4 | pytest 5 | pytest-cov 6 | python-coveralls 7 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = ipymd 4 | omit = 5 | ipymd/ext/six.py 6 | ipymd/core/contents_manager.py 7 | ipymd/utils/tempdir.py 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include Makefile 3 | include README.md 4 | 5 | graft examples 6 | graft ipymd 7 | 8 | global-exclude *.pyc 9 | global-exclude *.pyo 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | __pycache__ 3 | experimental 4 | wiki 5 | _old 6 | notebooks 7 | images 8 | *checkpoints* 9 | .fuse* 10 | *.pyc 11 | *.egg-info 12 | build/ 13 | dist/ 14 | .eggs/ 15 | -------------------------------------------------------------------------------- /examples/ex1.python.py: -------------------------------------------------------------------------------- 1 | # # Header 2 | 3 | # A paragraph. 4 | 5 | # Python code: 6 | 7 | print("Hello world!") 8 | 9 | # JavaScript code: 10 | 11 | # ```javascript 12 | # console.log("Hello world!"); 13 | # ``` 14 | -------------------------------------------------------------------------------- /ipymd/__init__.py: -------------------------------------------------------------------------------- 1 | from . import formats 2 | from .core.format_manager import convert, format_manager 3 | from .core.contents_manager import IPymdContentsManager 4 | from .core.scripts import convert_files 5 | 6 | 7 | __version__ = '0.1.3' 8 | -------------------------------------------------------------------------------- /examples/ex1.markdown.md: -------------------------------------------------------------------------------- 1 | # Header 2 | 3 | A paragraph. 4 | 5 | Python code: 6 | 7 | ```python 8 | >>> print("Hello world!") 9 | Hello world! 10 | ``` 11 | 12 | JavaScript code: 13 | 14 | ```javascript 15 | console.log("Hello world!"); 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/ex1.atlas.md: -------------------------------------------------------------------------------- 1 | # Header 2 | 3 | A paragraph. 4 | 5 | Python code: 6 | 7 |
10 | print("Hello world!")
11 | 
12 | 13 | JavaScript code: 14 | 15 | ```javascript 16 | console.log("Hello world!"); 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/ex3.markdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | slideshow: 3 | slide_type: slide 4 | ... 5 | 6 | # Header 7 | 8 | --- 9 | slideshow: 10 | slide_type: slide 11 | ... 12 | 13 | A paragraph. 14 | 15 | --- 16 | 17 | Python code: 18 | 19 | --- 20 | slideshow: 21 | slide_type: fragment 22 | ... 23 | 24 | ```python 25 | >>> print("Hello world!") 26 | Hello world! 27 | ``` 28 | 29 | --- 30 | slideshow: 31 | slide_type: skip 32 | ... 33 | 34 | JavaScript code: 35 | 36 | --- 37 | slideshow: 38 | slide_type: skip 39 | ... 40 | 41 | ```javascript 42 | console.log("Hello world!"); 43 | ``` 44 | -------------------------------------------------------------------------------- /examples/ex1.py: -------------------------------------------------------------------------------- 1 | # List of ipymd cells expected for this example. 2 | output = [ 3 | 4 | {'cell_type': 'markdown', 5 | 'source': '# Header'}, 6 | 7 | {'cell_type': 'markdown', 8 | 'source': 'A paragraph.'}, 9 | 10 | {'cell_type': 'markdown', 11 | 'source': 'Python code:'}, 12 | 13 | {'cell_type': 'code', 14 | 'input': 'print("Hello world!")', 15 | 'output': 'Hello world!'}, 16 | 17 | {'cell_type': 'markdown', 18 | 'source': 'JavaScript code:'}, 19 | 20 | {'cell_type': 'markdown', 21 | 'source': '```javascript\nconsole.log("Hello world!");\n```'} 22 | 23 | ] 24 | -------------------------------------------------------------------------------- /examples/ex4.markdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: A Slideshow 3 | --- 4 | 5 | --- 6 | slideshow: 7 | slide_type: slide 8 | ... 9 | 10 | # Header 11 | 12 | --- 13 | slideshow: 14 | slide_type: slide 15 | ... 16 | 17 | A paragraph. 18 | 19 | --- 20 | 21 | Python code: 22 | 23 | --- 24 | slideshow: 25 | slide_type: fragment 26 | ... 27 | 28 | ```python 29 | >>> print("Hello world!") 30 | Hello world! 31 | ``` 32 | 33 | --- 34 | slideshow: 35 | slide_type: skip 36 | ... 37 | 38 | JavaScript code: 39 | 40 | --- 41 | slideshow: 42 | slide_type: skip 43 | ... 44 | 45 | ```javascript 46 | console.log("Hello world!"); 47 | ``` 48 | -------------------------------------------------------------------------------- /ipymd/utils/tests/test_tempdir.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test tempdir.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | from ..tempdir import TemporaryDirectory 10 | 11 | 12 | #------------------------------------------------------------------------------ 13 | # Tests 14 | #------------------------------------------------------------------------------ 15 | 16 | def test_temporary_directory(): 17 | with TemporaryDirectory() as temporary_directory: 18 | assert temporary_directory is not None 19 | -------------------------------------------------------------------------------- /ipymd/utils/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test utils.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | from ..utils import _diff 10 | 11 | 12 | #------------------------------------------------------------------------------ 13 | # Tests 14 | #------------------------------------------------------------------------------ 15 | 16 | def test_diff(): 17 | s = 'abcdef ghijkl' 18 | assert _diff(s, s) == '' 19 | 20 | assert _diff(s, ' ' + s) == s 21 | assert _diff(s, s + ' ') == s 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "clean - remove all build, test, coverage and Python artifacts" 3 | @echo "clean-build - remove build artifacts" 4 | @echo "clean-pyc - remove Python file artifacts" 5 | @echo "lint - check style with flake8" 6 | @echo "test - run tests quickly with the default Python" 7 | 8 | clean: clean-build clean-pyc 9 | 10 | clean-build: 11 | rm -fr build/ 12 | rm -fr dist/ 13 | rm -fr *.egg-info 14 | 15 | clean-pyc: 16 | find . -name '*.pyc' -exec rm -f {} + 17 | find . -name '*.pyo' -exec rm -f {} + 18 | find . -name '*~' -exec rm -f {} + 19 | find . -name '__pycache__' -exec rm -fr {} + 20 | 21 | lint: 22 | flake8 ipymd setup.py --exclude=ipymd/ext/six.py,ipymd/core/contents_manager.py --ignore=E226,E265,F401,F403,F811 23 | 24 | test: lint 25 | python setup.py test 26 | -------------------------------------------------------------------------------- /ipymd/formats/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test utils.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | import os.path as op 10 | from ._utils import _test_file_path, _exec_test_file 11 | 12 | 13 | #------------------------------------------------------------------------------ 14 | # Test Markdown parser 15 | #------------------------------------------------------------------------------ 16 | 17 | def test_file_path(): 18 | filename = 'ex1' 19 | assert op.exists(_test_file_path(filename, 'markdown')) 20 | 21 | 22 | def test_exec_test_file(): 23 | filename = 'ex1' 24 | assert isinstance(_exec_test_file(filename), list) 25 | -------------------------------------------------------------------------------- /ipymd/lib/tests/test_python.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test Python routines.""" 4 | 5 | # ----------------------------------------------------------------------------- 6 | # Imports 7 | # ----------------------------------------------------------------------------- 8 | 9 | from ..python import _is_python, PythonFilter 10 | 11 | 12 | # ----------------------------------------------------------------------------- 13 | # Test Python 14 | # ----------------------------------------------------------------------------- 15 | 16 | def test_python(): 17 | assert _is_python("print('Hello world!')") 18 | assert not _is_python("Hello world!") 19 | 20 | 21 | def test_python_filter(): 22 | filter = PythonFilter() 23 | assert filter('a\nb # ipymd-skip\nc\n') == 'a\nb # ipymd-skip\nc' 24 | 25 | filter = PythonFilter(ipymd_skip=True) 26 | assert filter('a\nb # ipymd-skip\nc\n') == 'a\nc' 27 | -------------------------------------------------------------------------------- /examples/ex3.py: -------------------------------------------------------------------------------- 1 | # List of ipymd cells expected for this example. 2 | output = [ 3 | 4 | {'cell_type': 'markdown', 5 | 'source': '# Header', 6 | 'metadata': {'slideshow': {'slide_type': 'slide'}}}, 7 | 8 | {'cell_type': 'markdown', 9 | 'source': 'A paragraph.', 10 | 'metadata': {'slideshow': {'slide_type': 'slide'}}}, 11 | 12 | {'cell_type': 'markdown', 13 | 'source': 'Python code:', 14 | 'metadata': {'ipymd': {'empty_meta': True}}}, 15 | 16 | {'cell_type': 'code', 17 | 'input': 'print("Hello world!")', 18 | 'output': 'Hello world!', 19 | 'metadata': {'slideshow': {'slide_type': 'fragment'}}}, 20 | 21 | {'cell_type': 'markdown', 22 | 'source': 'JavaScript code:', 23 | 'metadata': {'slideshow': {'slide_type': 'skip'}}}, 24 | 25 | {'cell_type': 'markdown', 26 | 'source': '```javascript\nconsole.log("Hello world!");\n```', 27 | 'metadata': {'slideshow': {'slide_type': 'skip'}}}, 28 | 29 | ] 30 | -------------------------------------------------------------------------------- /examples/ex4.py: -------------------------------------------------------------------------------- 1 | # List of ipymd cells expected for this example. 2 | output = [ 3 | 4 | {'cell_type': 'notebook_metadata', 5 | 'metadata': {'title': 'A Slideshow'}}, 6 | 7 | {'cell_type': 'markdown', 8 | 'source': '# Header', 9 | 'metadata': {'slideshow': {'slide_type': 'slide'}}}, 10 | 11 | {'cell_type': 'markdown', 12 | 'source': 'A paragraph.', 13 | 'metadata': {'slideshow': {'slide_type': 'slide'}}}, 14 | 15 | {'cell_type': 'markdown', 16 | 'source': 'Python code:', 17 | 'metadata': {'ipymd': {'empty_meta': True}}}, 18 | 19 | {'cell_type': 'code', 20 | 'input': 'print("Hello world!")', 21 | 'output': 'Hello world!', 22 | 'metadata': {'slideshow': {'slide_type': 'fragment'}}}, 23 | 24 | {'cell_type': 'markdown', 25 | 'source': 'JavaScript code:', 26 | 'metadata': {'slideshow': {'slide_type': 'skip'}}}, 27 | 28 | {'cell_type': 'markdown', 29 | 'source': '```javascript\nconsole.log("Hello world!");\n```', 30 | 'metadata': {'slideshow': {'slide_type': 'skip'}}}, 31 | 32 | ] 33 | -------------------------------------------------------------------------------- /ipymd/lib/tests/test_base_lexer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test base lexer.""" 4 | 5 | # ----------------------------------------------------------------------------- 6 | # Imports 7 | # ----------------------------------------------------------------------------- 8 | 9 | import re 10 | 11 | from ..base_lexer import BaseLexer, BaseGrammar 12 | 13 | 14 | # ----------------------------------------------------------------------------- 15 | # Tests 16 | # ----------------------------------------------------------------------------- 17 | 18 | class Grammar(BaseGrammar): 19 | word = re.compile(r'^\w+') 20 | space = re.compile(r'^\s+') 21 | 22 | 23 | class Lexer(BaseLexer): 24 | grammar_class = Grammar 25 | default_rules = ['word', 'space'] 26 | words = [] 27 | 28 | def parse_word(self, m): 29 | self.words.append(m.group(0)) 30 | 31 | def parse_space(self, m): 32 | pass 33 | 34 | 35 | def test_base_lexer(): 36 | lexer = Lexer() 37 | text = "hello world" 38 | lexer.read(text) 39 | assert lexer.words == ['hello', 'world'] 40 | -------------------------------------------------------------------------------- /ipymd/lib/python.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Python utility functions.""" 4 | 5 | 6 | # ----------------------------------------------------------------------------- 7 | # Imports 8 | # ----------------------------------------------------------------------------- 9 | 10 | import ast 11 | 12 | from ..ext.six import string_types 13 | 14 | 15 | # ----------------------------------------------------------------------------- 16 | # Utility functions 17 | # ----------------------------------------------------------------------------- 18 | 19 | def _is_python(source): 20 | """Return whether a string contains valid Python code.""" 21 | try: 22 | ast.parse(source) 23 | return True 24 | except SyntaxError: 25 | return False 26 | 27 | 28 | class PythonFilter(object): 29 | def __init__(self, ipymd_skip=None): 30 | self._ipymd_skip = ipymd_skip 31 | 32 | def filter(self, code): 33 | code = code.rstrip() 34 | if self._ipymd_skip: 35 | return '\n'.join(line for line in code.splitlines() 36 | if 'ipymd-skip' not in line) 37 | else: 38 | return code 39 | 40 | def __call__(self, code): 41 | return self.filter(code) 42 | -------------------------------------------------------------------------------- /examples/ex2.python.py: -------------------------------------------------------------------------------- 1 | # # Test notebook 2 | 3 | # This is a text notebook. Here *are* some **rich text**, `code`, $\pi\simeq 3.1415$ equations. 4 | 5 | # Another equation: 6 | 7 | # $$\sum_{i=1}^n x_i$$ 8 | 9 | # Python code: 10 | 11 | # some code in python 12 | def f(x): 13 | y = x * x 14 | return y 15 | 16 | # Random code: 17 | 18 | # ```javascript 19 | # console.log("hello" + 3); 20 | # ``` 21 | 22 | # Python code: 23 | 24 | import IPython 25 | print("Hello world!") 26 | 27 | 2*2 28 | 29 | def decorator(f): 30 | return f 31 | 32 | @decorator 33 | def f(x): 34 | pass 35 | 3*3 36 | 37 | # some text 38 | 39 | print(4*4) 40 | 41 | %%bash 42 | echo 'hello' 43 | 44 | # An image: 45 | 46 | # ![Hello world](http://wristgeek.com/wp-content/uploads/2014/09/hello_world.png) 47 | 48 | # ### Subtitle 49 | 50 | # a list 51 | 52 | # * One [small](http://www.google.fr) link! 53 | # * Two 54 | # * 2.1 55 | # * 2.2 56 | # * Three 57 | 58 | # and 59 | 60 | # 1. Un 61 | # 2. Deux 62 | 63 | import numpy as np 64 | import matplotlib.pyplot as plt 65 | %matplotlib inline 66 | 67 | plt.imshow(np.random.rand(5,5,4), interpolation='none'); 68 | 69 | # > TIP (a block quote): That's all folks. 70 | # > Last line. 71 | 72 | # Last paragraph. 73 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | install: 8 | # - sudo apt-get update 9 | # You may want to periodically update this, although the conda update 10 | # conda line below will keep everything up-to-date. We do this 11 | # conditionally because it saves us some downloading if the version is 12 | # the same. 13 | - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then 14 | wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; 15 | else 16 | wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; 17 | fi 18 | - bash miniconda.sh -b -p $HOME/miniconda 19 | - export PATH="$HOME/miniconda/bin:$PATH" 20 | - hash -r 21 | - conda config --set always_yes yes --set changeps1 no 22 | - conda update -q conda 23 | - conda info -a 24 | # Create the environment. 25 | - conda create -q -n testenv python=$TRAVIS_PYTHON_VERSION 26 | - source activate testenv 27 | # Optional dependencies. 28 | # TODO: IPython 2 env as well 29 | - conda install pip ipython-notebook 30 | # Require master version of odfpy 31 | - pip install -r requirements-dev.txt 32 | # Install ipymd. 33 | - pip install -e . 34 | script: 35 | - make test 36 | after_success: 37 | - coveralls 38 | -------------------------------------------------------------------------------- /examples/ex2.markdown.md: -------------------------------------------------------------------------------- 1 | # Test notebook 2 | 3 | This is a text notebook. Here *are* some **rich text**, `code`, $\pi\simeq 3.1415$ equations. 4 | 5 | Another equation: 6 | 7 | $$\sum_{i=1}^n x_i$$ 8 | 9 | Python code: 10 | 11 | ```python 12 | >>> # some code in python 13 | ... def f(x): 14 | ... y = x * x 15 | ... return y 16 | ``` 17 | 18 | Random code: 19 | 20 | ```javascript 21 | console.log("hello" + 3); 22 | ``` 23 | 24 | Python code: 25 | 26 | ```python 27 | >>> import IPython 28 | >>> print("Hello world!") 29 | Hello world! 30 | ``` 31 | 32 | ```python 33 | >>> 2*2 34 | 4 35 | ``` 36 | 37 | ```python 38 | >>> def decorator(f): 39 | ... return f 40 | ``` 41 | 42 | ```python 43 | >>> @decorator 44 | ... def f(x): 45 | ... pass 46 | >>> 3*3 47 | 9 48 | ``` 49 | 50 | some text 51 | 52 | ```python 53 | >>> print(4*4) 54 | 16 55 | ``` 56 | 57 | ```python 58 | >>> %%bash 59 | ... echo 'hello' 60 | hello 61 | ``` 62 | 63 | An image: 64 | 65 | ![Hello world](http://wristgeek.com/wp-content/uploads/2014/09/hello_world.png) 66 | 67 | ### Subtitle 68 | 69 | a list 70 | 71 | * One [small](http://www.google.fr) link! 72 | * Two 73 | * 2.1 74 | * 2.2 75 | * Three 76 | 77 | and 78 | 79 | 1. Un 80 | 2. Deux 81 | 82 | ```python 83 | >>> import numpy as np 84 | >>> import matplotlib.pyplot as plt 85 | >>> %matplotlib inline 86 | ``` 87 | 88 | ```python 89 | >>> plt.imshow(np.random.rand(5,5,4), interpolation='none'); 90 | ``` 91 | 92 | > TIP (a block quote): That's all folks. 93 | > Last line. 94 | 95 | Last paragraph. 96 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Cyrille Rossant 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | * Neither the name of ipymd nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /examples/ex1.notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Header" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "A paragraph." 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "Python code:" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 1, 27 | "metadata": { 28 | "collapsed": false 29 | }, 30 | "outputs": [ 31 | { 32 | "name": "stdout", 33 | "output_type": "stream", 34 | "text": [ 35 | "Hello world!\n" 36 | ] 37 | } 38 | ], 39 | "source": [ 40 | "print(\"Hello world!\")" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "JavaScript code:" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "```javascript\n", 55 | "console.log(\"Hello world!\");\n", 56 | "```" 57 | ] 58 | } 59 | ], 60 | "metadata": { 61 | "kernelspec": { 62 | "display_name": "Python 3", 63 | "language": "python", 64 | "name": "python3" 65 | }, 66 | "language_info": { 67 | "codemirror_mode": { 68 | "name": "ipython", 69 | "version": 3 70 | }, 71 | "file_extension": ".py", 72 | "mimetype": "text/x-python", 73 | "name": "python", 74 | "nbconvert_exporter": "python", 75 | "pygments_lexer": "ipython3", 76 | "version": "3.4.2" 77 | } 78 | }, 79 | "nbformat": 4, 80 | "nbformat_minor": 0 81 | } 82 | -------------------------------------------------------------------------------- /examples/ex3.notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "cells": [ 4 | { 5 | "metadata": { 6 | "slideshow": { 7 | "slide_type": "slide" 8 | } 9 | }, 10 | "cell_type": "markdown", 11 | "source": "# Header" 12 | }, 13 | { 14 | "metadata": { 15 | "slideshow": { 16 | "slide_type": "slide" 17 | } 18 | }, 19 | "cell_type": "markdown", 20 | "source": "A paragraph." 21 | }, 22 | { 23 | "metadata": { 24 | "ipymd": {"empty_meta": true} 25 | }, 26 | "cell_type": "markdown", 27 | "source": "Python code:" 28 | }, 29 | { 30 | "outputs": [ 31 | { 32 | "output_type": "execute_result", 33 | "metadata": {}, 34 | "execution_count": 1, 35 | "data": { 36 | "text/plain": "Hello world!" 37 | } 38 | } 39 | ], 40 | "metadata": { 41 | "slideshow": { 42 | "slide_type": "fragment" 43 | } 44 | }, 45 | "execution_count": 1, 46 | "cell_type": "code", 47 | "source": "print(\"Hello world!\")" 48 | }, 49 | { 50 | "metadata": { 51 | "slideshow": { 52 | "slide_type": "skip" 53 | } 54 | }, 55 | "cell_type": "markdown", 56 | "source": "JavaScript code:" 57 | }, 58 | { 59 | "metadata": { 60 | "slideshow": { 61 | "slide_type": "skip" 62 | } 63 | }, 64 | "cell_type": "markdown", 65 | "source": "```javascript\nconsole.log(\"Hello world!\");\n```" 66 | } 67 | ], 68 | "metadata": {}, 69 | "nbformat_minor": 0 70 | } 71 | -------------------------------------------------------------------------------- /examples/ex4.notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "cells": [ 4 | { 5 | "cell_type": "markdown", 6 | "source": "# Header", 7 | "metadata": { 8 | "slideshow": { 9 | "slide_type": "slide" 10 | } 11 | } 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "source": "A paragraph.", 16 | "metadata": { 17 | "slideshow": { 18 | "slide_type": "slide" 19 | } 20 | } 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "source": "Python code:", 25 | "metadata": { 26 | "ipymd": { 27 | "empty_meta": true 28 | } 29 | } 30 | }, 31 | { 32 | "cell_type": "code", 33 | "source": "print(\"Hello world!\")", 34 | "execution_count": 1, 35 | "outputs": [ 36 | { 37 | "data": { 38 | "text/plain": "Hello world!" 39 | }, 40 | "execution_count": 1, 41 | "output_type": "execute_result", 42 | "metadata": {} 43 | } 44 | ], 45 | "metadata": { 46 | "slideshow": { 47 | "slide_type": "fragment" 48 | } 49 | } 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "source": "JavaScript code:", 54 | "metadata": { 55 | "slideshow": { 56 | "slide_type": "skip" 57 | } 58 | } 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "source": "```javascript\nconsole.log(\"Hello world!\");\n```", 63 | "metadata": { 64 | "slideshow": { 65 | "slide_type": "skip" 66 | } 67 | } 68 | } 69 | ], 70 | "metadata": { 71 | "title": "A Slideshow" 72 | }, 73 | "nbformat_minor": 0 74 | } 75 | -------------------------------------------------------------------------------- /ipymd/formats/tests/test_atlas.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test Atlas parser and reader.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | from ...core.format_manager import format_manager, convert 10 | from ...utils.utils import _remove_output, _diff, _show_outputs 11 | from ._utils import (_test_reader, _test_writer, 12 | _exec_test_file, _read_test_file) 13 | 14 | 15 | #------------------------------------------------------------------------------ 16 | # Test Atlas parser 17 | #------------------------------------------------------------------------------ 18 | 19 | def _test_atlas_reader(basename): 20 | """Check that (test cells) and (test contents ==> cells) are the same.""" 21 | converted, expected = _test_reader(basename, 'atlas') 22 | 23 | # Assert all cells are identical except the outputs which are lost 24 | # in translation. 25 | assert _remove_output(converted) == _remove_output(expected) 26 | 27 | 28 | def _test_atlas_writer(basename): 29 | """Check that (test contents) and (test cells ==> contents) are the same. 30 | """ 31 | converted, expected = _test_writer(basename, 'atlas') 32 | assert _diff(converted, expected) == '' 33 | 34 | 35 | def _test_atlas_atlas(basename): 36 | """Check that the double conversion is the identity.""" 37 | 38 | contents = _read_test_file(basename, 'atlas') 39 | cells = convert(contents, from_='atlas') 40 | converted = convert(cells, to='atlas') 41 | 42 | assert _diff(contents, converted) == '' 43 | 44 | 45 | def test_atlas_reader(): 46 | _test_atlas_reader('ex1') 47 | _test_atlas_reader('ex2') 48 | 49 | 50 | def test_atlas_writer(): 51 | _test_atlas_writer('ex1') 52 | _test_atlas_writer('ex2') 53 | 54 | 55 | def test_atlas_atlas(): 56 | _test_atlas_atlas('ex1') 57 | _test_atlas_atlas('ex2') 58 | -------------------------------------------------------------------------------- /ipymd/formats/tests/test_notebook.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test notebook parser and reader.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | from ...core.format_manager import format_manager, convert 10 | from ..notebook import _compare_notebooks 11 | from ...utils.utils import _diff, _show_outputs 12 | from ._utils import (_test_reader, _test_writer, 13 | _exec_test_file, _read_test_file) 14 | 15 | 16 | #------------------------------------------------------------------------------ 17 | # Test notebook parser 18 | #------------------------------------------------------------------------------ 19 | 20 | def _test_notebook_reader(basename): 21 | """Check that (test cells) and (test nb ==> cells) are the same.""" 22 | converted, expected = _test_reader(basename, 'notebook') 23 | assert converted == expected 24 | 25 | 26 | def _test_notebook_writer(basename): 27 | """Check that (test nb) and (test cells ==> nb) are the same. 28 | """ 29 | converted, expected = _test_writer(basename, 'notebook') 30 | assert _compare_notebooks(converted, expected) 31 | 32 | 33 | def _test_notebook_notebook(basename): 34 | """Check that the double conversion is the identity.""" 35 | 36 | contents = _read_test_file(basename, 'notebook') 37 | cells = convert(contents, from_='notebook') 38 | converted = convert(cells, to='notebook') 39 | 40 | assert _compare_notebooks(contents, converted) 41 | 42 | 43 | def test_notebook_reader(): 44 | _test_notebook_reader('ex1') 45 | _test_notebook_reader('ex2') 46 | _test_notebook_reader('ex3') 47 | 48 | 49 | def test_notebook_writer(): 50 | _test_notebook_writer('ex1') 51 | _test_notebook_writer('ex2') 52 | _test_notebook_writer('ex3') 53 | 54 | 55 | def test_notebook_notebook(): 56 | _test_notebook_notebook('ex1') 57 | _test_notebook_notebook('ex2') 58 | _test_notebook_notebook('ex3') 59 | -------------------------------------------------------------------------------- /ipymd/formats/opendocument.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """OpenDocument renderers.""" 4 | 5 | 6 | # ----------------------------------------------------------------------------- 7 | # Imports 8 | # ----------------------------------------------------------------------------- 9 | 10 | from ..core.prompt import create_prompt 11 | from ..lib.markdown import InlineLexer, BlockLexer, BaseRenderer 12 | from ..lib.opendocument import (ODFDocument, ODFRenderer, 13 | odf_to_markdown, 14 | load_odf, save_odf) 15 | from ..lib.python import PythonFilter 16 | from .markdown import MarkdownReader 17 | 18 | 19 | # ----------------------------------------------------------------------------- 20 | # ODF renderers 21 | # ----------------------------------------------------------------------------- 22 | 23 | class ODFReader(object): 24 | def read(self, contents): 25 | # contents is an ODFDocument. 26 | md = odf_to_markdown(contents) 27 | reader = MarkdownReader() 28 | return reader.read(md) 29 | 30 | 31 | class ODFWriter(object): 32 | def __init__(self, prompt=None, odf_doc=None, 33 | odf_renderer=None, ipymd_skip=False): 34 | self._odf_doc = odf_doc or ODFDocument() 35 | if odf_renderer is None: 36 | odf_renderer = ODFRenderer 37 | self._prompt = create_prompt(prompt) 38 | renderer = odf_renderer(self._odf_doc) 39 | self._block_lexer = BlockLexer(renderer=renderer) 40 | self._code_filter = PythonFilter(ipymd_skip=ipymd_skip) 41 | 42 | def write(self, cell): 43 | if cell['cell_type'] == 'markdown': 44 | md = cell['source'] 45 | # Convert the Markdown cell to ODF. 46 | self._block_lexer.read(md) 47 | elif cell['cell_type'] == 'code': 48 | # Add the code cell to ODF. 49 | cell['input'] = self._code_filter(cell['input']) 50 | source = self._prompt.from_cell(cell['input'], cell['output']) 51 | self._odf_doc.code(source) 52 | 53 | @property 54 | def contents(self): 55 | return self._odf_doc 56 | 57 | 58 | ODF_FORMAT = dict( 59 | reader=ODFReader, 60 | writer=ODFWriter, 61 | file_extension='.odt', 62 | load=load_odf, 63 | save=save_odf, 64 | ) 65 | -------------------------------------------------------------------------------- /examples/ex2.atlas.md: -------------------------------------------------------------------------------- 1 | # Test notebook 2 | 3 | This is a text notebook. Here *are* some **rich text**, `code`, \\(\pi\simeq 3.1415\\) equations. 4 | 5 | Another equation: 6 | 7 | \\(\sum_{i=1}^n x_i\\) 8 | 9 | Python code: 10 | 11 |
 14 | # some code in python
 15 | def f(x):
 16 |     y = x * x
 17 |     return y
 18 | 
19 | 20 | Random code: 21 | 22 | ```javascript 23 | console.log("hello" + 3); 24 | ``` 25 | 26 | Python code: 27 | 28 |
 31 | import IPython
 32 | print("Hello world!")
 33 | 
34 | 35 |
 38 | 2*2
 39 | 
40 | 41 |
 44 | def decorator(f):
 45 |     return f
 46 | 
47 | 48 |
 51 | @decorator
 52 | def f(x):
 53 |     pass
 54 | 3*3
 55 | 
56 | 57 | some text 58 | 59 |
 62 | print(4*4)
 63 | 
64 | 65 |
 68 | %%bash
 69 | echo 'hello'
 70 | 
71 | 72 | An image: 73 | 74 | ![Hello world](http://wristgeek.com/wp-content/uploads/2014/09/hello_world.png) 75 | 76 | ### Subtitle 77 | 78 | a list 79 | 80 | * One [small](http://www.google.fr) link! 81 | * Two 82 | * 2.1 83 | * 2.2 84 | * Three 85 | 86 | and 87 | 88 | 1. Un 89 | 2. Deux 90 | 91 |
 94 | import numpy as np
 95 | import matplotlib.pyplot as plt
 96 | %matplotlib inline
 97 | 
98 | 99 |
102 | plt.imshow(np.random.rand(5,5,4), interpolation='none');
103 | 
104 | 105 | > TIP (a block quote): That's all folks. 106 | > Last line. 107 | 108 | Last paragraph. 109 | -------------------------------------------------------------------------------- /ipymd/formats/tests/_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Testing utilities.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | import os 10 | import os.path as op 11 | import difflib 12 | from pprint import pprint 13 | 14 | from ...core.format_manager import format_manager, convert 15 | from ...core.scripts import _load_file, _save_file 16 | from ...ext.six import exec_ 17 | 18 | 19 | #------------------------------------------------------------------------------ 20 | # Test Markdown parser 21 | #------------------------------------------------------------------------------ 22 | 23 | def _script_dir(): 24 | return op.dirname(op.realpath(__file__)) 25 | 26 | 27 | def _test_file_path(basename, format=None): 28 | """Return the full path to an example filename in the 'examples' 29 | directory.""" 30 | if format is not None: 31 | file_extension = format_manager().file_extension(format) 32 | filename = basename + '.' + format + file_extension 33 | else: 34 | # format=None ==> .py test file 35 | filename = basename + '.py' 36 | return op.realpath(op.join(_script_dir(), '../../../examples', filename)) 37 | 38 | 39 | def _exec_test_file(basename): 40 | """Return the 'output' object defined in a Python file.""" 41 | path = _test_file_path(basename) 42 | with open(path, 'r') as f: 43 | contents = f.read() 44 | ns = {} 45 | exec_(contents, ns) 46 | return ns.get('output', None) 47 | 48 | 49 | def _read_test_file(basename, format): 50 | """Read a test file.""" 51 | path = _test_file_path(basename, format) 52 | return _load_file(path, format) 53 | 54 | 55 | def _test_reader(basename, format, ignore_notebook_meta=True): 56 | """Return converted and expected ipymd cells of a given example.""" 57 | contents = _read_test_file(basename, format) 58 | converted = convert(contents, from_=format) 59 | expected = _exec_test_file(basename) 60 | converted = [cell for cell in converted 61 | if (not ignore_notebook_meta) or 62 | cell["cell_type"] != "notebook_metadata"] 63 | 64 | return converted, expected 65 | 66 | 67 | def _test_writer(basename, format): 68 | """Return converted and expected ipymd cells of a given example.""" 69 | cells = _exec_test_file(basename) 70 | converted = convert(cells, to=format) 71 | expected = _read_test_file(basename, format) 72 | return converted, expected 73 | -------------------------------------------------------------------------------- /ipymd/core/tests/test_core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test core.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | import os.path as op 10 | import shutil 11 | 12 | from ..format_manager import FormatManager, format_manager 13 | from ...utils.tempdir import TemporaryDirectory 14 | 15 | 16 | #------------------------------------------------------------------------------ 17 | # Test core functions 18 | #------------------------------------------------------------------------------ 19 | 20 | def load_mock(path): 21 | # Return a list of lines. 22 | with open(path, 'r') as f: 23 | return [line.rstrip() for line in f.readlines()[1:]] 24 | 25 | 26 | def save_mock(path, contents): 27 | # contents is a list of lines. 28 | with open(path, 'w') as f: 29 | f.write('mock\n') 30 | f.write('\n'.join(contents)) 31 | 32 | 33 | class MockReader(object): 34 | def read(self, contents): 35 | # contents is a list of lines. 36 | return [{'cell_type': 'markdown', 37 | 'source': line} for line in contents] 38 | 39 | 40 | class MockWriter(object): 41 | def __init__(self): 42 | self.contents = [] 43 | 44 | def write(self, cell): 45 | # contents is a list of lines. 46 | if cell['cell_type'] == 'markdown': 47 | self.contents.append(cell['source']) 48 | 49 | 50 | def test_format_manager(): 51 | fm = format_manager() 52 | fm.register(name='mock', 53 | reader=MockReader, 54 | writer=MockWriter, 55 | file_extension='.mock', 56 | load=load_mock, 57 | save=save_mock) 58 | 59 | contents = ['line 1', 'line 2', 'line 3'] 60 | 61 | with TemporaryDirectory() as tempdir: 62 | path = op.join(tempdir, 'test.mock') 63 | fm.save(path, contents) 64 | 65 | # Test the custom load/save functions. 66 | loaded = fm.load(path) 67 | assert loaded == contents 68 | 69 | fm.save(path, loaded) 70 | with open(path, 'r') as f: 71 | assert f.read() == 'mock\nline 1\nline 2\nline 3' 72 | 73 | # Test the conversion from/to ipymd cells. 74 | cells = fm.convert(contents, from_='mock') 75 | assert cells == [{'cell_type': 'markdown', 76 | 'source': line} for line in contents] 77 | assert fm.convert(cells, to='mock') == contents 78 | 79 | fm.unregister('mock') 80 | -------------------------------------------------------------------------------- /ipymd/core/tests/test_scripts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test scripts.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | import os 10 | import os.path as op 11 | import shutil 12 | 13 | from ..scripts import convert_files, _common_root 14 | from ...formats.tests._utils import _test_file_path 15 | from ...utils.tempdir import TemporaryDirectory 16 | 17 | 18 | #------------------------------------------------------------------------------ 19 | # Test utility functions 20 | #------------------------------------------------------------------------------ 21 | 22 | def test_common_root(): 23 | assert not _common_root([]) 24 | assert _common_root(['a', 'b']) 25 | root = op.dirname(op.realpath(__file__)) 26 | if not root.endswith('/'): 27 | root = root + '/' 28 | assert _common_root([op.join(root, 'a'), 29 | op.join(root, 'b')]) == root 30 | assert (_common_root([op.join(root, '../tests/a'), 31 | op.join(root, '../tests/b')]) == root) 32 | 33 | 34 | #------------------------------------------------------------------------------ 35 | # Test CLI conversion tool 36 | #------------------------------------------------------------------------------ 37 | 38 | def test_convert_files(): 39 | basename = 'ex1' 40 | with TemporaryDirectory() as tempdir: 41 | 42 | # Copy some Markdown file to the temporary directory. 43 | md_orig = _test_file_path(basename, 'markdown') 44 | md_temp = op.join(tempdir, basename + '.md') 45 | shutil.copy(md_orig, md_temp) 46 | 47 | # Launch the CLI conversion tool. 48 | convert_files(md_temp, from_='markdown', to='notebook') 49 | 50 | # TODO: more tests 51 | assert op.exists(op.join(tempdir, basename + '.ipynb')) 52 | 53 | 54 | def test_output_folder(): 55 | with TemporaryDirectory() as tempdir: 56 | 57 | # Copy some Markdown file to the temporary directory. 58 | # 59 | # tempdir 60 | # |- ex1.md 61 | # |- subfolder/ex1.md 62 | md_orig = _test_file_path('ex1', 'markdown') 63 | md_temp_0 = op.join(tempdir, 'ex1.md') 64 | md_temp_1 = op.join(tempdir, 'subfolder', 'ex1.md') 65 | 66 | os.mkdir(op.join(tempdir, 'subfolder')) 67 | shutil.copy(md_orig, md_temp_0) 68 | shutil.copy(md_orig, md_temp_1) 69 | 70 | # Launch the CLI conversion tool. 71 | convert_files([md_temp_0, md_temp_1], from_='markdown', to='notebook', 72 | output_folder=op.join(tempdir, 'output')) 73 | 74 | assert op.exists(op.join(tempdir, 'output/ex1.ipynb')) 75 | assert op.exists(op.join(tempdir, 'output/subfolder/ex1.ipynb')) 76 | -------------------------------------------------------------------------------- /ipymd/formats/tests/test_opendocument.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test ODF parser and reader.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | from ...core.format_manager import format_manager, convert 10 | from ...utils.utils import (_remove_output, 11 | _remove_code_lang, 12 | _remove_images, 13 | _flatten_links, 14 | _diff 15 | ) 16 | from ._utils import (_test_reader, _test_writer, 17 | _exec_test_file, _read_test_file) 18 | 19 | 20 | #------------------------------------------------------------------------------ 21 | # Test ODF parser 22 | #------------------------------------------------------------------------------ 23 | 24 | def _diff_trees(tree_0, tree_1): 25 | for ch_0, ch_1 in zip(tree_0.get('children', []), 26 | tree_1.get('children', [])): 27 | if ch_0 != ch_1: 28 | print('****') 29 | print(ch_0) 30 | print(ch_1) 31 | return 32 | 33 | 34 | def _test_generate(): 35 | """Regenerate the ODF example documents.""" 36 | for ex in ('ex1', 37 | 'ex2', 38 | ): 39 | markdown = _read_test_file(ex, 'markdown') 40 | odf = convert(markdown, from_='markdown', to='opendocument') 41 | odf.save('examples/{0}.opendocument.odt'.format(ex)) 42 | 43 | 44 | def _process_md(md): 45 | for f in (_remove_code_lang, 46 | _remove_images, 47 | _flatten_links): 48 | md = f(md) 49 | return md 50 | 51 | 52 | def _test_odf_reader(basename): 53 | """Check that (test cells) and (test contents ==> cells) are the same.""" 54 | converted, expected = _test_reader(basename, 'opendocument') 55 | converted = _process_md(converted) 56 | expected = _process_md(expected) 57 | assert converted == expected 58 | 59 | 60 | def _test_odf_writer(basename): 61 | """Check that (test contents) and (test cells ==> contents) are the same. 62 | """ 63 | converted, expected = _test_writer(basename, 'opendocument') 64 | assert converted == expected 65 | 66 | 67 | def _test_odf_odf(basename): 68 | """Check that the double conversion is the identity.""" 69 | 70 | contents = _read_test_file(basename, 'opendocument') 71 | cells = convert(contents, from_='opendocument') 72 | converted = convert(cells, to='opendocument') 73 | assert contents.tree() == converted.tree() 74 | 75 | 76 | def test_odf_reader(): 77 | _test_odf_reader('ex1') 78 | _test_odf_reader('ex2') 79 | 80 | 81 | def test_odf_writer(): 82 | _test_odf_writer('ex1') 83 | _test_odf_writer('ex2') 84 | 85 | 86 | def test_odf_odf(): 87 | _test_odf_odf('ex1') 88 | _test_odf_odf('ex2') 89 | -------------------------------------------------------------------------------- /ipymd/lib/base_lexer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Base lexer. 4 | 5 | The code has been adapted from the mistune library: 6 | 7 | mistune 8 | https://github.com/lepture/mistune/ 9 | 10 | The fastest markdown parser in pure Python with renderer feature. 11 | :copyright: (c) 2014 - 2015 by Hsiaoming Yang. 12 | 13 | """ 14 | 15 | # ----------------------------------------------------------------------------- 16 | # Imports 17 | # ----------------------------------------------------------------------------- 18 | 19 | from functools import partial 20 | 21 | 22 | # ----------------------------------------------------------------------------- 23 | # Base lexer 24 | # ----------------------------------------------------------------------------- 25 | 26 | class BaseGrammar(object): 27 | pass 28 | 29 | 30 | class BaseRenderer(object): 31 | def __init__(self, verbose=False): 32 | self._verbose = verbose 33 | self._handler = self._process 34 | 35 | def handler(self, func): 36 | self._handler = func 37 | 38 | def _process(self, name, *args, **kwargs): 39 | if self._verbose: 40 | sargs = ', '.join(args) 41 | skwargs = ', '.join('{k}={v}'.format(k=k, v=v) 42 | for k, v in kwargs.items()) 43 | print(name, sargs, skwargs) 44 | 45 | def __getattr__(self, name): 46 | return partial(self._handler, name) 47 | 48 | 49 | class BaseLexer(object): 50 | grammar_class = BaseGrammar 51 | default_rules = [] 52 | renderer_class = BaseRenderer 53 | 54 | def __init__(self, renderer=None, grammar=None, rules=None): 55 | if grammar is None: 56 | grammar = self.grammar_class() 57 | if rules is None: 58 | rules = self.default_rules 59 | if renderer is None: 60 | renderer = self.renderer_class() 61 | self.grammar = grammar 62 | self.rules = rules 63 | self.renderer = renderer 64 | 65 | def manipulate(self, text, rules): 66 | for key in rules: 67 | rule = getattr(self.grammar, key) 68 | m = rule.match(text) 69 | if not m: 70 | continue 71 | out = getattr(self, 'parse_%s' % key)(m) 72 | return m, out 73 | return False, None 74 | 75 | def preprocess(self, text): 76 | return text.rstrip('\n') 77 | 78 | def read(self, text, rules=None): 79 | if rules is None: 80 | rules = self.rules 81 | text = self.preprocess(text) 82 | tokens = [] 83 | while text: 84 | m, out = self.manipulate(text, rules) 85 | if out is None: 86 | tokens.append(m) 87 | else: 88 | tokens.append(out) 89 | if m is not False: 90 | text = text[len(m.group(0)):] 91 | continue 92 | if text: 93 | raise RuntimeError('Infinite loop at: %s' % text) 94 | return tokens 95 | -------------------------------------------------------------------------------- /examples/ex2.py: -------------------------------------------------------------------------------- 1 | # List of ipymd cells expected for this example. 2 | output = [ 3 | 4 | {'cell_type': 'markdown', 5 | 'source': '# Test notebook'}, 6 | 7 | {'cell_type': 'markdown', 8 | 'source': 'This is a text notebook. Here *are* some **rich text**, ' 9 | '`code`, $\\pi\\simeq 3.1415$ equations.'}, 10 | 11 | {'cell_type': 'markdown', 12 | 'source': 'Another equation:'}, 13 | 14 | {'cell_type': 'markdown', 15 | 'source': '$$\\sum_{i=1}^n x_i$$'}, 16 | 17 | {'cell_type': 'markdown', 18 | 'source': 'Python code:'}, 19 | 20 | {'cell_type': 'code', 21 | 'input': '# some code in python\ndef f(x):\n y = x * x\n return y', 22 | 'output': ''}, 23 | 24 | {'cell_type': 'markdown', 25 | 'source': 'Random code:'}, 26 | 27 | {'cell_type': 'markdown', 28 | 'source': '```javascript\nconsole.log("hello" + 3);\n```'}, 29 | 30 | {'cell_type': 'markdown', 31 | 'source': 'Python code:'}, 32 | 33 | {'cell_type': 'code', 34 | 'input': 'import IPython\nprint("Hello world!")', 35 | 'output': 'Hello world!'}, 36 | 37 | {'cell_type': 'code', 38 | 'input': '2*2', 'output': '4'}, 39 | 40 | {'cell_type': 'code', 41 | 'input': 'def decorator(f):\n return f', 42 | 'output': ''}, 43 | 44 | {'cell_type': 'code', 45 | 'input': '@decorator\ndef f(x):\n pass\n3*3', 46 | 'output': '9'}, 47 | 48 | {'cell_type': 'markdown', 49 | 'source': 'some text'}, 50 | 51 | {'cell_type': 'code', 52 | 'input': 'print(4*4)', 'output': '16'}, 53 | 54 | {'cell_type': 'code', 55 | 'input': "%%bash\necho 'hello'", 'output': 'hello'}, 56 | 57 | {'cell_type': 'markdown', 58 | 'source': 'An image:'}, 59 | 60 | {'cell_type': 'markdown', 61 | 'source': '![Hello ' 62 | 'world](http://wristgeek.com/wp-content/uploads/2014/09/' 63 | 'hello_world.png)'}, 64 | 65 | {'cell_type': 'markdown', 66 | 'source': '### Subtitle'}, 67 | 68 | {'cell_type': 'markdown', 69 | 'source': 'a list'}, 70 | 71 | {'cell_type': 'markdown', 72 | 'source': '* One [small](http://www.google.fr) link!\n' 73 | '* Two\n' 74 | ' * 2.1\n' 75 | ' * 2.2\n' 76 | '* Three'}, 77 | 78 | {'cell_type': 'markdown', 79 | 'source': 'and'}, 80 | 81 | {'cell_type': 'markdown', 82 | 'source': '1. Un\n2. Deux'}, 83 | 84 | {'cell_type': 'code', 85 | 'input': 'import numpy as np\n' 86 | 'import matplotlib.pyplot as plt\n' 87 | '%matplotlib inline', 88 | 'output': ''}, 89 | 90 | {'cell_type': 'code', 91 | 'input': "plt.imshow(np.random.rand(5,5,4), interpolation='none');", 92 | 'output': ''}, 93 | 94 | {'cell_type': 'markdown', 95 | 'source': "> TIP (a block quote): That's all folks.\n> Last line."}, 96 | 97 | {'cell_type': 'markdown', 'source': 'Last paragraph.'} 98 | 99 | ] 100 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os.path 4 | import re 5 | import sys 6 | 7 | from setuptools import setup, find_packages 8 | from setuptools.command.test import test as TestCommand 9 | 10 | 11 | class PyTest(TestCommand): 12 | user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] 13 | 14 | def initialize_options(self): 15 | TestCommand.initialize_options(self) 16 | self.pytest_args = [] 17 | 18 | def finalize_options(self): 19 | TestCommand.finalize_options(self) 20 | self.test_args = [] 21 | self.test_suite = True 22 | 23 | def run_tests(self): 24 | #import here, cause outside the eggs aren't loaded 25 | import pytest 26 | errno = pytest.main(self.pytest_args) 27 | sys.exit(errno) 28 | 29 | 30 | curdir = os.path.dirname(os.path.realpath(__file__)) 31 | filename = os.path.join(curdir, 'ipymd', '__init__.py') 32 | with open(filename, 'r') as f: 33 | version = re.search(r"__version__ = '([^']+)'", f.read()).group(1) 34 | 35 | classes = """ 36 | Development Status :: 3 - Alpha 37 | License :: OSI Approved :: BSD License 38 | Environment :: Console 39 | Framework :: IPython 40 | Intended Audience :: Developers 41 | Natural Language :: English 42 | Operating System :: Unix 43 | Operating System :: POSIX 44 | Operating System :: MacOS :: MacOS X 45 | Programming Language :: Python 46 | Programming Language :: Python :: 2 47 | Programming Language :: Python :: 2.7 48 | Programming Language :: Python :: 3 49 | Programming Language :: Python :: 3.3 50 | Programming Language :: Python :: 3.4 51 | Topic :: Software Development :: Libraries 52 | Topic :: Text Processing :: Markup 53 | 54 | """ 55 | classifiers = [s.strip() for s in classes.split('\n') if s] 56 | 57 | description = ('Use the IPython notebook as an interactive Markdown editor') 58 | 59 | with open('README.md') as f: 60 | long_description = f.read() 61 | 62 | setup( 63 | name='ipymd', 64 | version=version, 65 | license='BSD', 66 | description=description, 67 | long_description=long_description, 68 | author='Cyrille Rossant', 69 | author_email='cyrille.rossant at gmail.com', 70 | maintainer='Cyrille Rossant', 71 | maintainer_email='cyrille.rossant at gmail.com', 72 | url='https://github.com/rossant/ipymd', 73 | classifiers=classifiers, 74 | packages=find_packages(), 75 | entry_points={ 76 | 'console_scripts': [ 77 | 'ipymd=ipymd.core.scripts:main', 78 | ], 79 | 'ipymd.format': [ 80 | 'markdown=ipymd.formats.markdown:MARKDOWN_FORMAT', 81 | 'atlas=ipymd.formats.atlas:ATLAS_FORMAT', 82 | 'notebook=ipymd.formats.notebook:NOTEBOOK_FORMAT', 83 | 'opendocument=ipymd.formats.opendocument:ODF_FORMAT[odf]', 84 | 'python=ipymd.formats.python:PYTHON_FORMAT', 85 | ] 86 | }, 87 | install_requires=['pyyaml'], 88 | extras_require={ 89 | 'odf': ['odfpy'], 90 | }, 91 | cmdclass={'test': PyTest}, 92 | ) 93 | -------------------------------------------------------------------------------- /ipymd/formats/tests/test_markdown.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test Markdown parser and reader.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | from ...core.format_manager import format_manager, convert 10 | from ...utils.utils import _diff, _show_outputs 11 | from ._utils import (_test_reader, _test_writer, 12 | _exec_test_file, _read_test_file) 13 | 14 | 15 | #------------------------------------------------------------------------------ 16 | # Test Markdown parser 17 | #------------------------------------------------------------------------------ 18 | 19 | def _test_markdown_reader(basename, ignore_notebook_meta=False): 20 | """Check that (test cells) and (test contents ==> cells) are the same.""" 21 | converted, expected = _test_reader(basename, 'markdown', 22 | ignore_notebook_meta) 23 | assert converted == expected 24 | 25 | 26 | def _test_markdown_writer(basename): 27 | """Check that (test contents) and (test cells ==> contents) are the same. 28 | """ 29 | converted, expected = _test_writer(basename, 'markdown') 30 | assert _diff(converted, expected) == '' 31 | 32 | 33 | def _test_markdown_markdown(basename): 34 | """Check that the double conversion is the identity.""" 35 | 36 | contents = _read_test_file(basename, 'markdown') 37 | cells = convert(contents, from_='markdown') 38 | converted = convert(cells, to='markdown') 39 | 40 | assert _diff(contents, converted) == '' 41 | 42 | 43 | def test_markdown_reader(): 44 | _test_markdown_reader('ex1') 45 | _test_markdown_reader('ex2') 46 | _test_markdown_reader('ex3') 47 | _test_markdown_reader('ex4', ignore_notebook_meta=False) 48 | 49 | 50 | def test_markdown_writer(): 51 | _test_markdown_writer('ex1') 52 | _test_markdown_writer('ex2') 53 | _test_markdown_writer('ex3') 54 | _test_markdown_writer('ex4') 55 | 56 | 57 | def test_markdown_markdown(): 58 | _test_markdown_markdown('ex1') 59 | _test_markdown_markdown('ex2') 60 | _test_markdown_markdown('ex3') 61 | _test_markdown_markdown('ex4') 62 | 63 | 64 | def test_decorator(): 65 | """Test a bug fix where empty '...' lines were added to the output.""" 66 | 67 | markdown = '\n'.join(('```', # Not putting python still works thanks 68 | # to the input prompt. 69 | '>>> @decorator', 70 | '... def f():', 71 | '... """Docstring."""', 72 | '...', 73 | '... # Comment.', 74 | '... pass', 75 | '...', 76 | '... # Comment.', 77 | '... pass', 78 | '... pass', 79 | 'blah', 80 | 'blah', 81 | '```')) 82 | 83 | cells = convert(markdown, from_='markdown') 84 | 85 | assert '...' not in cells[0]['input'] 86 | assert cells[0]['output'] == 'blah\nblah' 87 | 88 | markdown_bis = convert(cells, to='markdown') 89 | assert _diff(markdown, markdown_bis.replace('python', '')) == '' 90 | -------------------------------------------------------------------------------- /ipymd/core/tests/test_prompt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test prompt manager.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | from ..prompt import (SimplePromptManager, 10 | IPythonPromptManager, 11 | PythonPromptManager, 12 | _template_to_regex, 13 | _starts_with_regex, 14 | ) 15 | from ...utils.utils import _show_outputs 16 | 17 | 18 | #------------------------------------------------------------------------------ 19 | # Prompt manager tests 20 | #------------------------------------------------------------------------------ 21 | 22 | def test_utils(): 23 | template = 'In [{n}]: ' 24 | regex = _template_to_regex(template) 25 | assert regex == r'In \[\d+\]\: ' 26 | 27 | assert not _starts_with_regex('In []: ', regex) 28 | assert not _starts_with_regex('In [s]: ', regex) 29 | assert not _starts_with_regex('Out [1]: ', regex) 30 | assert _starts_with_regex('In [1]: ', regex) 31 | assert _starts_with_regex('In [23]: ', regex) 32 | assert _starts_with_regex('In [23]: print()\n', regex) 33 | 34 | 35 | class MockPromptManager(SimplePromptManager): 36 | input_prompt_template = '> ' 37 | output_prompt_template = '' 38 | 39 | 40 | def test_simple_split(): 41 | pm = MockPromptManager() 42 | assert pm.is_input('> 1') 43 | assert not pm.is_input('>1') 44 | assert not pm.is_input('1') 45 | 46 | 47 | def _test(prompt_manager_cls, in_out, text): 48 | input, output = in_out 49 | 50 | # Cell => text. 51 | pm = prompt_manager_cls() 52 | text_pm = pm.from_cell(input, output) 53 | assert text_pm == text 54 | 55 | # Text => cell. 56 | input_pm, output_pm = pm.to_cell(text) 57 | assert input_pm == input 58 | 59 | assert output_pm == output 60 | 61 | 62 | def test_simple_split(): 63 | pm = MockPromptManager() 64 | assert pm.split_input_output('> 1\n> 2\n3\n4') == (['> 1', '> 2'], 65 | ['3', '4']) 66 | 67 | 68 | def test_simple_prompt_manager(): 69 | input, output = 'print("1")\nprint("2")', '1\n2' 70 | text = '> print("1")\n> print("2")\n1\n2' 71 | 72 | _test(MockPromptManager, (input, output), text) 73 | 74 | 75 | def test_ipython_split(): 76 | pm = IPythonPromptManager() 77 | text = 'In [1]: print("1")\n print("2")\nOut [1]: 1\n 2' 78 | 79 | assert pm.split_input_output(text) == (['In [1]: print("1")', 80 | ' print("2")'], 81 | ['Out [1]: 1', 82 | ' 2']) 83 | 84 | 85 | def test_ipython_prompt_manager(): 86 | input, output = 'print("1")\nprint("2")', '1\n2' 87 | text = 'In [1]: print("1")\n print("2")\nOut[1]: 1\n 2' 88 | 89 | _test(IPythonPromptManager, (input, output), text) 90 | 91 | 92 | def test_python_split(): 93 | pm = PythonPromptManager() 94 | text = '>>> print("1")\n>>> print("2")\n>>> def f():\n... pass\n1\n2' 95 | 96 | assert pm.split_input_output(text) == (['>>> print("1")', 97 | '>>> print("2")', 98 | '>>> def f():', 99 | '... pass'], 100 | ['1', 101 | '2']) 102 | 103 | 104 | def test_python_prompt_manager(): 105 | input, output = 'print("1")\nprint("2")\ndef f():\n pass', '1\n2' 106 | text = '>>> print("1")\n>>> print("2")\n>>> def f():\n... pass\n1\n2' 107 | 108 | _test(PythonPromptManager, (input, output), text) 109 | -------------------------------------------------------------------------------- /ipymd/utils/tempdir.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Temporary directory used in unit tests.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | import warnings as _warnings 10 | import os as _os 11 | import sys as _sys 12 | from tempfile import mkdtemp 13 | 14 | from ..ext.six import print_ 15 | 16 | 17 | #------------------------------------------------------------------------------ 18 | # Temporary directory 19 | #------------------------------------------------------------------------------ 20 | 21 | class TemporaryDirectory(object): 22 | """Create and return a temporary directory. This has the same 23 | behavior as mkdtemp but can be used as a context manager. For 24 | example: 25 | 26 | with TemporaryDirectory() as tmpdir: 27 | ... 28 | 29 | Upon exiting the context, the directory and everything contained 30 | in it are removed. 31 | 32 | The code comes from http://stackoverflow.com/a/19299884/1595060 33 | 34 | """ 35 | def __init__(self, suffix="", prefix="tmp", dir=None): 36 | self._closed = False 37 | self.name = None # Handle mkdtemp raising an exception 38 | self.name = mkdtemp(suffix, prefix, dir) 39 | 40 | def __enter__(self): 41 | return self.name 42 | 43 | def cleanup(self, _warn=False): 44 | if self.name and not self._closed: 45 | try: 46 | self._rmtree(self.name) 47 | except (TypeError, AttributeError) as ex: 48 | # Issue #10188: Emit a warning on stderr 49 | # if the directory could not be cleaned 50 | # up due to missing globals 51 | if "None" not in str(ex): 52 | raise 53 | print_("ERROR: {!r} while cleaning up {!r}".format(ex, 54 | self,), 55 | file=_sys.stderr) 56 | return 57 | self._closed = True 58 | if _warn: 59 | # This should be a ResourceWarning, but it is not available in 60 | # Python 2.x. 61 | self._warn("Implicitly cleaning up {!r}".format(self), 62 | Warning) 63 | 64 | def __exit__(self, exc, value, tb): 65 | self.cleanup() 66 | 67 | def __del__(self): 68 | # Issue a ResourceWarning if implicit cleanup needed 69 | self.cleanup(_warn=True) 70 | 71 | # XXX (ncoghlan): The following code attempts to make 72 | # this class tolerant of the module nulling out process 73 | # that happens during CPython interpreter shutdown 74 | # Alas, it doesn't actually manage it. See issue #10188 75 | _listdir = staticmethod(_os.listdir) 76 | _path_join = staticmethod(_os.path.join) 77 | _isdir = staticmethod(_os.path.isdir) 78 | _islink = staticmethod(_os.path.islink) 79 | _remove = staticmethod(_os.remove) 80 | _rmdir = staticmethod(_os.rmdir) 81 | _warn = _warnings.warn 82 | 83 | def _rmtree(self, path): 84 | # Essentially a stripped down version of shutil.rmtree. We can't 85 | # use globals because they may be None'ed out at shutdown. 86 | for name in self._listdir(path): 87 | fullname = self._path_join(path, name) 88 | try: 89 | isdir = self._isdir(fullname) and not self._islink(fullname) 90 | except OSError: 91 | isdir = False 92 | if isdir: 93 | self._rmtree(fullname) 94 | else: 95 | try: 96 | self._remove(fullname) 97 | except OSError: 98 | pass 99 | try: 100 | self._rmdir(path) 101 | except OSError: 102 | pass 103 | -------------------------------------------------------------------------------- /ipymd/utils/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Utils""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | import os 10 | import os.path as op 11 | import re 12 | import difflib 13 | from pprint import pprint 14 | import json 15 | 16 | from ..ext.six import exec_, string_types 17 | 18 | 19 | #------------------------------------------------------------------------------ 20 | # Utils 21 | #------------------------------------------------------------------------------ 22 | 23 | def _rstrip_lines(source): 24 | if not isinstance(source, list): 25 | source = source.splitlines() 26 | return '\n'.join(line.rstrip() for line in source) 27 | 28 | 29 | def _ensure_string(source): 30 | """Ensure a source is a string.""" 31 | if isinstance(source, string_types): 32 | return source.rstrip() 33 | else: 34 | return _rstrip_lines(source) 35 | 36 | 37 | def _preprocess(text, tab=4): 38 | """Normalize a text.""" 39 | text = re.sub(r'\r\n|\r', '\n', text) 40 | text = text.replace('\t', ' ' * tab) 41 | text = text.replace('\u00a0', ' ') 42 | text = text.replace('\u2424', '\n') 43 | pattern = re.compile(r'^ +$', re.M) 44 | text = pattern.sub('', text) 45 | text = _rstrip_lines(text) 46 | return text 47 | 48 | 49 | def _remove_output_cell(cell): 50 | """Remove the output of an ipymd cell.""" 51 | cell = cell.copy() 52 | if cell['cell_type'] == 'code': 53 | cell['output'] = None 54 | return cell 55 | 56 | 57 | def _remove_output(cells): 58 | """Remove all code outputs from a list of ipymd cells.""" 59 | return [_remove_output_cell(cell) for cell in cells] 60 | 61 | 62 | def _remove_code_lang_code(cell): 63 | if cell['cell_type'] == 'markdown': 64 | cell['source'] = re.sub(r'```[^\n]*', '```', cell['source']) 65 | return cell 66 | 67 | 68 | def _remove_code_lang(cells): 69 | """Remove all lang in code cells.""" 70 | return [_remove_code_lang_code(cell) for cell in cells] 71 | 72 | 73 | def _remove_images(cells): 74 | """Remove markdown cells with images.""" 75 | return [cell for cell in cells 76 | if not cell.get('source', '').startswith('![')] 77 | 78 | 79 | def _flatten_links_cell(cell): 80 | if cell['cell_type'] == 'markdown': 81 | cell['source'] = re.sub(r'\[([^\]]+)\]\(([^\)]+)\)', r'\1 (\2)', 82 | cell['source']) 83 | return cell 84 | 85 | 86 | def _flatten_links(cells): 87 | """Replace hypertext links by simple URL text.""" 88 | return [_flatten_links_cell(cell) for cell in cells] 89 | 90 | 91 | def _diff_removed_lines(diff): 92 | return ''.join(x[2:] for x in diff if x.startswith('- ')) 93 | 94 | 95 | def _diff(text_0, text_1): 96 | """Return a diff between two strings.""" 97 | diff = difflib.ndiff(text_0.splitlines(), text_1.splitlines()) 98 | return _diff_removed_lines(diff) 99 | 100 | 101 | def _show_outputs(*outputs): 102 | for output in outputs: 103 | print() 104 | print("-" * 30) 105 | pprint(output) 106 | 107 | 108 | #------------------------------------------------------------------------------ 109 | # Reading/writing files from/to disk 110 | #------------------------------------------------------------------------------ 111 | 112 | def _read_json(file): 113 | """Read a JSON file.""" 114 | with open(file, 'r') as f: 115 | return json.load(f) 116 | 117 | 118 | def _write_json(file, contents): 119 | """Write a dict to a JSON file.""" 120 | with open(file, 'w') as f: 121 | return json.dump(contents, f, indent=2, sort_keys=True) 122 | 123 | 124 | def _read_text(file): 125 | """Read a Markdown file.""" 126 | with open(file, 'r') as f: 127 | return f.read() 128 | 129 | 130 | def _write_text(file, contents): 131 | """Write a Markdown file.""" 132 | with open(file, 'w') as f: 133 | f.write(contents) 134 | -------------------------------------------------------------------------------- /ipymd/formats/tests/test_python.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test Python parser and reader.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | from ...core.format_manager import format_manager, convert 10 | from ...utils.utils import _remove_output, _diff, _show_outputs 11 | from ._utils import (_test_reader, _test_writer, 12 | _exec_test_file, _read_test_file) 13 | from ..python import _split_python 14 | 15 | 16 | #------------------------------------------------------------------------------ 17 | # Test Python utility functions 18 | #------------------------------------------------------------------------------ 19 | 20 | def test_split_python(): 21 | python = '\n'.join(( 22 | 'a', 23 | 'b = "1"', 24 | '', 25 | "c = '''", 26 | 'd', 27 | '', 28 | '', 29 | 'e', 30 | "'''", 31 | '', 32 | '# comment', 33 | )) 34 | chunks = _split_python(python) 35 | assert len(chunks) == 3 36 | 37 | 38 | def test_python_headers(): 39 | cells = _exec_test_file('ex2') 40 | 41 | # Keep H1. 42 | converted = convert(cells, to='python', 43 | to_kwargs={'keep_markdown': 'h1'}) 44 | assert converted.startswith('# # Test notebook\n\n# some code in python') 45 | assert len(converted.splitlines()) == 30 46 | 47 | # Keep H2, H3. 48 | converted = convert(cells, to='python', 49 | to_kwargs={'keep_markdown': 'h2,h3'}) 50 | assert not converted.startswith('# # Test notebook\n\n# some code') 51 | assert len(converted.splitlines()) == 30 52 | 53 | # Keep all headers. 54 | converted = convert(cells, to='python', 55 | to_kwargs={'keep_markdown': 'headers'}) 56 | assert converted.startswith('# # Test notebook\n\n# some code in python') 57 | assert len(converted.splitlines()) == 32 58 | 59 | # Keep all Markdown. 60 | converted = convert(cells, to='python', 61 | to_kwargs={'keep_markdown': 'all'}) 62 | assert len(converted.splitlines()) == 72 63 | 64 | # Keep no Markdown. 65 | converted = convert(cells, to='python', 66 | to_kwargs={'keep_markdown': False}) 67 | assert len(converted.splitlines()) == 28 68 | 69 | 70 | def test_commented_python(): 71 | python = '\n'.join(( 72 | '# # Title', 73 | '', 74 | '# pass', 75 | '# Hello world.', 76 | '', 77 | '# # commented Python code should not be converted to Markdown', 78 | '# print(1)', 79 | '# 3+3', 80 | '# if False:', 81 | '# exit(1/0)', 82 | '', 83 | '# Text again.', 84 | )) 85 | cells = convert(python, from_='python') 86 | assert [cell['cell_type'] for cell in cells] == ['markdown', 87 | 'markdown', 88 | 'code', 89 | 'markdown', 90 | ] 91 | 92 | 93 | #------------------------------------------------------------------------------ 94 | # Test Python parser 95 | #------------------------------------------------------------------------------ 96 | 97 | def _test_python_reader(basename): 98 | """Check that (test cells) and (test contents ==> cells) are the same.""" 99 | converted, expected = _test_reader(basename, 'python') 100 | assert _remove_output(converted) == _remove_output(expected) 101 | 102 | 103 | def _test_python_writer(basename): 104 | """Check that (test contents) and (test cells ==> contents) are the same. 105 | """ 106 | converted, expected = _test_writer(basename, 'python') 107 | assert _diff(converted, expected) == '' 108 | 109 | 110 | def _test_python_python(basename): 111 | """Check that the double conversion is the identity.""" 112 | 113 | contents = _read_test_file(basename, 'python') 114 | cells = convert(contents, from_='python') 115 | converted = convert(cells, to='python') 116 | 117 | assert _diff(contents, converted) == '' 118 | 119 | 120 | def test_python_reader(): 121 | _test_python_reader('ex1') 122 | _test_python_reader('ex2') 123 | 124 | 125 | def test_python_writer(): 126 | _test_python_writer('ex1') 127 | _test_python_writer('ex2') 128 | 129 | 130 | def test_python_python(): 131 | _test_python_python('ex1') 132 | _test_python_python('ex2') 133 | -------------------------------------------------------------------------------- /ipymd/formats/atlas.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Atlas readers and writers.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | import re 10 | 11 | from .markdown import BaseMarkdownReader, BaseMarkdownWriter 12 | from ..ext.six.moves.html_parser import HTMLParser 13 | from ..ext.six.moves.html_entities import name2codepoint 14 | from ..utils.utils import _ensure_string 15 | 16 | 17 | #------------------------------------------------------------------------------ 18 | # HTML utility functions 19 | #------------------------------------------------------------------------------ 20 | 21 | class MyHTMLParser(HTMLParser): 22 | def __init__(self, *args, **kwargs): 23 | HTMLParser.__init__(self, *args, **kwargs) 24 | self.is_code = False 25 | self.is_math = False 26 | self.display = '' 27 | self.data = '' 28 | 29 | def handle_starttag(self, tag, attrs): 30 | if tag == 'pre' and ('data-type', 'programlisting') in attrs: 31 | self.is_code = True 32 | elif tag == 'span' and ('data-type', 'tex') in attrs: 33 | self.is_math = True 34 | 35 | if ('data-display', 'inline') in attrs: 36 | self.display = 'inline' 37 | elif ('data-display', 'block') in attrs: 38 | self.display = 'block' 39 | 40 | def handle_data(self, data): 41 | if self.is_code: 42 | self.data += data 43 | elif self.is_math: 44 | self.data += data 45 | 46 | 47 | def _get_html_contents(html): 48 | """Process a HTML block and detects whether it is a code block, 49 | a math block, or a regular HTML block.""" 50 | parser = MyHTMLParser() 51 | parser.feed(html) 52 | if parser.is_code: 53 | return ('code', parser.data.strip()) 54 | elif parser.is_math: 55 | return ('math', parser.data.strip()) 56 | else: 57 | return '', '' 58 | 59 | 60 | #------------------------------------------------------------------------------ 61 | # Atlas 62 | #------------------------------------------------------------------------------ 63 | 64 | class AtlasReader(BaseMarkdownReader): 65 | 66 | code_wrap = ('
\n'
 69 |                  '{code}\n'
 70 |                  '
') 71 | 72 | math_wrap = '{equation}' 73 | 74 | # Utility methods 75 | # ------------------------------------------------------------------------- 76 | 77 | def _remove_math_span(self, source): 78 | # Remove any equation tag that would be in a Markdown cell. 79 | source = source.replace('', '') 80 | source = source.replace('', '') 81 | return source 82 | 83 | # Parser 84 | # ------------------------------------------------------------------------- 85 | 86 | def parse_fences(self, m): 87 | return self._markdown_cell_from_regex(m) 88 | 89 | def parse_block_code(self, m): 90 | return self._markdown_cell_from_regex(m) 91 | 92 | def parse_block_html(self, m): 93 | text = m.group(0).strip() 94 | 95 | type, contents = _get_html_contents(text) 96 | if type == 'code': 97 | return self._code_cell(contents) 98 | elif type == 'math': 99 | return self._markdown_cell(contents) 100 | else: 101 | return self._markdown_cell(text) 102 | 103 | def parse_text(self, m): 104 | text = m.group(0).strip() 105 | 106 | if (text.startswith('')): 108 | # Replace '\\(' by '$$' in the notebook. 109 | text = text.replace('\\\\(', '$$') 110 | text = text.replace('\\\\)', '$$') 111 | text = text.strip() 112 | else: 113 | # Process math equations. 114 | text = text.replace('\\\\(', '$') 115 | text = text.replace('\\\\)', '$') 116 | 117 | # Remove the math . 118 | text = self._remove_math_span(text) 119 | 120 | # Add the processed Markdown cell. 121 | return self._markdown_cell(text.strip()) 122 | 123 | 124 | class AtlasWriter(BaseMarkdownWriter): 125 | 126 | _math_regex = '''(?P[\$]{1,2})([^\$]+)(?P=dollars)''' 127 | 128 | def append_markdown(self, source, metadata=None): 129 | source = _ensure_string(source) 130 | # Wrap math equations. 131 | source = re.sub(self._math_regex, 132 | AtlasReader.math_wrap.format(equation=r'\\\\(\2\\\\)'), 133 | source) 134 | # Write the processed Markdown. 135 | self._output.write(source.rstrip()) 136 | 137 | def append_code(self, input, output=None, metadata=None): 138 | # Wrap code. 139 | wrapped = AtlasReader.code_wrap.format(lang='python', code=input) 140 | # Write the HTML code block. 141 | self._output.write(wrapped) 142 | 143 | 144 | ATLAS_FORMAT = dict( 145 | reader=AtlasReader, 146 | writer=AtlasWriter, 147 | file_extension='.md', 148 | file_type='text', 149 | ) 150 | -------------------------------------------------------------------------------- /ipymd/lib/tests/test_opendocument.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test OpenDocument routines.""" 4 | 5 | 6 | # ----------------------------------------------------------------------------- 7 | # Imports 8 | # ----------------------------------------------------------------------------- 9 | 10 | from ..base_lexer import BaseRenderer 11 | from ..markdown import BlockLexer 12 | from ..opendocument import (ODFDocument, ODFRenderer, BaseODFReader, 13 | odf_to_markdown, 14 | markdown_to_odf, 15 | _merge_text) 16 | from ...utils.utils import _show_outputs 17 | 18 | 19 | # ----------------------------------------------------------------------------- 20 | # Test ODFDocument 21 | # ----------------------------------------------------------------------------- 22 | 23 | def test_merge_text(): 24 | items = [{'tag': 'span', 'text': '1'}, 25 | {'tag': 'span', 'text': '2'}, 26 | {'tag': 'span', 'text': '-', 'style': 'bold'}, 27 | {'tag': 'span', 'text': '3'}, 28 | {'tag': 'span', 'text': '4'}] 29 | merged = _merge_text(*items) 30 | assert merged == [{'tag': 'span', 'text': '12'}, 31 | {'tag': 'span', 'text': '-', 'style': 'bold'}, 32 | {'tag': 'span', 'text': '34'}] 33 | 34 | 35 | def _example_opendocument(): 36 | doc = ODFDocument() 37 | 38 | doc.heading("The title", 1) 39 | 40 | with doc.paragraph(): 41 | doc.text("Some text. ") 42 | doc.bold("This is bold.") 43 | 44 | with doc.list(): 45 | with doc.list_item(): 46 | with doc.paragraph(): 47 | doc.text("Item 1.") 48 | with doc.list_item(): 49 | with doc.paragraph(): 50 | doc.text("Item 2.") 51 | with doc.list(): 52 | with doc.list_item(): 53 | with doc.paragraph(): 54 | doc.text("Item 2.1. This is ") 55 | doc.inline_code("code") 56 | doc.text(". Oh, and here is a link: ") 57 | doc.link("http://google.com") 58 | doc.text(".") 59 | with doc.list_item(): 60 | with doc.paragraph(): 61 | doc.text("Item 2.2.") 62 | with doc.list_item(): 63 | with doc.paragraph(): 64 | doc.text("Item 3.") 65 | 66 | doc.start_quote() 67 | with doc.paragraph(): 68 | doc.text("This is a citation.") 69 | doc.linebreak() 70 | doc.text("End.") 71 | doc.end_quote() 72 | 73 | with doc.numbered_list(): 74 | with doc.list_item(): 75 | with doc.paragraph(): 76 | doc.text("Item 1.") 77 | with doc.list_item(): 78 | with doc.paragraph(): 79 | doc.text("Item 2.") 80 | 81 | doc.code("def f():\n" 82 | " print('Hello world!')\n") 83 | 84 | with doc.paragraph(): 85 | doc.text("End.") 86 | 87 | return doc 88 | 89 | 90 | def _example_markdown(): 91 | return '\n'.join(('# The title', 92 | '', 93 | 'Some text. **This is bold.**', 94 | '', 95 | '* Item 1.', 96 | '* Item 2.', 97 | (' * Item 2.1. This is `code`. ' 98 | 'Oh, and here is a link: http://google.com.'), 99 | ' * Item 2.2.', 100 | '* Item 3.', 101 | '', 102 | '> This is a citation.', 103 | '> End.', 104 | '', 105 | '1. Item 1.', 106 | '2. Item 2.', 107 | '', 108 | '```', 109 | 'def f():', 110 | ' print(\'Hello world!\')', 111 | '```', 112 | '', 113 | 'End.', 114 | '')) 115 | 116 | 117 | def test_odf_document(): 118 | doc = _example_opendocument() 119 | doc.show_styles() 120 | 121 | 122 | def test_odf_renderer(): 123 | doc = ODFDocument() 124 | renderer = ODFRenderer(doc) 125 | block_lexer = BlockLexer(renderer=renderer) 126 | text = "Hello world!" 127 | block_lexer.read(text) 128 | 129 | 130 | def test_odf_reader(): 131 | doc = _example_opendocument() 132 | reader = BaseODFReader() 133 | 134 | _items = [] 135 | 136 | @reader.handler 137 | def f(name, *args, **kwargs): 138 | _items.append(name) 139 | 140 | reader.read(doc) 141 | 142 | assert len(_items) == 64 143 | 144 | 145 | # ----------------------------------------------------------------------------- 146 | # Test ODF <=> Markdown converter 147 | # ----------------------------------------------------------------------------- 148 | 149 | def test_odf_markdown_converter(): 150 | doc = _example_opendocument() 151 | md = _example_markdown() 152 | converted = odf_to_markdown(doc) 153 | 154 | assert md == converted 155 | 156 | 157 | def test_markdown_odf_converter(): 158 | doc = _example_opendocument() 159 | md = _example_markdown() 160 | converted = markdown_to_odf(md) 161 | 162 | assert doc == converted 163 | -------------------------------------------------------------------------------- /ipymd/formats/notebook.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Notebook reader and writer.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | import json 10 | 11 | try: 12 | import nbformat as nbf 13 | from nbformat.v4.nbbase import validate 14 | except ImportError: 15 | import IPython.nbformat as nbf 16 | from IPython.nbformat.v4.nbbase import validate 17 | 18 | from ..lib.markdown import MarkdownFilter 19 | from ..lib.python import PythonFilter 20 | from ..ext.six import string_types 21 | from ..utils.utils import _ensure_string 22 | 23 | 24 | #------------------------------------------------------------------------------ 25 | # Utility functions 26 | #------------------------------------------------------------------------------ 27 | 28 | def _cell_input(cell): 29 | """Return the input of an ipynb cell.""" 30 | return _ensure_string(cell.get('source', [])) 31 | 32 | 33 | def _cell_output(cell): 34 | """Return the output of an ipynb cell.""" 35 | outputs = cell.get('outputs', []) 36 | # Add stdout. 37 | stdout = ('\n'.join(_ensure_string(output.get('text', '')) 38 | for output in outputs)).rstrip() 39 | # Add text output. 40 | text_outputs = [] 41 | for output in outputs: 42 | out = output.get('data', {}).get('text/plain', []) 43 | out = _ensure_string(out) 44 | # HACK: skip outputs. 45 | if out.startswith('= 4 81 | 82 | yield { 83 | 'cell_type': 'notebook_metadata', 84 | "metadata": nb['metadata'] 85 | } 86 | 87 | for cell in nb['cells']: 88 | ipymd_cell = {} 89 | metadata = self.clean_meta(cell) 90 | if metadata: 91 | ipymd_cell['metadata'] = metadata 92 | ctype = cell['cell_type'] 93 | ipymd_cell['cell_type'] = ctype 94 | if ctype == 'code': 95 | ipymd_cell['input'] = _cell_input(cell) 96 | ipymd_cell['output'] = _cell_output(cell) 97 | elif ctype == 'markdown': 98 | ipymd_cell['source'] = _ensure_string(cell['source']) 99 | else: 100 | continue 101 | yield ipymd_cell 102 | 103 | def clean_meta(self, cell): 104 | metadata = cell.get('metadata', {}) 105 | for key in self.ignore_meta: 106 | metadata.pop(key, None) 107 | return metadata 108 | 109 | 110 | #------------------------------------------------------------------------------ 111 | # Notebook writer 112 | #------------------------------------------------------------------------------ 113 | 114 | class NotebookWriter(object): 115 | def __init__(self, keep_markdown=None, ipymd_skip=False): 116 | self._nb = nbf.v4.new_notebook() 117 | self._count = 1 118 | self._markdown_filter = MarkdownFilter(keep_markdown) 119 | self._code_filter = PythonFilter(ipymd_skip=ipymd_skip) 120 | 121 | def append_markdown(self, source, metadata=None): 122 | # Filter Markdown contents. 123 | source = self._markdown_filter(source) 124 | if not source: 125 | return 126 | self._nb['cells'].append( 127 | nbf.v4.new_markdown_cell(source, 128 | metadata=metadata)) 129 | 130 | def append_code(self, input, output=None, image=None, metadata=None): 131 | input = self._code_filter(input) 132 | cell = nbf.v4.new_code_cell(input, 133 | execution_count=self._count, 134 | metadata=metadata) 135 | if output: 136 | cell.outputs.append(nbf.v4.new_output('execute_result', 137 | {'text/plain': output}, 138 | execution_count=self._count, 139 | metadata={}, 140 | )) 141 | if image: 142 | # TODO 143 | raise NotImplementedError("Output images not implemented yet.") 144 | self._nb['cells'].append(cell) 145 | self._count += 1 146 | 147 | def write_notebook_metadata(self, metadata): 148 | self._nb.metadata.update(metadata) 149 | 150 | def write(self, cell): 151 | metadata = cell.get("metadata", {}) 152 | if cell['cell_type'] == 'markdown': 153 | self.append_markdown(cell['source'], metadata=metadata) 154 | elif cell['cell_type'] == 'code': 155 | self.append_code(cell['input'], cell['output'], metadata=metadata) 156 | 157 | @property 158 | def contents(self): 159 | validate(self._nb) 160 | return self._nb 161 | 162 | 163 | NOTEBOOK_FORMAT = dict( 164 | reader=NotebookReader, 165 | writer=NotebookWriter, 166 | file_extension='.ipynb', 167 | file_type='json', 168 | ) 169 | -------------------------------------------------------------------------------- /ipymd/formats/python.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Python reader and writer.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | import re 10 | import ast 11 | from collections import OrderedDict 12 | 13 | from ..lib.base_lexer import BaseGrammar, BaseLexer 14 | from ..lib.markdown import MarkdownFilter 15 | from ..lib.python import _is_python 16 | from ..ext.six import StringIO 17 | from ..utils.utils import _ensure_string, _preprocess 18 | 19 | 20 | #------------------------------------------------------------------------------ 21 | # Python reader and writer 22 | #------------------------------------------------------------------------------ 23 | 24 | class PythonSplitGrammar(BaseGrammar): 25 | """Grammar used to split Python code into chunks while not cutting 26 | long Python strings.""" 27 | 28 | _triple_quotes = "'''" 29 | _triple_doublequotes = '"""' 30 | _triple = _triple_quotes + '|' + _triple_doublequotes 31 | 32 | # '''text''' or """text""". 33 | text_var = re.compile(r"^({0})((?!{0}).|\n)*?\1".format(_triple)) 34 | 35 | # Two new lines followed by non-space 36 | newline = re.compile(r'^[\n]{2,}(?=[^ ])') 37 | 38 | linebreak = re.compile(r'^\n+') 39 | other = re.compile(r'^(?!{0}).'.format(_triple)) 40 | 41 | 42 | class PythonSplitLexer(BaseLexer): 43 | """Lexer for splitting Python code into chunks.""" 44 | 45 | grammar_class = PythonSplitGrammar 46 | default_rules = ['text_var', 'newline', 'linebreak', 'other'] 47 | 48 | def __init__(self): 49 | super(PythonSplitLexer, self).__init__() 50 | self._chunks = [''] 51 | 52 | @property 53 | def current(self): 54 | if not self._chunks: 55 | return None 56 | else: 57 | return self._chunks[-1] 58 | 59 | @property 60 | def chunks(self): 61 | return [chunk for chunk in self._chunks if chunk] 62 | 63 | @current.setter 64 | def current(self, value): 65 | self._chunks[-1] = value 66 | 67 | def new_chunk(self): 68 | self._chunks.append('') 69 | 70 | def append(self, text): 71 | self.current += text 72 | 73 | def parse_newline(self, m): 74 | self.new_chunk() 75 | 76 | def parse_linebreak(self, m): 77 | self.append(m.group(0)) 78 | 79 | def parse_text_var(self, m): 80 | self.append(m.group(0)) 81 | 82 | def parse_other(self, m): 83 | self.append(m.group(0)) 84 | 85 | 86 | def _split_python(python): 87 | """Split Python source into chunks. 88 | 89 | Chunks are separated by at least two return lines. The break must not 90 | be followed by a space. Also, long Python strings spanning several lines 91 | are not splitted. 92 | 93 | """ 94 | python = _preprocess(python) 95 | if not python: 96 | return [] 97 | lexer = PythonSplitLexer() 98 | lexer.read(python) 99 | return lexer.chunks 100 | 101 | 102 | def _is_chunk_markdown(source): 103 | """Return whether a chunk contains Markdown contents.""" 104 | lines = source.splitlines() 105 | if all(line.startswith('# ') for line in lines): 106 | # The chunk is a Markdown *unless* it is commented Python code. 107 | source = '\n'.join(line[2:] for line in lines 108 | if not line[2:].startswith('#')) # skip headers 109 | if not source: 110 | return True 111 | # Try to parse the chunk: if it fails, it is Markdown, otherwise, 112 | # it is Python. 113 | return not _is_python(source) 114 | return False 115 | 116 | 117 | def _remove_hash(source): 118 | """Remove the leading '#' of every line in the source.""" 119 | return '\n'.join(line[2:].rstrip() for line in source.splitlines()) 120 | 121 | 122 | def _add_hash(source): 123 | """Add a leading hash '#' at the beginning of every line in the source.""" 124 | source = '\n'.join('# ' + line.rstrip() 125 | for line in source.splitlines()) 126 | return source 127 | 128 | 129 | class PythonReader(object): 130 | """Python reader.""" 131 | def read(self, python): 132 | chunks = _split_python(python) 133 | for chunk in chunks: 134 | if _is_chunk_markdown(chunk): 135 | yield self._markdown_cell(_remove_hash(chunk)) 136 | else: 137 | yield self._code_cell(chunk) 138 | 139 | def _code_cell(self, source): 140 | return {'cell_type': 'code', 141 | 'input': source, 142 | 'output': None} 143 | 144 | def _markdown_cell(self, source): 145 | return {'cell_type': 'markdown', 146 | 'source': source} 147 | 148 | 149 | class PythonWriter(object): 150 | """Python writer.""" 151 | def __init__(self, keep_markdown=None): 152 | self._output = StringIO() 153 | self._markdown_filter = MarkdownFilter(keep_markdown) 154 | 155 | def _new_paragraph(self): 156 | self._output.write('\n\n') 157 | 158 | def append_comments(self, source): 159 | source = source.rstrip() 160 | # Filter Markdown contents. 161 | source = self._markdown_filter(source) 162 | # Skip empty cells. 163 | if not source: 164 | return 165 | comments = _add_hash(source) 166 | self._output.write(comments) 167 | self._new_paragraph() 168 | 169 | def append_code(self, input): 170 | self._output.write(input) 171 | self._new_paragraph() 172 | 173 | def write(self, cell): 174 | if cell['cell_type'] == 'markdown': 175 | self.append_comments(cell['source']) 176 | elif cell['cell_type'] == 'code': 177 | self.append_code(cell['input']) 178 | 179 | @property 180 | def contents(self): 181 | return self._output.getvalue().rstrip() + '\n' # end of file \n 182 | 183 | def close(self): 184 | self._output.close() 185 | 186 | def __del__(self): 187 | self.close() 188 | 189 | 190 | PYTHON_FORMAT = dict( 191 | reader=PythonReader, 192 | writer=PythonWriter, 193 | file_extension='.py', 194 | file_type='text', 195 | ) 196 | -------------------------------------------------------------------------------- /ipymd/core/scripts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | 4 | """CLI scripts.""" 5 | 6 | #------------------------------------------------------------------------------ 7 | # Imports 8 | #------------------------------------------------------------------------------ 9 | 10 | import argparse 11 | import os 12 | import os.path as op 13 | import glob 14 | import json 15 | 16 | from ..ext.six import string_types 17 | from .format_manager import convert, format_manager 18 | 19 | 20 | #------------------------------------------------------------------------------ 21 | # Utility functions 22 | #------------------------------------------------------------------------------ 23 | 24 | def _flatten(l): 25 | return [item for sublist in l for item in sublist] 26 | 27 | 28 | def _ensure_list(l): 29 | if isinstance(l, string_types): 30 | return [l] 31 | elif isinstance(l, list): 32 | return l 33 | else: 34 | raise RuntimeError("This should be a string or a list: " 35 | "{0:s}.".format(str(l))) 36 | 37 | 38 | def _to_skip(dirname): 39 | out = op.basename(dirname).startswith(('.', '_', '/')) 40 | return out 41 | 42 | 43 | def _expand_dirs_to_files(files_or_dirs, recursive=False): 44 | files = [] 45 | files_or_dirs = _ensure_list(files_or_dirs) 46 | for file_or_dir in files_or_dirs: 47 | file_or_dir = op.realpath(file_or_dir) 48 | if op.isdir(file_or_dir): 49 | # Skip dirnames starting with '.' 50 | if _to_skip(file_or_dir): 51 | continue 52 | # Recursively visit the directories and add the files. 53 | if recursive: 54 | files.extend(_expand_dirs_to_files([op.join(file_or_dir, file) 55 | for file in os.listdir(file_or_dir)], 56 | recursive=recursive)) 57 | else: 58 | files.extend([op.join(file_or_dir, file) 59 | for file in os.listdir(file_or_dir)]) 60 | elif '*' in file_or_dir: 61 | files.extend(glob.glob(file_or_dir)) 62 | else: 63 | files.append(file_or_dir) 64 | return files 65 | 66 | 67 | def _common_root(files): 68 | files = [op.realpath(file) for file in files] 69 | root = op.commonprefix(files) 70 | if not op.exists(root): 71 | root = op.dirname(root) 72 | if root: 73 | assert op.exists(root) 74 | assert op.isdir(root), root 75 | return root 76 | 77 | 78 | def _construct_tree(path): 79 | if not op.exists(path): 80 | try: 81 | os.makedirs(op.dirname(path)) 82 | except OSError: 83 | pass 84 | 85 | 86 | def _file_has_extension(file, extensions): 87 | if not isinstance(extensions, list): 88 | extensions = [extensions] 89 | return any(file.endswith(extension) for extension in extensions) 90 | 91 | 92 | def _filter_files_by_extension(files, extensions): 93 | return [file for file in files if _file_has_extension(file, extensions)] 94 | 95 | 96 | def _load_file(file, from_): 97 | return format_manager().load(file, name=from_) 98 | 99 | 100 | def _save_file(file, to, contents, overwrite=False): 101 | format_manager().save(file, contents, name=to, overwrite=overwrite) 102 | 103 | 104 | #------------------------------------------------------------------------------ 105 | # Conversion functions 106 | #------------------------------------------------------------------------------ 107 | 108 | def _converted_filename(file, from_, to): 109 | base, from_extension = op.splitext(file) 110 | to_extension = format_manager().file_extension(to) 111 | return ''.join((base, to_extension)) 112 | 113 | 114 | def convert_files(files_or_dirs, 115 | overwrite=None, 116 | from_=None, 117 | to=None, 118 | from_kwargs=None, 119 | to_kwargs=None, 120 | output_folder=None, 121 | recursive=False, 122 | simulate=False, 123 | extension=None, 124 | ): 125 | # Find all files. 126 | files = _expand_dirs_to_files(files_or_dirs, recursive=recursive) 127 | 128 | # Filter by from extension. 129 | from_extension = format_manager().file_extension(from_) 130 | files = _filter_files_by_extension(files, from_extension) 131 | 132 | # Get the common root of all files. 133 | if output_folder: 134 | output_folder = op.realpath(output_folder) 135 | root = _common_root(files) if len(files) > 1 else op.dirname(files[0]) 136 | 137 | # Convert all files. 138 | for file in files: 139 | print("Converting {0:s}...".format(file), end=' ') 140 | converted = convert(file, from_, to, 141 | from_kwargs=from_kwargs, to_kwargs=to_kwargs) 142 | file_to = _converted_filename(file, from_, to) 143 | if extension: 144 | file_to = op.splitext(file_to)[0] + '.' + extension 145 | print("done.") 146 | 147 | # Compute the output path. 148 | if output_folder: 149 | # Path relative to the common root. 150 | rel_file = op.relpath(file_to, root) 151 | # Reconstruct the internal folder structure within the output 152 | # folder. 153 | file_to = op.join(output_folder, rel_file) 154 | # Create the subfolders if necessary. 155 | _construct_tree(file_to) 156 | 157 | print(" Saving to {0:s}...".format(file_to), end=' ') 158 | if simulate: 159 | print("skipped (simulation).") 160 | else: 161 | _save_file(file_to, to, converted, overwrite=overwrite) 162 | print('done.') 163 | 164 | 165 | def main(): 166 | desc = 'Convert files across formats supported by ipymd.' 167 | parser = argparse.ArgumentParser(description=desc) 168 | 169 | parser.add_argument('files_or_dirs', nargs='+', 170 | help=('list of files or directories to convert')) 171 | 172 | formats = ', '.join(format_manager().formats) 173 | parser.add_argument('--from', dest='from_', required=True, 174 | help='one of {0:s}'.format(formats)) 175 | 176 | parser.add_argument('--to', dest='to', required=True, 177 | help='one of {0:s}'.format(formats)) 178 | 179 | parser.add_argument('--output', dest='output', 180 | help='output folder') 181 | 182 | parser.add_argument('--extension', dest='extension', 183 | help='output file extension') 184 | 185 | parser.add_argument('--overwrite', dest='overwrite', action='store_true', 186 | help=('overwrite target file if it exists ' 187 | '(false by default)')) 188 | 189 | # Parse the CLI arguments. 190 | args = parser.parse_args() 191 | convert_files(args.files_or_dirs, 192 | overwrite=args.overwrite, 193 | from_=args.from_, 194 | to=args.to, 195 | extension=args.extension, 196 | output_folder=args.output, 197 | ) 198 | 199 | 200 | if __name__ == '__main__': 201 | main() 202 | -------------------------------------------------------------------------------- /ipymd/core/contents_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Notebook contents manager.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | import io 10 | import os 11 | import os.path as op 12 | 13 | from tornado import web 14 | 15 | try: 16 | import nbformat 17 | except ImportError: 18 | from IPython import nbformat 19 | 20 | try: 21 | from traitlets import Unicode, Bool 22 | from traitlets.config import Configurable 23 | except ImportError: 24 | from IPython.utils.traitlets import Unicode, Bool 25 | from IPython.config.configurable import Configurable 26 | 27 | try: 28 | from notebook import transutils 29 | from notebook.services.contents.filemanager import FileContentsManager 30 | except ImportError: 31 | from IPython.html.services.contents.filemanager import FileContentsManager 32 | 33 | from .format_manager import convert, format_manager 34 | from ipymd.ext.six.moves.urllib.error import HTTPError 35 | 36 | 37 | #------------------------------------------------------------------------------ 38 | # MarkdownContentsManager 39 | #------------------------------------------------------------------------------ 40 | 41 | def _file_extension(os_path): 42 | return op.splitext(os_path)[1] 43 | 44 | 45 | class IPymdContentsManager(FileContentsManager, Configurable): 46 | format = Unicode('markdown', config=True) 47 | 48 | # The name of the default kernel: if left blank, assume native (pythonX), 49 | # won't store kernelspec/language_info unless forced with verbose_metadata. 50 | # This will be passed to the FormatManager, overwriting any config there. 51 | default_kernel_name = Unicode(config=True) 52 | 53 | # Don't strip any metadata. 54 | # This will be passed to the FormatManager, overwriting any config there. 55 | verbose_metadata = Bool(False, config=True) 56 | 57 | def __init__(self, *args, **kwargs): 58 | super(IPymdContentsManager, self).__init__(*args, **kwargs) 59 | 60 | self._fm = format_manager() 61 | self._fm.default_kernel_name = self.default_kernel_name 62 | self._fm.verbose_metadata = self.verbose_metadata 63 | 64 | def get(self, path, content=True, type=None, format=None): 65 | """ Takes a path for an entity and returns its model 66 | Parameters 67 | ---------- 68 | path : str 69 | the API path that describes the relative path for the target 70 | content : bool 71 | Whether to include the contents in the reply 72 | type : str, optional 73 | The requested type - 'file', 'notebook', or 'directory'. 74 | Will raise HTTPError 400 if the content doesn't match. 75 | format : str, optional 76 | The requested format for file contents. 'text' or 'base64'. 77 | Ignored if this returns a notebook or directory model. 78 | Returns 79 | ------- 80 | model : dict 81 | the contents model. If content=True, returns the contents 82 | of the file or directory as well. 83 | """ 84 | path = path.strip('/') 85 | 86 | # File extension of the chosen format. 87 | file_extension = format_manager().file_extension(self.format) 88 | 89 | if not self.exists(path): 90 | raise web.HTTPError(404, u'No such file or directory: %s' % path) 91 | 92 | os_path = self._get_os_path(path) 93 | if os.path.isdir(os_path): 94 | if type not in (None, 'directory'): 95 | raise web.HTTPError(400, 96 | u'%s is a directory, not a %s' % (path, type), reason='bad type') 97 | model = self._dir_model(path, content=content) 98 | elif type == 'notebook' or (type is None and 99 | (path.endswith('.ipynb') or 100 | path.endswith(file_extension))): # NEW 101 | model = self._notebook_model(path, content=content) 102 | else: 103 | if type == 'directory': 104 | raise web.HTTPError(400, 105 | u'%s is not a directory', reason='bad type') 106 | model = self._file_model(path, content=content, format=format) 107 | return model 108 | 109 | 110 | def _read_notebook(self, os_path, as_version=4): 111 | """Read a notebook from an os path.""" 112 | with self.open(os_path, 'r', encoding='utf-8') as f: 113 | try: 114 | 115 | # NEW 116 | file_ext = _file_extension(os_path) 117 | if file_ext == '.ipynb': 118 | return nbformat.read(f, as_version=as_version) 119 | else: 120 | return convert(os_path, from_=self.format, to='notebook') 121 | 122 | except Exception as e: 123 | raise HTTPError( 124 | 400, 125 | u"Unreadable Notebook: %s %r" % (os_path, e), 126 | ) 127 | 128 | def save(self, model, path=''): 129 | """Save the file model and return the model with no content.""" 130 | path = path.strip('/') 131 | 132 | if 'type' not in model: 133 | raise web.HTTPError(400, u'No file type provided') 134 | if 'content' not in model and model['type'] != 'directory': 135 | raise web.HTTPError(400, u'No file content provided') 136 | 137 | self.run_pre_save_hook(model=model, path=path) 138 | 139 | os_path = self._get_os_path(path) 140 | self.log.debug("Saving %s", os_path) 141 | try: 142 | if model['type'] == 'notebook': 143 | 144 | # NEW 145 | file_ext = _file_extension(os_path) 146 | if file_ext == '.ipynb': 147 | nb = nbformat.from_dict(model['content']) 148 | self.check_and_sign(nb, path) 149 | self._save_notebook(os_path, nb) 150 | else: 151 | 152 | contents = convert(model['content'], 153 | from_='notebook', 154 | to=self.format) 155 | 156 | # Save a text file. 157 | if (format_manager().file_type(self.format) in 158 | ('text', 'json')): 159 | self._save_file(os_path, contents, 'text') 160 | # Save to a binary file. 161 | else: 162 | format_manager().save(os_path, contents, 163 | name=self.format, 164 | overwrite=True) 165 | 166 | # One checkpoint should always exist for notebooks. 167 | if not self.checkpoints.list_checkpoints(path): 168 | self.create_checkpoint(path) 169 | elif model['type'] == 'file': 170 | # Missing format will be handled internally by _save_file. 171 | self._save_file(os_path, model['content'], model.get('format')) 172 | elif model['type'] == 'directory': 173 | self._save_directory(os_path, model, path) 174 | else: 175 | raise web.HTTPError(400, "Unhandled contents type: %s" % model['type']) 176 | except web.HTTPError: 177 | raise 178 | except Exception as e: 179 | self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True) 180 | raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e)) 181 | 182 | validation_message = None 183 | if model['type'] == 'notebook': 184 | self.validate_notebook_model(model) 185 | validation_message = model.get('message', None) 186 | 187 | model = self.get(path, content=False) 188 | if validation_message: 189 | model['message'] = validation_message 190 | 191 | self.run_post_save_hook(model=model, os_path=os_path) 192 | 193 | return model 194 | -------------------------------------------------------------------------------- /ipymd/core/prompt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Prompt manager.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | import re 10 | 11 | 12 | #------------------------------------------------------------------------------ 13 | # Base Prompt manager 14 | #------------------------------------------------------------------------------ 15 | 16 | def _to_lines(code): 17 | return [line.rstrip() for line in code.rstrip().splitlines()] 18 | 19 | 20 | def _to_code(lines): 21 | return '\n'.join(line.rstrip() for line in lines) 22 | 23 | 24 | def _add_line_prefix(lines, prefix): 25 | return [prefix + line for line in lines] 26 | 27 | 28 | def _template_to_regex(template): 29 | regex = template 30 | # Escape special characters. 31 | for char in r'{}[]()+*?:-': 32 | regex = regex.replace(char, '\\' + char) 33 | regex = regex.replace(r'\{n\}', r'\d+') 34 | return regex 35 | 36 | 37 | def _starts_with_regex(line, regex): 38 | """Return whether a line starts with a regex or not.""" 39 | if not regex.startswith('^'): 40 | regex = '^' + regex 41 | reg = re.compile(regex) 42 | return reg.match(line) 43 | 44 | 45 | class BasePromptManager(object): 46 | """Add and remove prompt from code cells.""" 47 | 48 | input_prompt_template = '' # may contain {n} for the input number 49 | output_prompt_template = '' 50 | 51 | input_prompt_regex = '' 52 | output_prompt_regex = '' 53 | 54 | def __init__(self): 55 | self.reset() 56 | if not self.input_prompt_regex: 57 | self.input_prompt_regex = _template_to_regex( 58 | self.input_prompt_template) 59 | if not self.output_prompt_regex: 60 | self.output_prompt_regex = _template_to_regex( 61 | self.output_prompt_template) 62 | 63 | def reset(self): 64 | self._number = 1 65 | 66 | def _replace_template(self, pattern, **by): 67 | if not by: 68 | by = dict(n=self._number) 69 | if '{n}' in pattern: 70 | return pattern.format(**by) 71 | else: 72 | return pattern 73 | 74 | @property 75 | def input_prompt(self): 76 | return self._replace_template(self.input_prompt_template) 77 | 78 | @property 79 | def output_prompt(self): 80 | return self._replace_template(self.output_prompt_template) 81 | 82 | def is_input(self, line): 83 | """Return whether a code line is an input, based on the input 84 | prompt.""" 85 | return _starts_with_regex(line, self.input_prompt_regex) 86 | 87 | def split_input_output(self, text): 88 | """Split code into input lines and output lines, according to the 89 | input and output prompt templates.""" 90 | lines = _to_lines(text) 91 | i = 0 92 | for line in lines: 93 | if _starts_with_regex(line, self.input_prompt_regex): 94 | i += 1 95 | else: 96 | break 97 | return lines[:i], lines[i:] 98 | 99 | def from_cell(self, input, output): 100 | """Convert input and output to code text with prompts.""" 101 | raise NotImplementedError() 102 | 103 | def to_cell(self, code): 104 | """Convert code text with prompts to input and output.""" 105 | raise NotImplementedError() 106 | 107 | 108 | #------------------------------------------------------------------------------ 109 | # Simple prompt manager 110 | #------------------------------------------------------------------------------ 111 | 112 | class SimplePromptManager(BasePromptManager): 113 | """No prompt number, same input prompt at every line, idem for output.""" 114 | input_prompt_template = '' 115 | output_prompt_template = '' 116 | 117 | def from_cell(self, input, output): 118 | input_l = _to_lines(input) 119 | output_l = _to_lines(output) 120 | 121 | input_l = _add_line_prefix(input_l, self.input_prompt) 122 | output_l = _add_line_prefix(output_l, self.output_prompt) 123 | 124 | return _to_code(input_l) + '\n' + _to_code(output_l) 125 | 126 | def to_cell(self, code): 127 | input_l, output_l = self.split_input_output(code) 128 | 129 | n = len(self.input_prompt_template) 130 | input = _to_code([line[n:] for line in input_l]) 131 | 132 | n = len(self.output_prompt_template) 133 | output = _to_code([line[n:] for line in output_l]) 134 | 135 | return input.rstrip(), output.rstrip() 136 | 137 | 138 | #------------------------------------------------------------------------------ 139 | # IPython prompt manager 140 | #------------------------------------------------------------------------------ 141 | 142 | class IPythonPromptManager(BasePromptManager): 143 | input_prompt_template = 'In [{n}]: ' 144 | input_prompt_regex = '(In \[\d+\]\: | {6,})' 145 | 146 | output_prompt_template = 'Out[{n}]: ' 147 | 148 | def _add_prompt(self, lines, prompt): 149 | lines[:1] = _add_line_prefix(lines[:1], prompt) 150 | lines[1:] = _add_line_prefix(lines[1:], ' ' * len(prompt)) 151 | return lines 152 | 153 | def from_cell(self, input, output=None): 154 | input_l = _to_lines(input) 155 | output_l = _to_lines(output) 156 | 157 | input_l = self._add_prompt(input_l, self.input_prompt) 158 | output_l = self._add_prompt(output_l, self.output_prompt) 159 | 160 | input_p = _to_code(input_l) 161 | output_p = _to_code(output_l) 162 | 163 | self._number += 1 164 | 165 | return input_p + '\n' + output_p 166 | 167 | def to_cell(self, text): 168 | input_l, output_l = self.split_input_output(text) 169 | 170 | m = _starts_with_regex(input_l[0], self.input_prompt_regex) 171 | assert m 172 | input_prompt = m.group(0) 173 | n_in = len(input_prompt) 174 | input_l = [line[n_in:] for line in input_l] 175 | input = _to_code(input_l) 176 | 177 | m = _starts_with_regex(output_l[0], self.output_prompt_regex) 178 | assert m 179 | output_prompt = m.group(0) 180 | n_out = len(output_prompt) 181 | output_l = [line[n_out:] for line in output_l] 182 | output = _to_code(output_l) 183 | 184 | return input, output 185 | 186 | 187 | #------------------------------------------------------------------------------ 188 | # Python prompt manager 189 | #------------------------------------------------------------------------------ 190 | 191 | class PythonPromptManager(SimplePromptManager): 192 | input_prompt_template = '>>> ' 193 | second_input_prompt_template = '... ' 194 | 195 | input_prompt_regex = r'>>>|\.\.\.' 196 | 197 | output_prompt_template = '' 198 | 199 | def from_cell(self, input, output): 200 | lines = _to_lines(input) 201 | first = self.input_prompt_template 202 | second = self.second_input_prompt_template 203 | 204 | lines_prompt = [] 205 | prompt = first 206 | lock = False 207 | for line in lines: 208 | if line.startswith('%%'): 209 | lines_prompt.append(prompt + line) 210 | prompt = second 211 | lock = True 212 | elif line.startswith('#') or line.startswith('@'): 213 | lines_prompt.append(prompt + line) 214 | prompt = second 215 | # Empty line = second prompt. 216 | elif line.rstrip() == '': 217 | lines_prompt.append((second + line).rstrip()) 218 | elif line.startswith(' '): 219 | prompt = second 220 | lines_prompt.append(prompt + line) 221 | if not lock: 222 | prompt = first 223 | else: 224 | lines_prompt.append(prompt + line) 225 | if not lock: 226 | prompt = first 227 | 228 | return _to_code(lines_prompt) + '\n' + output.rstrip() 229 | 230 | 231 | def create_prompt(prompt): 232 | """Create a prompt manager. 233 | 234 | Parameters 235 | ---------- 236 | 237 | prompt : str or class driving from BasePromptManager 238 | The prompt name ('python' or 'ipython') or a custom PromptManager 239 | class. 240 | 241 | """ 242 | if prompt is None: 243 | prompt = 'python' 244 | if prompt == 'python': 245 | prompt = PythonPromptManager 246 | elif prompt == 'ipython': 247 | prompt = IPythonPromptManager 248 | # Instanciate the class. 249 | if isinstance(prompt, BasePromptManager): 250 | return prompt 251 | else: 252 | return prompt() 253 | -------------------------------------------------------------------------------- /ipymd/formats/markdown.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Markdown readers and writers 4 | 5 | Much of the code comes from the mistune library. 6 | 7 | """ 8 | 9 | #------------------------------------------------------------------------------ 10 | # Imports 11 | #------------------------------------------------------------------------------ 12 | 13 | import re 14 | from collections import OrderedDict 15 | 16 | import yaml 17 | 18 | from ..ext.six import StringIO 19 | from ..utils.utils import _ensure_string, _preprocess 20 | from ..lib.markdown import (BlockGrammar, BlockLexer, 21 | InlineGrammar, InlineLexer) 22 | from ..core.prompt import create_prompt 23 | 24 | 25 | #------------------------------------------------------------------------------ 26 | # Base Markdown 27 | #------------------------------------------------------------------------------ 28 | 29 | class BaseMarkdownReader(BlockLexer): 30 | def __init__(self): 31 | grammar = BlockGrammar() 32 | grammar.text = re.compile(r'^.+?\n\n|.+?$', re.DOTALL) 33 | rules = ['block_code', 'fences', 'meta', 'block_html', 'text', 34 | 'newline'] 35 | super(BaseMarkdownReader, self).__init__(grammar=grammar, 36 | rules=rules) 37 | 38 | def parse_block_code(self, m): 39 | raise NotImplementedError("This method must be overriden.") 40 | 41 | def parse_fences(self, m): 42 | # 2: lang 43 | # 3: code 44 | raise NotImplementedError("This method must be overriden.") 45 | 46 | def parse_block_html(self, m): 47 | raise NotImplementedError("This method must be overriden.") 48 | 49 | def parse_text(self, m): 50 | raise NotImplementedError("This method must be overriden.") 51 | 52 | def parse_newline(self, m): 53 | pass 54 | 55 | def _code_cell(self, source): 56 | # Can be overriden to separate input/output from source. 57 | return {'cell_type': 'code', 58 | 'input': source, 59 | 'output': None} 60 | 61 | def _markdown_cell(self, source): 62 | return {'cell_type': 'markdown', 63 | 'source': source} 64 | 65 | def _meta(self, source, is_notebook=False): 66 | """Turn a YAML string into ipynb cell/notebook metadata 67 | """ 68 | if is_notebook: 69 | return {'cell_type': 'notebook_metadata', 70 | 'metadata': source} 71 | return {'cell_type': 'cell_metadata', 72 | 'metadata': source} 73 | 74 | def _markdown_cell_from_regex(self, m): 75 | return self._markdown_cell(m.group(0).rstrip()) 76 | 77 | def _meta_from_regex(self, m): 78 | """Extract and parse YAML metadata from a meta match 79 | 80 | Notebook metadata must appear at the beginning of the file and follows 81 | the Jekyll front-matter convention of dashed delimiters: 82 | 83 | --- 84 | some: yaml 85 | --- 86 | 87 | Cell metadata follows the YAML spec of dashes and periods 88 | 89 | --- 90 | some: yaml 91 | ... 92 | 93 | Both must be followed by at least one blank line (\n\n). 94 | """ 95 | body = m.group('body') 96 | is_notebook = m.group('sep_close') == '---' 97 | 98 | if is_notebook: 99 | # make it into a valid YAML object by stripping --- 100 | body = body.strip()[:-3] + '...' 101 | try: 102 | if body: 103 | return self._meta(yaml.safe_load(m.group('body')), is_notebook) 104 | else: 105 | return self._meta({'ipymd': {'empty_meta': True}}, is_notebook) 106 | except Exception as err: 107 | raise Exception(body, err) 108 | 109 | 110 | class BaseMarkdownWriter(object): 111 | """Base Markdown writer.""" 112 | 113 | def __init__(self): 114 | self._output = StringIO() 115 | 116 | def _new_paragraph(self): 117 | self._output.write('\n\n') 118 | 119 | def meta(self, source, is_notebook=False): 120 | if source is None: 121 | return '' 122 | 123 | if source.get('ipymd', {}).get('empty_meta', None): 124 | return '---\n\n' 125 | 126 | if not source: 127 | if is_notebook: 128 | return '' 129 | return '---\n\n' 130 | 131 | meta = '{}\n'.format(yaml.safe_dump(source, 132 | explicit_start=True, 133 | explicit_end=True, 134 | default_flow_style=False)) 135 | 136 | if is_notebook: 137 | # Replace the trailing `...\n\n` 138 | meta = meta[:-5] + '---\n\n' 139 | 140 | return meta 141 | 142 | def append_markdown(self, source, metadata): 143 | source = _ensure_string(source) 144 | self._output.write(self.meta(metadata) + source.rstrip()) 145 | 146 | def append_code(self, input, output=None, metadata=None): 147 | raise NotImplementedError("This method must be overriden.") 148 | 149 | def write_notebook_metadata(self, metadata): 150 | self._output.write(self.meta(metadata, is_notebook=True)) 151 | 152 | def write(self, cell): 153 | metadata = cell.get('metadata', None) 154 | if cell['cell_type'] == 'markdown': 155 | self.append_markdown(cell['source'], metadata) 156 | elif cell['cell_type'] == 'code': 157 | self.append_code(cell['input'], cell['output'], metadata) 158 | self._new_paragraph() 159 | 160 | @property 161 | def contents(self): 162 | return self._output.getvalue().rstrip() + '\n' # end of file \n 163 | 164 | def close(self): 165 | self._output.close() 166 | 167 | def __del__(self): 168 | self.close() 169 | 170 | 171 | #------------------------------------------------------------------------------ 172 | # Default Markdown 173 | #------------------------------------------------------------------------------ 174 | 175 | class MarkdownReader(BaseMarkdownReader): 176 | """Default Markdown reader.""" 177 | 178 | def __init__(self, prompt=None): 179 | super(MarkdownReader, self).__init__() 180 | self._prompt = create_prompt(prompt) 181 | self._notebook_metadata = {} 182 | 183 | def read(self, text, rules=None): 184 | raw_cells = super(MarkdownReader, self).read(text, rules) 185 | cells = [] 186 | 187 | last_index = len(raw_cells) - 1 188 | 189 | for i, cell in enumerate(raw_cells): 190 | if cell['cell_type'] == 'cell_metadata': 191 | if i + 1 <= last_index: 192 | raw_cells[i + 1].update(metadata=cell['metadata']) 193 | else: 194 | cells.append(cell) 195 | 196 | return cells 197 | 198 | # Helper functions to generate ipymd cells 199 | # ------------------------------------------------------------------------- 200 | 201 | def _code_cell(self, source): 202 | """Split the source into input and output.""" 203 | input, output = self._prompt.to_cell(source) 204 | return {'cell_type': 'code', 205 | 'input': input, 206 | 'output': output} 207 | 208 | # Parser methods 209 | # ------------------------------------------------------------------------- 210 | 211 | def parse_fences(self, m): 212 | lang = m.group(2) 213 | code = m.group(3).rstrip() 214 | if lang == 'python': 215 | return self._code_cell(code) 216 | else: 217 | # Test the first line of the cell. 218 | first_line = code.splitlines()[0] 219 | if self._prompt.is_input(first_line): 220 | return self._code_cell(code) 221 | else: 222 | return self._markdown_cell_from_regex(m) 223 | 224 | def parse_block_code(self, m): 225 | return self._markdown_cell_from_regex(m) 226 | 227 | def parse_block_html(self, m): 228 | return self._markdown_cell_from_regex(m) 229 | 230 | def parse_text(self, m): 231 | return self._markdown_cell_from_regex(m) 232 | 233 | def parse_meta(self, m): 234 | return self._meta_from_regex(m) 235 | 236 | 237 | class MarkdownWriter(BaseMarkdownWriter): 238 | """Default Markdown writer.""" 239 | 240 | def __init__(self, prompt=None): 241 | super(MarkdownWriter, self).__init__() 242 | self._prompt = create_prompt(prompt) 243 | 244 | def append_code(self, input, output=None, metadata=None): 245 | code = self._prompt.from_cell(input, output) 246 | wrapped = '```python\n{code}\n```'.format(code=code.rstrip()) 247 | self._output.write(self.meta(metadata) + wrapped) 248 | 249 | 250 | MARKDOWN_FORMAT = dict( 251 | reader=MarkdownReader, 252 | writer=MarkdownWriter, 253 | file_extension='.md', 254 | file_type='text', 255 | ) 256 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/rossant/ipymd.svg?branch=travis)](https://travis-ci.org/rossant/ipymd) 2 | [![Coverage Status](https://coveralls.io/repos/rossant/ipymd/badge.svg)](https://coveralls.io/r/rossant/ipymd) 3 | 4 | # Replace .ipynb with .md in the IPython Notebook 5 | 6 | The goal of ipymd is to replace `.ipynb` notebook files like: 7 | 8 | ```json 9 | { 10 | "cells": [ 11 | { 12 | "cell_type": "markdown", 13 | "source": [ 14 | "Here is some Python code:" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "outputs": [ 20 | { 21 | "name": "stdout", 22 | "output_type": "stream", 23 | "text": [ 24 | "Hello world!\n" 25 | ] 26 | } 27 | ], 28 | "source": [ 29 | "print(\"Hello world!\")" 30 | ] 31 | } 32 | ... 33 | ] 34 | } 35 | ``` 36 | 37 | with: 38 | 39 | Here is some Python code: 40 | 41 | ```python 42 | >>> print("Hello world!") 43 | Hello world! 44 | ``` 45 | 46 | The JSON `.ipynb` are removed from the equation, and the conversion happens on the fly. The IPython Notebook becomes an interactive Markdown text editor! 47 | 48 | A drawback is that you lose prompt numbers and images (for now). 49 | 50 | This is useful when you write technical documents, blog posts, books, etc. 51 | 52 | ![image](https://cloud.githubusercontent.com/assets/1942359/5570181/f656a484-8f7d-11e4-8ec2-558d022b13d3.png) 53 | 54 | ## Installation 55 | 56 | 1. Install ipymd: 57 | 58 | To install the latest release version: 59 | 60 | ```shell 61 | pip install ipymd 62 | ``` 63 | 64 | Alternatively, to install the development version: 65 | 66 | ```shell 67 | pip install git+https://github.com/rossant/ipymd 68 | ``` 69 | 70 | 2. **Optional:** 71 | To interact with `.ipynb` files: 72 | 73 | ```shell 74 | pip install jupyter ipython 75 | ``` 76 | 77 | To interact with `.odt` files: 78 | 79 | ```shell 80 | pip install git+https://github.com/eea/odfpy 81 | ``` 82 | 83 | 3. Open your `jupyter_notebook_config.py`. Here's how to find it: 84 | 85 | 86 | ``` 87 | jupyter notebook --generate-config # generate a default config file 88 | jupyter --config-dir # find out the path to the config file 89 | ``` 90 | 91 | 4. Add the following in `jupyter_notebook_config.py`: 92 | 93 | ```python 94 | c.NotebookApp.contents_manager_class = 'ipymd.IPymdContentsManager' 95 | ``` 96 | 97 | 5. Now, you can open `.md` files in the Notebook. 98 | 99 | ## Why? 100 | 101 | ### IPython Notebook 102 | 103 | Pros: 104 | 105 | * Excellent UI for executing code interactively *and* writing text 106 | 107 | Cons: 108 | 109 | * `.ipynb` not git-friendly 110 | * Cannot easily edit in a text editor 111 | * Cannot easily edit on GitHub's web interface 112 | 113 | 114 | ### Markdown 115 | 116 | Pros: 117 | 118 | * Simple ASCII/Unicode format to write code and text 119 | * Can easily edit in a text editor 120 | * Can easily edit on GitHub's web interface 121 | * Git-friendly 122 | 123 | Cons: 124 | 125 | * No UI to execute code interactively 126 | 127 | 128 | ### ipymd 129 | 130 | All pros of IPython Notebook and Markdown, no cons! 131 | 132 | 133 | ## How it works 134 | 135 | * Write in Markdown in `document.md` 136 | * Either in a text editor (convenient when working on text) 137 | * Or in the Notebook (convenient when writing code examples) 138 | * Markdown cells, code cells and (optionally) notebook metadata are saved in 139 | the file 140 | * Collaborators can work on the Markdown document using GitHub's web interface. 141 | * By convention, a **notebook code cell** is equivalent to a **Markdown code block with explicit `python` syntax highlighting**: 142 | 143 | ``` 144 | >>> print("Hello world") 145 | Hello world 146 | ``` 147 | 148 | * **Notebook metadata** can be specified in [YAML](http://yaml.org/) inside 149 | Jekyll-style [front-matter](http://jekyllrb.com/docs/frontmatter/) dashes 150 | at the beginning of a document: 151 | 152 | ```markdown 153 | --- 154 | kernelspec: 155 | name: some-non-native-kernel 156 | --- 157 | 158 | First cell content 159 | ``` 160 | 161 | Native kernel metadata will be elided by default: non-python kernels haven't 162 | been tested yet, but support is planned. 163 | 164 | * **Cell metadata** is specified with YAML stream documents with dashes and 165 | periods, such as to create slides: 166 | 167 | ```markdown 168 | # Previous slide 169 | 170 | --- 171 | slideshow: 172 | slide_type: slide 173 | ... 174 | 175 | # Some Slide Content 176 | ``` 177 | 178 | > NOTE: You probably shouldn't use `---` to mean an `
`: `***` 179 | could be a suitable substitute. 180 | 181 | * Null metadata (i.e. splitting a markdown cell) can be created with just 182 | three dashes. This is useful when adding slideshow notes or skipped cells. 183 | 184 | ```markdown 185 | A cell 186 | 187 | --- 188 | 189 | Another cell 190 | ``` 191 | 192 | * The back-and-forth conversion is not strictly the identity function: 193 | * Extra line breaks in Markdown are discarded 194 | * Text output and standard output are combined into a single text output (stdout lines first, output lines last) 195 | 196 | 197 | ## Caveats 198 | 199 | **WARNING**: use this library at your own risks, backup your data, and version-control your notebooks and Markdown files! 200 | 201 | * Renaming doesn't work yet (issue #4) 202 | * New notebook doesn't work yet (issue #5) 203 | * Only nbformat v4 is supported currently (IPython 3.0) 204 | 205 | 206 | ## Formats 207 | 208 | ipymd uses a modular architecture that lets you define new formats. The following formats are currently implemented, and can be selected by modifying `~/.ipython/profile_/ipython_notebook_config.py`: 209 | 210 | * IPython notebook (`.ipynb`) 211 | * Markdown (`.md`) 212 | * `c.IPymdContentsManager.format = 'markdown'` 213 | * [O'Reilly Atlas](http://odewahn.github.io/publishing-workflows-for-jupyter/#1) (`.md` with special HTML tags for code and mathematical equations) 214 | * `c.IPymdContentsManager.format = 'atlas'` 215 | * Python (`.py`): code cells are delimited by double line breaks. Markdown cells = Python comments. [TODO: this doesn't work well, see #28 and #31] 216 | * Opendocument (`.odt`). You need to install the [development version of odfpy](https://github.com/eea/odfpy/). 217 | 218 | You can convert from any supported format to any supported format. This works by converting to an intermediate format that is basically a list of notebook cells. 219 | 220 | ### ipymd cells 221 | 222 | An **ipymd cell** is a Python dictionary with the following fields: 223 | 224 | * `cell_type`: `markdown`, `code` or `notebok_metadata` (if implemented) 225 | * `input`: a string with the code input (code cell only) 226 | * `output`: a string with the text output and stdout (code cell only) 227 | * `source`: a string containing Markdown markup (markdown cell only) 228 | * `metadata`: a dictionary containing cell (or notebook) metadata 229 | 230 | ### Kernel Metadata 231 | 232 | By default, notebook metadata for the native kernel (usually `python2` or 233 | `python3`) won't be written to markdown. Since ipymd doesn't yet support other 234 | kernels, this doesn't matter much, but if you would like to pick a non-native 235 | python kernel to be interpreted as the default for ipymd, and store 236 | `kernelspec` and `language_info` for the other, you can add this to your 237 | `ipython_notebook_config.py` file: 238 | * `c.IPymdContentsManager.default_kernel_name = 'python2'` 239 | 240 | Or, to always remember all notebook-level metadata: 241 | * `c.IPymdContentsManager.verbose_metadata = True` 242 | 243 | ### Customize the Markdown format 244 | 245 | You can customize the exact way the notebook is converted from/to Markdown by deriving from `BaseMarkdownReader` or `MarkdownReader` (idem with writers). Look at `ipymd/formats/markdown.py`. 246 | 247 | ### Implement your own format 248 | 249 | You can also implement your own format by following these instructions: 250 | 251 | * Create a `MyFormatReader` class that implements: 252 | * `self.read(contents)`: yields ipymd cells from a `contents` string 253 | * Create a `MyFormatWriter` class that implements: 254 | * `self.write(cell)`: append an ipymd cell 255 | * (optional) `self.write_notebook_metadata(cell)`: write the notebook 256 | metadata dictionary 257 | * `self.contents`: return the contents as a string 258 | 259 | * To activate this format, call this at Notebook launch time (not in a kernel!), perhaps in your `ipython_notebook_config.py`: 260 | 261 | ```python 262 | from ipymd import format_manager 263 | format_manager().register( 264 | name='my_format', 265 | reader=MyFormatReader, 266 | writer=MyFormatWriter, 267 | file_extension='.md', # or anything else 268 | file_type='text', # or JSON 269 | ) 270 | ``` 271 | 272 | * Now you can convert contents: `ipymd.convert(contents, from_='notebook', to='my_format')` or any other combination. 273 | 274 | ### Contributing a new ipymd format 275 | * To further integrate your format in ipymd, create a `ipymd/formats/my_format.py` file. 276 | * Put your reader and writer class in there, as well as a top-level variable: 277 | 278 | ```python 279 | MY_FORMAT = dict( 280 | reader=MyFormatReader, 281 | writer=MyFormatWriter, 282 | file_extension='.md', 283 | file_type='text', 284 | ) 285 | ``` 286 | 287 | * In `setup.py`, add this to `entry_points`: 288 | 289 | ```python 290 | ... 291 | entry_points={ 292 | 'ipymd.format': [ 293 | ... 294 | 'my_format=myformat:MY_FORMAT', 295 | ... 296 | ] 297 | } 298 | ``` 299 | 300 | > Note that the `entry_point` name will be used by default. you may override 301 | it, if you like, but Don't Repeat Yourself. 302 | 303 | * Add some unit tests in `ipymd/formats/tests`. 304 | * Propose a PR! 305 | 306 | Look at the existing format implementations for more details. 307 | 308 | 309 | ### Packaging a format 310 | * 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. 311 | * Someplace easy to import, e.g. `myformat.py` or `myformat/__init__.py`, add: 312 | 313 | ```python 314 | MY_FORMAT = dict( 315 | reader=MyFormatReader, 316 | writer=MyFormatWriter, 317 | file_extension='.md', # or anything else 318 | file_type='text', # or JSON 319 | ) 320 | ``` 321 | 322 | and this to your `setup.py`: 323 | 324 | ```python 325 | ... 326 | entry_points={ 327 | 'ipymd.format': [ 328 | 'my_format=myformat:MY_FORMAT', 329 | ], 330 | }, 331 | ... 332 | ``` 333 | 334 | * Publish on pypi! 335 | * Your users will now be able to `pip install myformat`, then configure their Notebook to use your format with the name `my_format`. 336 | -------------------------------------------------------------------------------- /examples/ex2.notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Test notebook" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "This is a text notebook. Here *are* some **rich text**, `code`, $\\pi\\simeq 3.1415$ equations." 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "Another equation:" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "$$\\sum_{i=1}^n x_i$$" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "Python code:" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 1, 41 | "metadata": { 42 | "collapsed": true 43 | }, 44 | "outputs": [], 45 | "source": [ 46 | "# some code in python\n", 47 | "def f(x):\n", 48 | " y = x * x\n", 49 | " return y" 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "metadata": {}, 55 | "source": [ 56 | "Random code:" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "```javascript\n", 64 | "console.log(\"hello\" + 3);\n", 65 | "```" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": {}, 71 | "source": [ 72 | "Python code:" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 2, 78 | "metadata": { 79 | "collapsed": false 80 | }, 81 | "outputs": [ 82 | { 83 | "name": "stdout", 84 | "output_type": "stream", 85 | "text": [ 86 | "Hello world!\n" 87 | ] 88 | } 89 | ], 90 | "source": [ 91 | "import IPython\n", 92 | "print(\"Hello world!\")" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": 3, 98 | "metadata": { 99 | "collapsed": false 100 | }, 101 | "outputs": [ 102 | { 103 | "data": { 104 | "text/plain": [ 105 | "4" 106 | ] 107 | }, 108 | "execution_count": 3, 109 | "metadata": {}, 110 | "output_type": "execute_result" 111 | } 112 | ], 113 | "source": [ 114 | "2*2" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 4, 120 | "metadata": { 121 | "collapsed": true 122 | }, 123 | "outputs": [], 124 | "source": [ 125 | "def decorator(f):\n", 126 | " return f" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 5, 132 | "metadata": { 133 | "collapsed": false 134 | }, 135 | "outputs": [ 136 | { 137 | "data": { 138 | "text/plain": [ 139 | "9" 140 | ] 141 | }, 142 | "execution_count": 5, 143 | "metadata": {}, 144 | "output_type": "execute_result" 145 | } 146 | ], 147 | "source": [ 148 | "@decorator\n", 149 | "def f(x):\n", 150 | " pass\n", 151 | "3*3" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "some text" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": 6, 164 | "metadata": { 165 | "collapsed": false 166 | }, 167 | "outputs": [ 168 | { 169 | "name": "stdout", 170 | "output_type": "stream", 171 | "text": [ 172 | "16\n" 173 | ] 174 | } 175 | ], 176 | "source": [ 177 | "print(4*4)" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": 7, 183 | "metadata": { 184 | "collapsed": false 185 | }, 186 | "outputs": [ 187 | { 188 | "name": "stdout", 189 | "output_type": "stream", 190 | "text": [ 191 | "hello\n" 192 | ] 193 | } 194 | ], 195 | "source": [ 196 | "%%bash\n", 197 | "echo 'hello'" 198 | ] 199 | }, 200 | { 201 | "cell_type": "markdown", 202 | "metadata": {}, 203 | "source": [ 204 | "An image:" 205 | ] 206 | }, 207 | { 208 | "cell_type": "markdown", 209 | "metadata": {}, 210 | "source": [ 211 | "![Hello world](http://wristgeek.com/wp-content/uploads/2014/09/hello_world.png)" 212 | ] 213 | }, 214 | { 215 | "cell_type": "markdown", 216 | "metadata": {}, 217 | "source": [ 218 | "### Subtitle" 219 | ] 220 | }, 221 | { 222 | "cell_type": "markdown", 223 | "metadata": {}, 224 | "source": [ 225 | "a list" 226 | ] 227 | }, 228 | { 229 | "cell_type": "markdown", 230 | "metadata": {}, 231 | "source": [ 232 | "* One [small](http://www.google.fr) link!\n", 233 | "* Two\n", 234 | " * 2.1\n", 235 | " * 2.2\n", 236 | "* Three" 237 | ] 238 | }, 239 | { 240 | "cell_type": "markdown", 241 | "metadata": {}, 242 | "source": [ 243 | "and" 244 | ] 245 | }, 246 | { 247 | "cell_type": "markdown", 248 | "metadata": {}, 249 | "source": [ 250 | "1. Un\n", 251 | "2. Deux" 252 | ] 253 | }, 254 | { 255 | "cell_type": "code", 256 | "execution_count": 8, 257 | "metadata": { 258 | "collapsed": true 259 | }, 260 | "outputs": [], 261 | "source": [ 262 | "import numpy as np\n", 263 | "import matplotlib.pyplot as plt\n", 264 | "%matplotlib inline" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": 9, 270 | "metadata": { 271 | "collapsed": false 272 | }, 273 | "outputs": [ 274 | { 275 | "data": { 276 | "image/png": [ 277 | "iVBORw0KGgoAAAANSUhEUgAAAPYAAAD7CAYAAABZjGkWAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\n", 278 | "AAALEgAACxIB0t1+/AAACzFJREFUeJzt3WuMXHUZx/Hfr7utLValULVAV4pLlQJV0IpUiVgTzEpU\n", 279 | "IlGgCaLGkGi84CWGRH1jjPjKaNQXGgUvkBQSENQ04AUqQoDGki60bIulCGyrrZK2QC1td7ePLzpr\n", 280 | "at3dmdM5Z87sw/eTbDKzc/LfJ5t8c+aW/3FECEAuM+oeAED5CBtIiLCBhAgbSIiwgYQIG0iot90F\n", 281 | "bPN5GVCjiPDRv2s7bElaMHPOVWWsc7Rdowc+dELvy24ve93vP9v/47LXlKRbvrVz5uVfe+1IFWt/\n", 282 | "adGLF1ex7s7nt338ta9c+POy1/3cimsuLXvNcTf/5ca3X/G2j64te92/HVjzTNlrStJd6x5678Cy\n", 283 | "839fwdJbf7T69l9N9ABPxYGECBtIqKvDnuUZm+qeoYizLnj5WN0zFPWy3tmDdc9Q1OLXvHFb3TMU\n", 284 | "ccr8V2/t9N/s6rDn9szcXPcMRZx94dxDdc9Q1PHHzZ92Yb/11PO21z1DEUsX9RM2gPYRNpAQYQMJ\n", 285 | "ETaQEGEDCRE2kBBhAwkRNpAQYQMJETaQEGEDCRE2kFDTsG0P2N5se4vtazsxFID2TBm27R5JP5Q0\n", 286 | "IOlMSSttL+nEYACOXbMz9nmSnoiIpyJiRNLNki6pfiwA7WgW9imSho+4v63xOwBdrNlmhi3tQLpr\n", 287 | "9MCHxm/P8oxN022DBGC62PDU1v7tz/6rX5L2jxzcNdlxzcLeLqnviPt9OnzW/h9V7CQK4P8tXdS/\n", 288 | "9YgdWbb+aPXtl090XLOn4uskLba9yPYsSZdL+k2JcwKowJRn7IgYtf1ZSb+T1CPp+oiYVhsMAi9F\n", 289 | "TS8YEBF3SrqzA7MAKAnfPAMSImwgIcIGEiJsICHCBhIibCAhwgYSImwgIcIGEiJsICHCBhIibCAh\n", 290 | "wgYSImwgIcIGEiJsICHCBhJquoNKK958qg+WsU6njFw69+66ZyjqBzfvvLHuGYr42HX3fLPuGYoa\n", 291 | "u+gLV9c9Q0F/1uqJ9xHljA0kRNhAQoQNJETYQEKEDSRE2EBChA0kRNhAQoQNJETYQEKEDSRE2EBC\n", 292 | "hA0kRNhAQoQNJETYQEKEDSTUNGzbN9jeaXtDJwYC0L5Wztg/kzRQ9SAAytM07Ii4T9LuDswCoCS8\n", 293 | "xgYSImwgoVK2H/7L9v0fHr89b07P0OknzBwqY10A/2t084OvGHvy4VdIUuzds3yy40oJ+22nzL61\n", 294 | "jHUATK33jOUv9J6x/IXG3Qf//egfJ4y7lY+7Vkl6QNIbbA/b/kSJcwKoQNMzdkSs7MQgAMrDm2dA\n", 295 | "QoQNJETYQEKEDSRE2EBChA0kRNhAQoQNJETYQEKEDSRE2EBChA0kRNhAQoQNJETYQEKEDSRE2EBC\n", 296 | "hA0k5IhobwE7zpj5gU+WNE9HvP59wy/WPUNRf3rgifV1z1DEihl73173DEU92rfkmbpnKOjZ4Yc3\n", 297 | "PRoRPvoBzthAQoQNJETYQEKEDSRE2EBChA0kRNhAQoQNJETYQEKEDSRE2EBChA0kRNhAQoQNJETY\n", 298 | "QEKEDSRE2EBCTcO23Wd7je3HbG+0/flODAbg2PW2cMyIpC9GxKDtuZIetv2HiNhU8WwAjlHTM3ZE\n", 299 | "7IiIwcbtvZI2STq56sEAHLtCr7FtL5J0rqS1VQwDoBytPBWXJDWeht8q6ZrGmfu/hkcfumT89nGe\n", 300 | "v/nEntMfL29EAON2D+9484G9L54jSTF2aN9kx7UUtu2Zkm6TdFNE3HH043295//6WAcF0Lp5fQse\n", 301 | "kfRI4+6zww9v+vREx7XyrrglXS9pKCK+V96IAKrSymvsd0q6UtIK2+sbPwMVzwWgDU2fikfE/eKL\n", 302 | "LMC0QrBAQoQNJETYQEKEDSRE2EBChA0kRNhAQoQNJETYQEKEDSRE2EBChA0kRNhAQoQNJETYQEKE\n", 303 | "DSRE2EBCLe9SOpU9V77+rDLW6ZSVSxZ/pO4Zirpz9YbT656hiDWvmrm77hmKWv6pb2ype4aC7hy+\n", 304 | "+rIJH+CMDSRE2EBChA0kRNhAQoQNJETYQEKEDSRE2EBChA0kRNhAQoQNJETYQEKEDSRE2EBChA0k\n", 305 | "RNhAQoQNJNQ0bNuzba+1PWh7yPa3OzEYgGPXdGukiNhve0VE7LPdK+l+2xdExP0dmA/AMWjpqXhE\n", 306 | "7GvcnCWpR9KuyiYC0LaWwrY9w/agpJ2S1kTEULVjAWhHq2fsQxFxjqSFkt5l+92VTgWgLYW2H46I\n", 307 | "52yvlrRM0p/Gf7/n3ruWj9+edfLrho9bfOa20iYE8F+7Nm3o2f34Y72SdPD55ybd9rtp2LbnSxqN\n", 308 | "iD2250i6SNI3jjzm+AsHHmxzXgAtOGHJ0rETliwda9x97O/33T1h3K2csU+S9AvbM3T4qfuNEXF3\n", 309 | "SXMCqEArH3dtkPSWDswCoCR88wxIiLCBhAgbSIiwgYQIG0iIsIGECBtIiLCBhAgbSIiwgYQIG0iI\n", 310 | "sIGECBtIiLCBhAgbSIiwgYQIG0iIsIGECu1SOpkvLbnHZazTKT/ZeNpddc9Q1HVf7n+k7hmK+Pp3\n", 311 | "/rq07hmKOvWZ626re4aCBiVdNtEDnLGBhAgbSIiwgYQIG0iIsIGECBtIiLCBhAgbSIiwgYQIG0iI\n", 312 | "sIGECBtIiLCBhAgbSIiwgYQIG0iIsIGEWgrbdo/t9bZ/W/VAANrX6hn7GklDkqLCWQCUpGnYthdK\n", 313 | "uljSTyVNq73NgJeqVs7Y35X0FUmHKp4FQEmm3KXU9vsl/TMi1tt+92THrVo1vHz89uLFc4eXLZu3\n", 314 | "rbwRAYzbunH3gr8/+cICSdq/b3TBZMc12374HZI+aPtiSbMlvdL2LyPiqiMPWrmy78F2BwbQXP/Z\n", 315 | "83b0nz1vR+Pu4A3fHByY6Lgpn4pHxFcjoi8iTpN0haR7jo4aQPcp+jk274oD00DLVwKJiHsl3Vvh\n", 316 | "LABKwjfPgIQIG0iIsIGECBtIiLCBhAgbSIiwgYQIG0iIsIGECBtIiLCBhAgbSIiwgYQIG0ioq8Ne\n", 317 | "t273wrpnKGLnk9tOqnuGoh5/8t/H1T1DUWNjurDuGYrYunH3pFsYVaWrw96yZW9f3TMUsecf/zq5\n", 318 | "7hmK2vr0vmkXtjS9wh7fo6yTujpsAMem5R1UmqhkV9IDB8aer2jtORWsqbGR0X5JT1extqQTq1j0\n", 319 | "wMFDSyRtqmDpKrfRiorWf6qCNTVycGxRRWv/c7IHHNHe/8c2+6ABNYqI/7uQR9thA+g+vMYGEiJs\n", 320 | "IKGuDNv2gO3NtrfYvrbueZqxfYPtnbY31D1Lq2z32V5j+zHbG21/vu6ZpmJ7tu21tgdtD9n+dt0z\n", 321 | "taqOy1B3Xdi2eyT9UNKApDMlrbS9pN6pmvqZDs87nYxI+mJEnCXpfEmf6eb/c0Tsl7QiIs6R9CZJ\n", 322 | "K2xfUPNYrer4Zai7LmxJ50l6IiKeiogRSTdLuqTmmaYUEfdJ2l33HEVExI6IGGzc3qvDH3l19Rds\n", 323 | "ImJf4+YsST2SdtU4Tkvqugx1N4Z9iqThI+5va/wOFbG9SNK5ktbWO8nUbM+wPShpp6Q1ETFU90wt\n", 324 | "qOUy1N0YNp+/dZDtuZJulXRN48zdtSLiUOOp+EJJ75rq0s7d4MjLUKuDZ2upO8PeLunI74j3qaJv\n", 325 | "tr3U2Z4p6TZJN0XEHXXP06qIeE7SaknL6p6lifHLUP9N0ipJ77H9y0784W4Me52kxbYX2Z4l6XJJ\n", 326 | "v6l5pnRsW9L1koYi4nt1z9OM7fm2j2/cniPpIknr651qanVehrrrwo6IUUmflfQ7HX4n8ZaIqOK7\n", 327 | "zKWxvUrSA5LeYHvY9ifqnqkF75R0pQ6/u7y+8dPN7+yfJOmexmvstZJ+GxF31zxTUR17mclXSoGE\n", 328 | "uu6MDaB9hA0kRNhAQoQNJETYQEKEDSRE2EBChA0k9B9wzmxMrDNitQAAAABJRU5ErkJggg==\n" 329 | ], 330 | "text/plain": [ 331 | "" 332 | ] 333 | }, 334 | "metadata": {}, 335 | "output_type": "display_data" 336 | } 337 | ], 338 | "source": [ 339 | "plt.imshow(np.random.rand(5,5,4), interpolation='none');" 340 | ] 341 | }, 342 | { 343 | "cell_type": "markdown", 344 | "metadata": {}, 345 | "source": [ 346 | "> TIP (a block quote): That's all folks.\n", 347 | "> Last line." 348 | ] 349 | }, 350 | { 351 | "cell_type": "markdown", 352 | "metadata": {}, 353 | "source": [ 354 | "Last paragraph." 355 | ] 356 | } 357 | ], 358 | "metadata": { 359 | "kernelspec": { 360 | "display_name": "Python 3", 361 | "language": "python", 362 | "name": "python3" 363 | }, 364 | "language_info": { 365 | "codemirror_mode": { 366 | "name": "ipython", 367 | "version": 3 368 | }, 369 | "file_extension": ".py", 370 | "mimetype": "text/x-python", 371 | "name": "python", 372 | "nbconvert_exporter": "python", 373 | "pygments_lexer": "ipython3", 374 | "version": "3.4.2" 375 | } 376 | }, 377 | "nbformat": 4, 378 | "nbformat_minor": 0 379 | } 380 | -------------------------------------------------------------------------------- /ipymd/core/format_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Core conversion functions.""" 4 | 5 | #------------------------------------------------------------------------------ 6 | # Imports 7 | #------------------------------------------------------------------------------ 8 | 9 | import argparse 10 | import re 11 | import os 12 | import os.path as op 13 | import glob 14 | import json 15 | 16 | from pkg_resources import iter_entry_points, DistributionNotFound 17 | 18 | try: 19 | from traitlets import Unicode, Bool 20 | from traitlets.config import LoggingConfigurable 21 | except ImportError: 22 | from IPython.utils.traitlets import Unicode, Bool 23 | from IPython.config.configurable import LoggingConfigurable 24 | 25 | try: 26 | from jupyter_client import KernelManager 27 | except ImportError: 28 | from IPython.kernel import KernelManager 29 | 30 | from ..ext.six import string_types 31 | from ..utils.utils import _read_text, _read_json, _write_text, _write_json 32 | 33 | 34 | #------------------------------------------------------------------------------ 35 | # Format manager 36 | #------------------------------------------------------------------------------ 37 | 38 | def _is_path(s): 39 | """Return whether an object is a path.""" 40 | if isinstance(s, string_types): 41 | try: 42 | return op.exists(s) 43 | except (OSError, ValueError): 44 | return False 45 | else: 46 | return False 47 | 48 | 49 | DEFAULT_CELL_METADATA = {"deletable": True, "editable": True} 50 | 51 | 52 | class FormatManager(LoggingConfigurable): 53 | # The name of the setup_tools entry point group to use in setup.py 54 | entry_point_group = "ipymd.format" 55 | 56 | # The name of the default kernel: if left blank, assume native (pythonX), 57 | # won't store kernelspec/language_info unless forced 58 | # TODO: where does this get set but by the ContentsManager? 59 | default_kernel_name = Unicode(config=True) 60 | 61 | # Don't strip any metadata 62 | # TODO: where does this get set but by the ContentsManager? 63 | verbose_metadata = Bool(False, config=True) 64 | 65 | # The singleton. There can be only one. 66 | _instance = None 67 | 68 | def __init__(self, *args, **kwargs): 69 | super(FormatManager, self).__init__(*args, **kwargs) 70 | 71 | if self._instance is not None: 72 | raise ValueError("FormatManager is a singleton, access with" 73 | " FormatManager.format_manager") 74 | 75 | self._formats = {} 76 | self._km = KernelManager() 77 | 78 | @classmethod 79 | def format_manager(cls): 80 | """Return the instance singleton, creating if necessary 81 | """ 82 | if cls._instance is None: 83 | # Discover the formats and register them with a new singleton. 84 | cls._instance = cls().register_entrypoints() 85 | return cls._instance 86 | 87 | def register_entrypoints(self): 88 | """Look through the `setup_tools` `entry_points` and load all of 89 | the formats. 90 | """ 91 | for spec in iter_entry_points(self.entry_point_group): 92 | format_properties = {"name": spec.name} 93 | try: 94 | format_properties.update(spec.load()) 95 | except (DistributionNotFound, ImportError) as err: 96 | self.log.info( 97 | "ipymd format {} could not be loaded: {}".format( 98 | spec.name, err)) 99 | continue 100 | 101 | self.register(**format_properties) 102 | 103 | return self 104 | 105 | def register(self, name=None, **kwargs): 106 | """Register a format. 107 | 108 | Parameters 109 | ---------- 110 | 111 | reader : class 112 | A class that implements read(contents) which yield ipymd cells. 113 | writer : class 114 | A class that implements write(cell) and contents. 115 | file_extension : str 116 | The file extension with the leading dot, like '.md' 117 | file_type : 'text' or 'json' 118 | The type of the file format. 119 | load : function 120 | a custom `contents = load(path)` function if no file type 121 | is specified. 122 | save : function 123 | a custom `save(path, contents)` function if no file type 124 | is specified. 125 | 126 | """ 127 | assert name is not None 128 | self._formats[name] = kwargs 129 | 130 | def unregister(self, name): 131 | """Unregister a format.""" 132 | del self._formats[name] 133 | 134 | @property 135 | def formats(self): 136 | """Return the sorted list of registered formats.""" 137 | return sorted(self._formats) 138 | 139 | def _check_format(self, name): 140 | if name not in self._formats: 141 | raise ValueError("This format '{0:s}' has not ".format(name) + 142 | "been registered.") 143 | 144 | def file_extension(self, name): 145 | """Return the file extension of a registered format.""" 146 | return self._formats[name]['file_extension'] 147 | 148 | def format_from_extension(self, extension): 149 | """Find a format from its extension.""" 150 | formats = [name 151 | for name, format in self._formats.items() 152 | if format.get('file_extension', None) == extension] 153 | if len(formats) == 0: 154 | return None 155 | elif len(formats) == 2: 156 | raise RuntimeError("Several extensions are registered with " 157 | "that extension; please specify the format " 158 | "explicitly.") 159 | else: 160 | return formats[0] 161 | 162 | def file_type(self, name): 163 | """Return the file type of a registered format.""" 164 | return self._formats[name].get('file_type', None) 165 | 166 | def load(self, file, name=None): 167 | """Load a file. The format name can be specified explicitly or 168 | inferred from the file extension.""" 169 | if name is None: 170 | name = self.format_from_extension(op.splitext(file)[1]) 171 | file_format = self.file_type(name) 172 | if file_format == 'text': 173 | return _read_text(file) 174 | elif file_format == 'json': 175 | return _read_json(file) 176 | else: 177 | load_function = self._formats[name].get('load', None) 178 | if load_function is None: 179 | raise IOError("The format must declare a file type or " 180 | "load/save functions.") 181 | return load_function(file) 182 | 183 | def save(self, file, contents, name=None, overwrite=False): 184 | """Save contents into a file. The format name can be specified 185 | explicitly or inferred from the file extension.""" 186 | if name is None: 187 | name = self.format_from_extension(op.splitext(file)[1]) 188 | file_format = self.file_type(name) 189 | if file_format == 'text': 190 | _write_text(file, contents) 191 | elif file_format == 'json': 192 | _write_json(file, contents) 193 | else: 194 | write_function = self._formats[name].get('save', None) 195 | if write_function is None: 196 | raise IOError("The format must declare a file type or " 197 | "load/save functions.") 198 | if op.exists(file) and not overwrite: 199 | print("The file already exists, please use overwrite=True.") 200 | return 201 | write_function(file, contents) 202 | 203 | def create_reader(self, name, *args, **kwargs): 204 | """Create a new reader instance for a given format.""" 205 | self._check_format(name) 206 | return self._formats[name]['reader'](*args, **kwargs) 207 | 208 | def create_writer(self, name, *args, **kwargs): 209 | """Create a new writer instance for a given format.""" 210 | self._check_format(name) 211 | return self._formats[name]['writer'](*args, **kwargs) 212 | 213 | def convert(self, 214 | contents_or_path, 215 | from_=None, 216 | to=None, 217 | reader=None, 218 | writer=None, 219 | from_kwargs=None, 220 | to_kwargs=None, 221 | ): 222 | """Convert contents between supported formats. 223 | 224 | Parameters 225 | ---------- 226 | 227 | contents : str 228 | The contents to convert from. 229 | from_ : str or None 230 | The name of the source format. If None, this is the 231 | ipymd_cells format. 232 | to : str or None 233 | The name of the target format. If None, this is the 234 | ipymd_cells format. 235 | reader : a Reader instance or None 236 | writer : a Writer instance or None 237 | from_kwargs : dict 238 | Optional keyword arguments to pass to the reader instance. 239 | to_kwargs : dict 240 | Optional keyword arguments to pass to the writer instance. 241 | 242 | """ 243 | 244 | # Load the file if 'contents_or_path' is a path. 245 | if _is_path(contents_or_path): 246 | contents = self.load(contents_or_path, from_) 247 | else: 248 | contents = contents_or_path 249 | 250 | if from_kwargs is None: 251 | from_kwargs = {} 252 | if to_kwargs is None: 253 | to_kwargs = {} 254 | 255 | if reader is None: 256 | reader = (self.create_reader(from_, **from_kwargs) 257 | if from_ is not None else None) 258 | 259 | if writer is None: 260 | writer = (self.create_writer(to, **to_kwargs) 261 | if to is not None else None) 262 | 263 | if reader is not None: 264 | # Convert from the source format to ipymd cells. 265 | cells = [cell for cell in reader.read(contents)] 266 | else: 267 | # If no reader is specified, 'contents' is assumed to already be 268 | # a list of ipymd cells. 269 | cells = contents 270 | 271 | notebook_metadata = [cell for cell in cells 272 | if cell["cell_type"] == "notebook_metadata"] 273 | 274 | if writer is not None: 275 | if notebook_metadata: 276 | [cells.remove(cell) for cell in notebook_metadata] 277 | notebook_metadata = self.clean_meta( 278 | notebook_metadata[0]["metadata"] 279 | ) 280 | if hasattr(writer, "write_notebook_metadata"): 281 | writer.write_notebook_metadata(notebook_metadata) 282 | else: 283 | print("{} does not support notebook metadata, " 284 | "dropping metadata: {}".format( 285 | writer, 286 | notebook_metadata)) 287 | 288 | # Convert from ipymd cells to the target format. 289 | for cell in cells: 290 | meta = self.clean_cell_meta(cell.get("metadata", {})) 291 | if not meta: 292 | cell.pop("metadata", None) 293 | writer.write(cell) 294 | 295 | return writer.contents 296 | else: 297 | # If no writer is specified, the output is supposed to be 298 | # a list of ipymd cells. 299 | return cells 300 | 301 | def clean_meta(self, meta): 302 | """Removes unwanted metadata 303 | 304 | Parameters 305 | ---------- 306 | 307 | meta : dict 308 | Notebook metadata. 309 | """ 310 | if not self.verbose_metadata: 311 | default_kernel_name = (self.default_kernel_name or 312 | self._km.kernel_name) 313 | 314 | if (meta.get("kernelspec", {}) 315 | .get("name", None) == default_kernel_name): 316 | del meta["kernelspec"] 317 | meta.pop("language_info", None) 318 | 319 | return meta 320 | 321 | def clean_cell_meta(self, meta): 322 | """Remove cell metadata that matches the default cell metadata.""" 323 | for k, v in DEFAULT_CELL_METADATA.items(): 324 | if meta.get(k, None) == v: 325 | meta.pop(k, None) 326 | return meta 327 | 328 | 329 | def format_manager(): 330 | """Return a FormatManager singleton instance.""" 331 | return FormatManager.format_manager() 332 | 333 | 334 | def convert(*args, **kwargs): 335 | """Alias for format_manager().convert().""" 336 | return format_manager().convert(*args, **kwargs) 337 | -------------------------------------------------------------------------------- /ipymd/lib/tests/test_markdown.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test Markdown lexers.""" 4 | 5 | # ----------------------------------------------------------------------------- 6 | # Imports 7 | # ----------------------------------------------------------------------------- 8 | 9 | import re 10 | from pprint import pprint 11 | 12 | from ..base_lexer import BaseRenderer 13 | from ..markdown import BlockLexer, InlineLexer, MarkdownWriter 14 | from ...utils.utils import _show_outputs 15 | 16 | 17 | # ----------------------------------------------------------------------------- 18 | # Test renderers 19 | # ----------------------------------------------------------------------------- 20 | 21 | class BlockRenderer(BaseRenderer): 22 | def __init__(self): 23 | super(BlockRenderer, self).__init__() 24 | self.output = [] 25 | 26 | def paragraph(self, text): 27 | self.output.append('

') 28 | self.text(text) 29 | self.output.append('

') 30 | 31 | def list_start(self, ordered=None): 32 | self._ordered = ordered 33 | if self._ordered: 34 | self.output.append('
    ') 35 | else: 36 | self.output.append('
      ') 37 | 38 | def list_end(self): 39 | if self._ordered: 40 | self.output.append('
') 41 | else: 42 | self.output.append('') 43 | self._ordered = None 44 | 45 | def list_item_start(self): 46 | self.output.append('
  • ') 47 | 48 | def list_item_end(self): 49 | self.output.append('
  • ') 50 | 51 | def newline(self): 52 | self.output.append('\n') 53 | 54 | def text(self, text): 55 | self.output.append(text) 56 | 57 | def block_code(self, text, lang=None): 58 | self.output.append('') 59 | self.output.append(text) 60 | self.output.append('') 61 | 62 | def block_quote_start(self): 63 | self.output.append('') 64 | 65 | def block_quote_end(self): 66 | self.output.append('') 67 | 68 | 69 | class InlineRenderer(BaseRenderer): 70 | def __init__(self, output=None): 71 | super(InlineRenderer, self).__init__() 72 | if output is None: 73 | output = [] 74 | self.output = output 75 | 76 | def text(self, text): 77 | self.output.append(text) 78 | 79 | def emphasis(self, text): 80 | self.output.append('') 81 | self.text(text) 82 | self.output.append('') 83 | 84 | def double_emphasis(self, text): 85 | self.output.append('') 86 | self.text(text) 87 | self.output.append('') 88 | 89 | def codespan(self, text): 90 | self.output.append('') 91 | self.text(text) 92 | self.output.append('') 93 | 94 | def linebreak(self): 95 | self.output.append('
    ') 96 | 97 | def link(self, link, title, text): 98 | self.output.append('') 99 | self.text(text) 100 | self.output('') 101 | 102 | 103 | class FullBlockRenderer(BlockRenderer): 104 | def text(self, text): 105 | inline_renderer = InlineRenderer(self.output) 106 | inline_lexer = InlineLexer(renderer=inline_renderer) 107 | inline_lexer.read(text) 108 | 109 | 110 | # ----------------------------------------------------------------------------- 111 | # Tests Markdown block lexer 112 | # ----------------------------------------------------------------------------- 113 | 114 | _TEST_TEXT = ("First *paragraph*.\n**Second** line.\n\n" 115 | "* Item 1.\n* Item 2.\n\n```\ncode\n```\n\n" 116 | "1. First.\n2. Second.\n\n" 117 | "> End.\n") 118 | 119 | 120 | def test_block_lexer(): 121 | renderer = BlockRenderer() 122 | text = _TEST_TEXT 123 | lexer = BlockLexer(renderer=renderer) 124 | lexer.read(text) 125 | expected = ['

    ', 'First *paragraph*.\n**Second** line.', '

    ', 126 | 127 | '
      ', 128 | '
    • ', 'Item 1.', '
    • ', 129 | '
    • ', 'Item 2.', '
    • ', 130 | '
    ', 131 | 132 | '', 'code', '', 133 | 134 | '
      ', 135 | '
    1. ', 'First.', '
    2. ', 136 | '
    3. ', 'Second.', '
    4. ', 137 | '
    ', 138 | 139 | '', '

    ', 'End.', '

    ', '
    ' 140 | ] 141 | assert renderer.output == expected 142 | 143 | 144 | def test_block_lexer_list(): 145 | renderer = BlockRenderer() 146 | text = "* 1\n* 2\n * 2.1\n* 3" 147 | lexer = BlockLexer(renderer=renderer) 148 | lexer.read(text) 149 | expected = ['
      ', 150 | '
    • ', '1', '
    • ', 151 | '
    • ', '2', 152 | '
        ', 153 | '
      • ', '2.1', '
      • ', 154 | '
      ', 155 | '
    • ', 156 | '
    • ', '3', '
    • ', 157 | '
    ', 158 | ] 159 | assert renderer.output == expected 160 | 161 | 162 | def test_meta_split(): 163 | renderer = BlockRenderer() 164 | text = "---" 165 | lexer = BlockLexer(renderer=renderer) 166 | lexer.read(text) 167 | expected = ['META SPLIT'] 168 | assert renderer.output == expected 169 | 170 | 171 | def test_meta_single_alias(): 172 | renderer = BlockRenderer() 173 | text = "--- !FOO bar baz" 174 | lexer = BlockLexer(renderer=renderer) 175 | lexer.read(text) 176 | expected = ['META SINGLE ALIAS'] 177 | assert renderer.output == expected 178 | 179 | 180 | def test_meta_explicit(): 181 | renderer = BlockRenderer() 182 | text = ( 183 | "---\n" 184 | "foo: bar\n" 185 | "baz: boo\n" 186 | "..." 187 | ) 188 | lexer = BlockLexer(renderer=renderer) 189 | lexer.read(text) 190 | expected = ['META EXPLICIT'] 191 | assert renderer.output == expected 192 | 193 | 194 | def test_meta_stack(): 195 | renderer = BlockRenderer() 196 | text = ( 197 | "---\n" 198 | "- !FOO\n" 199 | "- foo: bar\n" 200 | "..." 201 | ) 202 | lexer = BlockLexer(renderer=renderer) 203 | lexer.read(text) 204 | expected = ['META EXPLICIT'] 205 | assert renderer.output == expected 206 | 207 | 208 | def test_meta_frontmatter(): 209 | renderer = BlockRenderer() 210 | text = ( 211 | "---\n" 212 | "- !FOO\n" 213 | "- foo: bar\n" 214 | "---" 215 | ) 216 | lexer = BlockLexer(renderer=renderer) 217 | lexer.read(text) 218 | expected = ['META EXPLICIT'] 219 | assert renderer.output == expected 220 | 221 | 222 | def test_meta_frontmatter_then_split(): 223 | renderer = BlockRenderer() 224 | text = ( 225 | "---\n" 226 | "- !FOO\n" 227 | "- foo: bar\n" 228 | "---\n\n" 229 | "---" 230 | ) 231 | lexer = BlockLexer(renderer=renderer) 232 | lexer.read(text) 233 | expected = ['META EXPLICIT', 'META SPLIT'] 234 | assert renderer.output == expected 235 | 236 | 237 | def test_meta_frontmatter_then_alias(): 238 | renderer = BlockRenderer() 239 | text = ( 240 | "--- !KERNEL python\n\n" 241 | "--- !SLIDE" 242 | ) 243 | lexer = BlockLexer(renderer=renderer) 244 | lexer.read(text) 245 | expected = ['META SINGLE ALIAS', 'META SINGLE ALIAS'] 246 | assert renderer.output == expected 247 | 248 | 249 | def test_meta_exp_frontmatter_then_alias(): 250 | renderer = BlockRenderer() 251 | text = ( 252 | "---\n" 253 | "foo: bar\n" 254 | "---\n\n" 255 | "--- !SLIDE" 256 | ) 257 | lexer = BlockLexer(renderer=renderer) 258 | lexer.read(text) 259 | expected = ['META EXPLICIT', 'META SINGLE ALIAS'] 260 | assert renderer.output == expected 261 | 262 | 263 | def test_meta_split_md_split_not_meta(): 264 | renderer = BlockRenderer() 265 | text = ( 266 | "---\n\n" 267 | "* Just some markdown" 268 | "\n\n---" 269 | ) 270 | lexer = BlockLexer(renderer=renderer) 271 | lexer.read(text) 272 | expected = ['META SPLIT', 273 | '
      ', '
    • ', 274 | 'Just some markdown', 275 | '
    • ', '
    ', 276 | 'META SPLIT'] 277 | assert renderer.output == expected 278 | 279 | 280 | # ----------------------------------------------------------------------------- 281 | # Tests Markdown inline lexer 282 | # ----------------------------------------------------------------------------- 283 | 284 | 285 | def test_inline_lexer(): 286 | renderer = InlineRenderer() 287 | text = ("First *paragraph*.\n**Second** line.") 288 | lexer = InlineLexer(renderer=renderer) 289 | lexer.read(text) 290 | expected = ['First ', 291 | '', 'paragraph', '', 292 | '.', 293 | '
    ', 294 | '', 'Second', '', 295 | ' line.' 296 | ] 297 | assert renderer.output == expected 298 | 299 | 300 | def test_brackets(): 301 | renderer = InlineRenderer() 302 | text = ("Some [1] reference.") 303 | lexer = InlineLexer(renderer=renderer) 304 | lexer.read(text) 305 | expected = ['Some ', 306 | '[1] reference.', 307 | ] 308 | assert renderer.output == expected 309 | 310 | 311 | # ----------------------------------------------------------------------------- 312 | # Tests full Markdown lexer 313 | # ----------------------------------------------------------------------------- 314 | 315 | def test_full_lexer(): 316 | renderer = FullBlockRenderer() 317 | lexer = BlockLexer(renderer=renderer) 318 | text = _TEST_TEXT 319 | lexer.read(text) 320 | expected = ['

    ', 321 | 'First ', '', 'paragraph', '', '.', 322 | '
    ', 323 | '', 'Second', '', ' line.', 324 | '

    ', 325 | 326 | '
      ', 327 | '
    • ', 'Item 1.', '
    • ', 328 | '
    • ', 'Item 2.', '
    • ', 329 | '
    ', 330 | 331 | '', 'code', '', 332 | 333 | '
      ', 334 | '
    1. ', 'First.', '
    2. ', 335 | '
    3. ', 'Second.', '
    4. ', 336 | '
    ', 337 | 338 | '', '

    ', 'End.', '

    ', '
    ' 339 | ] 340 | assert renderer.output == expected 341 | 342 | 343 | # ----------------------------------------------------------------------------- 344 | # Test Markdown writer 345 | # ----------------------------------------------------------------------------- 346 | 347 | def test_markdown_writer_newline(): 348 | w = MarkdownWriter() 349 | w.text('Hello.') 350 | w.ensure_newline(1) 351 | w.text('Hello.\n') 352 | w.ensure_newline(1) 353 | w.text('Hello.\n\n') 354 | w.ensure_newline(1) 355 | w.text('Hello.\n\n\n') 356 | w.ensure_newline(2) 357 | w.text('End') 358 | 359 | expected = ('Hello.\n' * 4) + '\nEnd\n' 360 | 361 | assert w.contents == expected 362 | 363 | 364 | def test_markdown_writer(): 365 | w = MarkdownWriter() 366 | 367 | expected = '\n'.join(("# First chapter", 368 | "", 369 | "**Hello** *world*!", 370 | "How are you? Some `code`.", 371 | "", 372 | "> Good, and you?", 373 | "> End of citation.", 374 | "", 375 | "* Item **1**.", 376 | "* Item 2.", 377 | "", 378 | "1. 1", 379 | " * 1.1", 380 | " * 1.1.1", 381 | "2. 2", 382 | "", 383 | "```", 384 | "print(\"Hello world!\")", 385 | "```", 386 | "", 387 | ("Go to [google](http://www.google.com). " 388 | "And here is an image for you:"), 389 | "", 390 | "![Some image](my_image.png)\n")) 391 | 392 | w.heading('First chapter', 1) 393 | w.newline() 394 | 395 | w.bold('Hello') 396 | w.text(' ') 397 | w.italic('world') 398 | w.text('!') 399 | w.linebreak() 400 | w.text('How are you? Some ') 401 | w.inline_code('code') 402 | w.text('.') 403 | w.newline() 404 | 405 | w.quote_start() 406 | w.text('Good, and you?') 407 | w.linebreak() 408 | w.text('End of citation.') 409 | w.quote_end() 410 | w.newline() 411 | 412 | w.list_item('Item ') 413 | w.bold('1') 414 | w.text('.') 415 | w.linebreak() 416 | w.list_item('Item 2.') 417 | w.newline() 418 | 419 | w.numbered_list_item('1') 420 | w.linebreak() 421 | w.list_item('1.1', level=1) 422 | w.linebreak() 423 | w.list_item('1.1.1', level=2) 424 | w.linebreak() 425 | w.numbered_list_item('2') 426 | w.newline() 427 | 428 | w.code_start() 429 | w.text('print("Hello world!")') 430 | w.code_end() 431 | w.newline() 432 | 433 | w.text('Go to ') 434 | w.link('google', 'http://www.google.com') 435 | w.text('. And here is an image for you:') 436 | w.newline() 437 | 438 | w.image('Some image', 'my_image.png') 439 | 440 | _show_outputs(w.contents, expected) 441 | assert w.contents == expected 442 | -------------------------------------------------------------------------------- /ipymd/lib/markdown.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Markdown lexers. 4 | 5 | The code has been adapted from the mistune library: 6 | 7 | mistune 8 | https://github.com/lepture/mistune/ 9 | 10 | The fastest markdown parser in pure Python with renderer feature. 11 | :copyright: (c) 2014 - 2015 by Hsiaoming Yang. 12 | 13 | """ 14 | 15 | 16 | # ----------------------------------------------------------------------------- 17 | # Imports 18 | # ----------------------------------------------------------------------------- 19 | 20 | import re 21 | 22 | from .base_lexer import BaseLexer, BaseRenderer 23 | from ..ext.six import StringIO, string_types 24 | 25 | 26 | # ----------------------------------------------------------------------------- 27 | # Block lexer 28 | # ----------------------------------------------------------------------------- 29 | 30 | def _pure_pattern(regex): 31 | pattern = regex.pattern 32 | if pattern.startswith('^'): 33 | pattern = pattern[1:] 34 | return pattern 35 | 36 | 37 | _key_pattern = re.compile(r'\s+') 38 | 39 | 40 | def _keyify(key): 41 | return _key_pattern.sub(' ', key.lower()) 42 | 43 | 44 | _tag = ( 45 | r'(?!(?:' 46 | r'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|' 47 | r'var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|' 48 | r'span|br|wbr|ins|del|img)\b)\w+(?!:/|[^\w\s@]*@)\b' 49 | ) 50 | 51 | 52 | class BlockGrammar(object): 53 | """Grammars for block level tokens.""" 54 | 55 | def_links = re.compile( 56 | r'^ *\[([^^\]]+)\]: *' # [key]: 57 | r']+)>?' # or link 58 | r'(?: +["(]([^\n]+)[")])? *(?:\n+|$)' 59 | # r'(?:["(]([^\n]+)[")])? *(?:\n+|$)' 60 | ) 61 | def_footnotes = re.compile( 62 | r'^\[\^([^\]]+)\]: *(' 63 | r'[^\n]*(?:\n+|$)' # [^key]: 64 | r'(?: {1,}[^\n]*(?:\n+|$))*' 65 | r')' 66 | ) 67 | 68 | newline = re.compile(r'^\n+') 69 | block_code = re.compile(r'^( {4}[^\n]+\n*)+') 70 | fences = re.compile( 71 | r'^ *(`{3,}|~{3,}) *(\S+)? *\n' # ```lang 72 | r'([\s\S]+?)\s*' 73 | r'\1 *(?:\n+|$)' # ``` 74 | ) 75 | hrule = re.compile(r'^ {0,3}[-*_](?: *[-*_]){2,} *(?:\n+|$)') 76 | heading = re.compile(r'^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)') 77 | lheading = re.compile(r'^([^\n]+)\n *(=|-)+ *(?:\n+|$)') 78 | block_quote = re.compile(r'^( *>[^\n]+(\n[^\n]+)*\n*)+') 79 | list_block = re.compile( 80 | r'^( *)([*+-]|\d+\.) [\s\S]+?' 81 | r'(?:' 82 | r'\n+(?=\1?(?:[-*_] *){3,}(?:\n+|$))' # hrule 83 | r'|\n+(?=%s)' # def links 84 | r'|\n+(?=%s)' # def footnotes 85 | r'|\n{2,}' 86 | r'(?! )' 87 | r'(?!\1(?:[*+-]|\d+\.) )\n*' 88 | r'|' 89 | r'\s*$)' % ( 90 | _pure_pattern(def_links), 91 | _pure_pattern(def_footnotes), 92 | ) 93 | ) 94 | list_item = re.compile( 95 | r'^(( *)(?:[*+-]|\d+\.) [^\n]*' 96 | r'(?:\n(?!\2(?:[*+-]|\d+\.) )[^\n]*)*)', 97 | flags=re.M 98 | ) 99 | list_bullet = re.compile(r'^ *(?:[*+-]|\d+\.) +') 100 | # Paragraph = Text not immediately followed by another non-text block. 101 | paragraph = re.compile( 102 | r'^((?:[^\n]+\n?(?!' 103 | r'%s|%s|%s|%s|%s|%s|%s|%s|%s' 104 | r'))+)\n*' % ( 105 | _pure_pattern(fences).replace(r'\1', r'\2'), 106 | _pure_pattern(list_block).replace(r'\1', r'\3'), 107 | _pure_pattern(hrule), 108 | _pure_pattern(heading), 109 | _pure_pattern(lheading), 110 | _pure_pattern(block_quote), 111 | _pure_pattern(def_links), 112 | _pure_pattern(def_footnotes), 113 | '<' + _tag, 114 | ) 115 | ) 116 | block_html = re.compile( 117 | r'^ *(?:%s|%s|%s) *(?:\n{2,}|\s*$)' % ( 118 | r'', 119 | r'<(%s)[\s\S]+?<\/\1>' % _tag, 120 | r'''<%s(?:"[^"]*"|'[^']*'|[^'">])*?>''' % _tag, 121 | ) 122 | ) 123 | table = re.compile( 124 | r'^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*' 125 | ) 126 | nptable = re.compile( 127 | r'^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*' 128 | ) 129 | text = re.compile(r'^[^\n]+') 130 | meta = re.compile( 131 | r'''(^|\n\n) 132 | (?P---)[ ]* # open YAML stream 133 | (?P.+)? # cell split, or !tag 134 | (\n 135 | (?P(.+\n)+) # optional YAML 136 | (?P(\.{3}|-{3})) # close YAML stream 137 | )?(?:\n{2}|$)''', re.X) 138 | 139 | 140 | class BlockLexer(BaseLexer): 141 | """Block level lexer for block grammars.""" 142 | grammar_class = BlockGrammar 143 | 144 | default_rules = [ 145 | 'newline', 'block_code', 'fences', 'meta', 'heading', 146 | 'nptable', 'lheading', 'block_quote', 147 | 'list_block', 'block_html', 'def_links', 148 | 'def_footnotes', 'table', 'paragraph', 'text', 149 | ] 150 | 151 | list_rules = ( 152 | 'newline', 'block_code', 'fences', 'meta', 'lheading', 153 | 'block_quote', 'list_block', 'block_html', 'text', 154 | ) 155 | 156 | footnote_rules = ( 157 | 'newline', 'block_code', 'fences', 'heading', 158 | 'nptable', 'lheading', 'hrule', 'block_quote', 159 | 'list_block', 'block_html', 'table', 'paragraph', 'text' 160 | ) 161 | 162 | def __init__(self, **kwargs): 163 | super(BlockLexer, self).__init__(**kwargs) 164 | self.def_links = {} 165 | self.def_footnotes = {} 166 | 167 | def parse_newline(self, m): 168 | length = len(m.group(0)) 169 | if length > 1: 170 | self.renderer.newline() 171 | 172 | def parse_block_code(self, m): 173 | code = m.group(0) 174 | pattern = re.compile(r'^ {4}', re.M) 175 | code = pattern.sub('', code) 176 | self.renderer.block_code(code, lang=None) 177 | 178 | def parse_fences(self, m): 179 | self.renderer.block_code(m.group(3), lang=m.group(2)) 180 | 181 | def parse_heading(self, m): 182 | self.renderer.heading(m.group(2), level=len(m.group(1))) 183 | 184 | def parse_lheading(self, m): 185 | """Parse setext heading.""" 186 | level = 1 if m.group(2) == '=' else 2 187 | self.renderer.heading(m.group(1), level=level) 188 | 189 | def parse_hrule(self, m): 190 | self.renderer.hrule() 191 | 192 | def parse_list_block(self, m): 193 | bull = m.group(2) 194 | self.renderer.list_start(ordered='.' in bull) 195 | cap = m.group(0) 196 | self._process_list_item(cap, bull) 197 | self.renderer.list_end() 198 | 199 | def _process_list_item(self, cap, bull): 200 | cap = self.grammar.list_item.findall(cap) 201 | 202 | _next = False 203 | length = len(cap) 204 | 205 | for i in range(length): 206 | item = cap[i][0] 207 | 208 | # remove the bullet 209 | space = len(item) 210 | item = self.grammar.list_bullet.sub('', item) 211 | 212 | # outdent 213 | if '\n ' in item: 214 | space = space - len(item) 215 | pattern = re.compile(r'^ {1,%d}' % space, flags=re.M) 216 | item = pattern.sub('', item) 217 | 218 | # determin whether item is loose or not 219 | loose = _next 220 | if not loose and re.search(r'\n\n(?!\s*$)', item): 221 | loose = True 222 | 223 | rest = len(item) 224 | if i != length - 1 and rest: 225 | _next = item[rest-1] == '\n' 226 | if not loose: 227 | loose = _next 228 | 229 | if loose: 230 | self.renderer.loose_item_start() 231 | else: 232 | self.renderer.list_item_start() 233 | 234 | # recurse 235 | self.read(item, self.list_rules) 236 | self.renderer.list_item_end() 237 | 238 | def parse_block_quote(self, m): 239 | self.renderer.block_quote_start() 240 | cap = m.group(0) 241 | pattern = re.compile(r'^ *> ?', flags=re.M) 242 | cap = pattern.sub('', cap) 243 | self.read(cap) 244 | self.renderer.block_quote_end() 245 | 246 | def parse_def_links(self, m): 247 | key = _keyify(m.group(1)) 248 | self.def_links[key] = { 249 | 'link': m.group(2), 250 | 'title': m.group(3), 251 | } 252 | 253 | def parse_def_footnotes(self, m): 254 | key = _keyify(m.group(1)) 255 | if key in self.def_footnotes: 256 | # footnote is already defined 257 | return 258 | 259 | self.def_footnotes[key] = 0 260 | 261 | self.renderer.footnote_start(key) 262 | 263 | text = m.group(2) 264 | 265 | if '\n' in text: 266 | lines = text.split('\n') 267 | whitespace = None 268 | for line in lines[1:]: 269 | space = len(line) - len(line.lstrip()) 270 | if space and (not whitespace or space < whitespace): 271 | whitespace = space 272 | newlines = [lines[0]] 273 | for line in lines[1:]: 274 | newlines.append(line[whitespace:]) 275 | text = '\n'.join(newlines) 276 | 277 | self.read(text, self.footnote_rules) 278 | 279 | self.renderer.footnote_end(key) 280 | 281 | def parse_table(self, m): 282 | item = self._process_table(m) 283 | 284 | cells = re.sub(r'(?: *\| *)?\n$', '', m.group(3)) 285 | cells = cells.split('\n') 286 | for i, v in enumerate(cells): 287 | v = re.sub(r'^ *\| *| *\| *$', '', v) 288 | cells[i] = re.split(r' *\| *', v) 289 | 290 | item['cells'] = cells 291 | self.renderer.table(item) 292 | 293 | def parse_nptable(self, m): 294 | item = self._process_table(m) 295 | 296 | cells = re.sub(r'\n$', '', m.group(3)) 297 | cells = cells.split('\n') 298 | for i, v in enumerate(cells): 299 | cells[i] = re.split(r' *\| *', v) 300 | 301 | item['cells'] = cells 302 | self.renderer.nptable(item) 303 | 304 | def _process_table(self, m): 305 | header = re.sub(r'^ *| *\| *$', '', m.group(1)) 306 | header = re.split(r' *\| *', header) 307 | align = re.sub(r' *|\| *$', '', m.group(2)) 308 | align = re.split(r' *\| *', align) 309 | 310 | for i, v in enumerate(align): 311 | if re.search(r'^ *-+: *$', v): 312 | align[i] = 'right' 313 | elif re.search(r'^ *:-+: *$', v): 314 | align[i] = 'center' 315 | elif re.search(r'^ *:-+ *$', v): 316 | align[i] = 'left' 317 | else: 318 | align[i] = None 319 | 320 | item = { 321 | 'type': 'table', 322 | 'header': header, 323 | 'align': align, 324 | } 325 | return item 326 | 327 | def parse_block_html(self, m): 328 | pre = m.group(1) in ['pre', 'script', 'style'] 329 | text = m.group(0) 330 | self.renderer.block_html(text, pre=pre) 331 | 332 | def parse_paragraph(self, m): 333 | text = m.group(1).rstrip('\n') 334 | self.renderer.paragraph(text) 335 | 336 | def parse_text(self, m): 337 | text = m.group(0) 338 | self.renderer.text(text) 339 | 340 | def parse_meta(self, m): 341 | if not m.group("alias") and not m.group("body"): 342 | self.renderer.text("META SPLIT") 343 | elif m.group("alias") and not m.group("body"): 344 | self.renderer.text("META SINGLE ALIAS") 345 | elif m.group("body"): 346 | self.renderer.text("META EXPLICIT") 347 | else: 348 | self.renderer.text("META") 349 | 350 | 351 | # ----------------------------------------------------------------------------- 352 | # Inline lexer 353 | # ----------------------------------------------------------------------------- 354 | 355 | class InlineGrammar(object): 356 | """Grammars for inline level tokens.""" 357 | 358 | escape = re.compile(r'^\\([\\`*{}\[\]()#+\-.!_>~|])') # \* \+ \! .... 359 | tag = re.compile( 360 | r'^|' # comment 361 | r'^<\/\w+>|' # close tag 362 | r'^<\w+[^>]*?>' # open tag 363 | ) 364 | autolink = re.compile(r'^ <([^ >]+(@|:\/)[^ >]+)>') 365 | link = re.compile( 366 | r'^!?\[(' 367 | r'(?:\[[^^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*' 368 | r')\]\(' 369 | r'''\s*?(?:\s+['"]([\s\S]*?)['"])?\s*''' 370 | r'\)' 371 | ) 372 | reflink = re.compile( 373 | r'^!?\[(' 374 | r'(?:\[[^^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*' 375 | r')\]\s*\[([^^\]]*)\]' 376 | ) 377 | nolink = re.compile(r'^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]') 378 | url = re.compile(r'''^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])''') 379 | double_emphasis = re.compile( 380 | r'^_{2}(.+?)_{2}(?!_)' # __word__ 381 | r'|' 382 | r'^\*{2}(.+?)\*{2}(?!\*)' # **word** 383 | ) 384 | emphasis = re.compile( 385 | r'^\b_((?:__|.)+?)_\b' # _word_ 386 | r'|' 387 | r'^\*((?:\*\*|.)+?)\*(?!\*)' # *word* 388 | ) 389 | code = re.compile(r'^(`+)\s*(.*?[^`])\s*\1(?!`)') # `code` 390 | linebreak = re.compile(r'^ {2,}\n(?!\s*$)') 391 | strikethrough = re.compile(r'^~~(?=\S)(.*?\S)~~') # ~~word~~ 392 | footnote = re.compile(r'^\[\^([^\]]+)\]') 393 | text = re.compile(r'^[\s\S]+?(?=[\\'): 455 | self._in_link = False 456 | self.renderer.tag(text) 457 | 458 | def parse_footnote(self, m): 459 | key = _keyify(m.group(1)) 460 | if key not in self.footnotes: 461 | return 462 | if self.footnotes[key]: 463 | return 464 | self.footnote_index += 1 465 | self.footnotes[key] = self.footnote_index 466 | self.renderer.footnote_ref(key, self.footnote_index) 467 | 468 | def parse_link(self, m): 469 | self._process_link(m, m.group(2), m.group(3)) 470 | 471 | def parse_reflink(self, m): 472 | key = _keyify(m.group(2) or m.group(1)) 473 | if key not in self.links: 474 | return 475 | ret = self.links[key] 476 | self._process_link(m, ret['link'], ret['title']) 477 | 478 | def parse_nolink(self, m): 479 | key = _keyify(m.group(1)) 480 | if key not in self.links: 481 | return 482 | ret = self.links[key] 483 | self._process_link(m, ret['link'], ret['title']) 484 | 485 | def _process_link(self, m, link, title=None): 486 | line = m.group(0) 487 | text = m.group(1) 488 | if line[0] == '!': 489 | self.renderer.image(link, title, text) 490 | return 491 | # self._in_link = True 492 | # NOTE: could recurse here with text 493 | self._in_link = False 494 | self.renderer.link(link, title, text) 495 | 496 | def parse_double_emphasis(self, m): 497 | text = m.group(2) or m.group(1) 498 | # NOTE: could recurse here with text 499 | self.renderer.double_emphasis(text) 500 | 501 | def parse_emphasis(self, m): 502 | text = m.group(2) or m.group(1) 503 | # NOTE: could recurse here with text 504 | self.renderer.emphasis(text) 505 | 506 | def parse_code(self, m): 507 | text = m.group(2) 508 | self.renderer.codespan(text) 509 | 510 | def parse_linebreak(self, m): 511 | self.renderer.linebreak() 512 | 513 | def parse_strikethrough(self, m): 514 | text = m.group(1) 515 | # NOTE: could recurse here with text 516 | self.renderer.strikethrough(text) 517 | 518 | def parse_text(self, m): 519 | text = m.group(0) 520 | self.renderer.text(text) 521 | 522 | 523 | # ----------------------------------------------------------------------------- 524 | # Markdown writer 525 | # ----------------------------------------------------------------------------- 526 | 527 | class MarkdownWriter(object): 528 | """A class for writing Markdown documents.""" 529 | def __init__(self): 530 | self._output = StringIO() 531 | self._list_number = 0 532 | self._in_quote = False 533 | 534 | # Buffer methods 535 | # ------------------------------------------------------------------------- 536 | 537 | @property 538 | def contents(self): 539 | return self._output.getvalue().rstrip() + '\n' # end of file \n 540 | 541 | def close(self): 542 | self._output.close() 543 | 544 | def __del__(self): 545 | self.close() 546 | 547 | def _write(self, contents): 548 | self._output.write(contents.rstrip('\n')) 549 | 550 | # New line methods 551 | # ------------------------------------------------------------------------- 552 | 553 | def newline(self): 554 | self._output.write('\n\n') 555 | self._list_number = 0 556 | 557 | def linebreak(self): 558 | self._output.write('\n') 559 | 560 | def ensure_newline(self, n): 561 | """Make sure there are 'n' line breaks at the end.""" 562 | assert n >= 0 563 | text = self._output.getvalue().rstrip('\n') 564 | if not text: 565 | return 566 | self._output = StringIO() 567 | self._output.write(text) 568 | self._output.write('\n' * n) 569 | text = self._output.getvalue() 570 | assert text[-n-1] != '\n' 571 | assert text[-n:] == '\n' * n 572 | 573 | # Block methods 574 | # ------------------------------------------------------------------------- 575 | 576 | def heading(self, text, level=None): 577 | assert 1 <= level <= 6 578 | self.ensure_newline(2) 579 | self.text(('#' * level) + ' ' + text) 580 | 581 | def numbered_list_item(self, text='', level=0): 582 | if level == 0: 583 | self._list_number += 1 584 | self.list_item(text, level=level, bullet=str(self._list_number), 585 | suffix='. ') 586 | 587 | def list_item(self, text='', level=0, bullet='*', suffix=' '): 588 | assert level >= 0 589 | self.text((' ' * level) + bullet + suffix + text) 590 | 591 | def code_start(self, lang=None): 592 | if lang is None: 593 | lang = '' 594 | self.text('```{0}'.format(lang)) 595 | self.ensure_newline(1) 596 | 597 | def code_end(self): 598 | self.ensure_newline(1) 599 | self.text('```') 600 | 601 | def quote_start(self): 602 | self._in_quote = True 603 | 604 | def quote_end(self): 605 | self._in_quote = False 606 | 607 | # Inline methods 608 | # ------------------------------------------------------------------------- 609 | 610 | def link(self, text, url): 611 | self.text('[{0}]({1})'.format(text, url)) 612 | 613 | def image(self, caption, url): 614 | self.text('![{0}]({1})'.format(caption, url)) 615 | 616 | def inline_code(self, text): 617 | self.text('`{0}`'.format(text)) 618 | 619 | def italic(self, text): 620 | self.text('*{0}*'.format(text)) 621 | 622 | def bold(self, text): 623 | self.text('**{0}**'.format(text)) 624 | 625 | def text(self, text): 626 | # Add quote '>' at the beginning of each line when quote is activated. 627 | if self._in_quote: 628 | if self._output.getvalue()[-1] == '\n': 629 | text = '> ' + text 630 | self._write(text) 631 | 632 | 633 | # ----------------------------------------------------------------------------- 634 | # Markdown filter 635 | # ----------------------------------------------------------------------------- 636 | 637 | def _replace_header_filter(filter): 638 | return {'h1': '# ', 639 | 'h2': '## ', 640 | 'h3': '### ', 641 | 'h4': '#### ', 642 | 'h5': '##### ', 643 | 'h6': '###### ', 644 | }[filter] 645 | 646 | 647 | def _filter_markdown(source, filters): 648 | """Only keep some Markdown headers from a Markdown string.""" 649 | lines = source.splitlines() 650 | # Filters is a list of 'hN' strings where 1 <= N <= 6. 651 | headers = [_replace_header_filter(filter) for filter in filters] 652 | lines = [line for line in lines if line.startswith(tuple(headers))] 653 | return '\n'.join(lines) 654 | 655 | 656 | class MarkdownFilter(object): 657 | """Filter Marakdown contents by keeping a subset of the contents. 658 | 659 | Parameters 660 | ---------- 661 | 662 | keep : str | None or False 663 | What to keep from Markdown cells. Can be: 664 | 665 | * None or False: don't keep Markdown contents 666 | * 'all': keep all Markdown contents 667 | * 'headers': just keep Markdown headers 668 | * 'h1,h3': just keep headers of level 1 and 3 (can be any combination) 669 | 670 | """ 671 | def __init__(self, keep=None): 672 | if keep is None: 673 | keep = 'all' 674 | if keep == 'headers': 675 | keep = 'h1,h2,h3,h4,h5,h6' 676 | if isinstance(keep, string_types) and keep != 'all': 677 | keep = keep.split(',') 678 | self._keep = keep 679 | 680 | def filter(self, source): 681 | # Skip Markdown cell. 682 | if not self._keep: 683 | return '' 684 | # Only keep some Markdown headers if keep_markdown is not 'all'. 685 | elif self._keep != 'all': 686 | source = _filter_markdown(source, self._keep) 687 | return source 688 | 689 | def __call__(self, source): 690 | return self.filter(source) 691 | --------------------------------------------------------------------------------