├── 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 | # 
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 | 
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 | 
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': ''},
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 | [](https://travis-ci.org/rossant/ipymd)
2 | [](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 | 
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 | ""
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 | '- ', 'First.', '
',
136 | '- ', 'Second.', '
',
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 | '- ', 'First.', '
',
335 | '- ', 'Second.', '
',
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 | "\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'([^\s>]+)>?' # 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\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(''.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 |
--------------------------------------------------------------------------------