├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── bin └── pymarkdown ├── examples ├── bokeh.html ├── bokeh.md ├── bokeh.rendered.md ├── images │ └── 8734720408301.png ├── matplotlib.md ├── render.py ├── text.html ├── text.md └── text.rendered.md ├── pymarkdown ├── __init__.py ├── compatibility.py ├── core.py └── tests │ ├── __init__.py │ └── test_core.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - "2.6" 5 | - "2.7" 6 | 7 | 8 | install: 9 | # Install conda 10 | - wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh 11 | - bash miniconda.sh -b -p $HOME/miniconda 12 | - export PATH="$HOME/miniconda/bin:$PATH" 13 | - conda config --set always_yes yes --set changeps1 no 14 | - conda update conda 15 | 16 | # Install dependencies 17 | - conda create -n test-environment python=$TRAVIS_PYTHON_VERSION 18 | - source activate test-environment 19 | - conda install pytest matplotlib bokeh toolz 20 | 21 | # Install pymarkdown 22 | - python setup.py install 23 | 24 | script: 25 | py.test --doctest-modules pymarkdown --verbose 26 | 27 | notifications: 28 | email: false 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Matthew Rocklin 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | a. Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | b. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | c. Neither the name of pymarkdown nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 26 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 27 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 28 | DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include pymarkdown *.py 2 | recursive-include docs *.rst 3 | 4 | include setup.py 5 | include README.rst 6 | include LICENSE 7 | include MANIFEST.in 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyMarkdown 2 | ========== 3 | 4 | *You should not use this project. You should use 5 | [`knitpy`](https://github.com/JanSchulz/knitpy).* 6 | 7 | Evaluate code in markdown. 8 | 9 | Why? 10 | ---- 11 | 12 | Mostly because I was jealous of 13 | [RMarkdown/knitr](http://rmarkdown.rstudio.com/). 14 | 15 | The Jupyter notebook teaches us that interleaving prose, code, and results 16 | conveys meaning well. However when we author persistent content we often want a 17 | simple static text-based format. Markdown is good here because it plays well 18 | with other tools like `vi/emacs`, `pandoc`, and `git`. 19 | 20 | RMarkdown/knitr has demonstrated value in the R ecosystem, lets mimic that. 21 | 22 | 23 | How does this work? 24 | ------------------- 25 | 26 | PyMarkdown leverages the `doctest` module to parse code into prose and code 27 | segments much like a docstring. It then executes each code segment 28 | sequentially with Python's `exec`, tracking state throughout the document, 29 | emitting or correcting results from computation where appropriate. For some 30 | outputs we use custom rendering, notably leveraging common protocols like 31 | `__repr_html__` and special casing plotting libraries. 32 | 33 | In simple cases both input and output documents are valid markdown appropriate 34 | for publication on github, your favorite blogging software, or with pandoc. 35 | For complex rendering we've specialized on emitting HTML-enhanced Markdown, 36 | which looks great in a browser but limits cross-markup-language compatibility 37 | (sorry LaTeX users). 38 | 39 | 40 | Example 41 | ------- 42 | 43 | ### Input 44 | 45 | Our documents contain prose with *rich formatting*. 46 | 47 | ```Python 48 | # And code blocks 49 | >>> x = 1 50 | >>> x + 1 51 | 52 | >>> 2 + 2*x 53 | with potentially missing or wrong results 54 | ``` 55 | 56 | We run pymarkdown and look at updated results: 57 | 58 | $ pymarkdown text.md text.out.md 59 | 60 | ### Output 61 | 62 | Our documents contain prose with *rich formatting*. 63 | 64 | ```Python 65 | # And code blocks 66 | >>> x = 1 67 | >>> x + 1 68 | 2 69 | 70 | >>> 2 + 2*x 71 | 4 72 | ``` 73 | 74 | Fancy Support 75 | ------------- 76 | 77 | ### HTML 78 | 79 | PyMarkdown leverages standard protocols like `to_html` or `__repr_html__`. 80 | 81 | ```python 82 | >>> import pandas as pd 83 | >>> df = pd.DataFrame({'name': ['Alice', 'Bob', 'Charlie'], 84 | ... 'balance': [100, 200, 300]}) 85 | >>> df 86 | ``` 87 | 88 | ### HTML 89 | 90 | PyMarkdown leverages standard protocols like `to_html` or `__repr_html__`. 91 | 92 | ```python 93 | >>> import pandas as pd 94 | >>> df = pd.DataFrame({'name': ['Alice', 'Bob', 'Charlie'], 95 | ... 'balance': [100, 200, 300]}) 96 | >>> df 97 | ``` 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
balancename
0 100 Alice
1 200 Bob
2 300 Charlie
124 | 125 | 126 | Images 127 | ------ 128 | 129 | PyMarkdown supports figure objects from both 130 | [Matplotlib](http://matplotlib.org/) and 131 | [Bokeh](http://bokeh.pydata.org/). 132 | 133 | Bokeh plots only work in browser but remain interactive. You must create a 134 | standalone HTML file, possibly with [Pandoc](http://johnmacfarlane.net/pandoc/) 135 | 136 | pymarkdown myfile.md myfile.out.md 137 | pandoc myfile.out.md -o myfile.html --standalone 138 | 139 | But that output doesn't look good in a README, so here we'll use matplotlib 140 | 141 | ```Python 142 | >>> import matplotlib.pyplot as plt 143 | 144 | >>> fig = plt.figure() 145 | >>> plt.plot([1, 2, 3, 4, 5], [6, 7, 2, 4, 5]) 146 | >>> fig 147 | ``` 148 | 149 | PyMarkdown supports figure objects from both 150 | [Matplotlib](http://matplotlib.org/) and 151 | [Bokeh](http://bokeh.pydata.org/). 152 | 153 | Bokeh plots only work in browser but remain interactive. You must create a 154 | standalone HTML file, possibly with [Pandoc](http://johnmacfarlane.net/pandoc/) 155 | 156 | pymarkdown myfile.md myfile.out.md 157 | pandoc myfile.out.md -o myfile.html --standalone 158 | 159 | But that output doesn't look good in a README, so here we'll use matplotlib 160 | 161 | ```Python 162 | >>> import matplotlib.pyplot as plt 163 | 164 | >>> fig = plt.figure() 165 | >>> plt.plot([1, 2, 3, 4, 5], [6, 7, 2, 4, 5]) 166 | [] 167 | >>> fig 168 | ``` 169 | ![](examples/images/8734720408301.png) 170 | 171 | 172 | Support 173 | ------- 174 | 175 | There is none! This is a single-weekend project. Use at your own risk. 176 | Please contribute and take this project over. 177 | 178 | I've learned both that this isn't that hard and that it would be well 179 | appreciated by many people. If you have the attention span to read this far 180 | then I encourage you to extend or reinvent this project. 181 | 182 | 183 | TODO 184 | ---- 185 | 186 | - [x] Interact with Bokeh plots. These already implement `__repr_html__` so 187 | this probably just means linking to some static content somewhere. 188 | - [x] Interact with matplotlib 189 | - [x] Better command line interface (should use something like `argparse` rather 190 | than `sys.argv`) 191 | - [ ] Support inlining of values in prose blocks 192 | - [ ] Support options like ignore, echo=False, etc.. 193 | - [ ] Handle exceptions 194 | - [ ] Find a better name? 195 | 196 | 197 | Open Questions 198 | -------------- 199 | 200 | * I specialized towards HTML because that's what I care about at the 201 | moment. This might not be a good approach long term though. 202 | * Do we want to integrate with pandoc? I tend to do something like the 203 | following 204 | 205 | pymarkdown myfile.md myfile.out.md && \ 206 | pandoc myfile.out.md -o myfile.html --standalone 207 | 208 | But it would be nice for this to be easier to write in one go 209 | 210 | pymarkdown myfile.md -o myfile.html 211 | 212 | Have other thoughts? Great! Please implement them :) 213 | -------------------------------------------------------------------------------- /bin/pymarkdown: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | from pymarkdown import process 5 | 6 | 7 | def gen_parser(): 8 | """ 9 | Generates the argument parser 10 | """ 11 | description = 'Evaluate code in markdown' 12 | parser = argparse.ArgumentParser(description=description) 13 | 14 | help = "Input md file" 15 | parser.add_argument('infile', type=str, help=help) 16 | 17 | help = "Output md file" 18 | parser.add_argument('output', type=str, help=help) 19 | 20 | return parser 21 | 22 | 23 | if __name__ == '__main__': 24 | parser = gen_parser() 25 | args = parser.parse_args() 26 | 27 | fn, outfn = args.infile, args.output 28 | 29 | with open(fn) as f: 30 | text = f.read() 31 | 32 | output = process(text) 33 | 34 | with open(outfn, 'w') as f: 35 | f.write(output) 36 | -------------------------------------------------------------------------------- /examples/bokeh.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 28 | 29 | 30 | 31 | 32 | 33 |

We try out bokeh plots

34 |
>>> from bokeh.plotting import figure
35 | >>> x = [1, 2, 3, 4, 5]
36 | >>> y = [6, 7, 2, 4, 5]
37 | 
38 | 
39 | >>> p = figure(title="simple line example", x_axis_label='x', y_axis_label='y')
40 | >>> p.line(x, y, legend="Temp.", line_width=2)
41 |
42 |
43 |

Did that work?

44 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /examples/bokeh.md: -------------------------------------------------------------------------------- 1 | We try out bokeh plots 2 | ====================== 3 | 4 | ```python 5 | >>> from bokeh.plotting import figure 6 | >>> x = [1, 2, 3, 4, 5] 7 | >>> y = [6, 7, 2, 4, 5] 8 | 9 | >>> p = figure(title="simple line example", x_axis_label='x', y_axis_label='y') 10 | >>> p.line(x, y, legend="Temp.", line_width=2) 11 | ``` 12 | 13 | Did that work? 14 | -------------------------------------------------------------------------------- /examples/bokeh.rendered.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We try out bokeh plots 5 | ====================== 6 | 7 | ```python 8 | >>> from bokeh.plotting import figure 9 | >>> x = [1, 2, 3, 4, 5] 10 | >>> y = [6, 7, 2, 4, 5] 11 | 12 | 13 | >>> p = figure(title="simple line example", x_axis_label='x', y_axis_label='y') 14 | >>> p.line(x, y, legend="Temp.", line_width=2) 15 | ``` 16 |
17 | ```python 18 | ``` 19 | 20 | Did that work? 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/images/8734720408301.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrocklin/pymarkdown/22ad745e3dd55c5f3b968d310376833020657668/examples/images/8734720408301.png -------------------------------------------------------------------------------- /examples/matplotlib.md: -------------------------------------------------------------------------------- 1 | Matplotlib 2 | ========== 3 | 4 | We save matplotlib figures in a local `images/` directory and then link to them 5 | from the markdown file. 6 | 7 | ```python 8 | >>> import matplotlib.pyplot as plt 9 | 10 | >>> fig = plt.figure() 11 | >>> plt.plot([1, 2, 3, 4, 5], [6, 7, 2, 4, 5]) 12 | >>> fig 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/render.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | from pymarkdown import process 3 | import os 4 | 5 | fns = sorted([fn for fn in glob('examples/*.md') 6 | if 'rendered' not in fn]) 7 | 8 | for fn in fns: 9 | with open(fn) as f: 10 | text = f.read() 11 | 12 | processed = process(text) 13 | 14 | rendered_fn = fn.rsplit('.', 1)[0] + '.rendered.md' 15 | with open(rendered_fn, 'w') as f: 16 | f.write(processed) 17 | 18 | html_fn = fn.rsplit('.', 1)[0] + '.html' 19 | os.popen('pandoc %s -o %s --standalone' % (rendered_fn, html_fn)).read() 20 | -------------------------------------------------------------------------------- /examples/text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 28 | 29 | 30 |

Title

31 |

Some prose

32 |
# And some code
33 | 
34 | >>> x = 1
35 | >>> x + 1
36 | 2
37 | 
38 | 
39 | >>> 2 + 2*x
40 | 4
41 |

We also handle HTML with __repr_html__

42 |
>>> import pandas as pd
43 | >>> df = pd.DataFrame({'name': ['Alice', 'Bob', 'Charlie'],
44 | ...                    'balance': [100, 200, 300]})
45 | >>> df
46 | 47 | 48 | 49 | 50 | 53 | 56 | 57 | 58 | 59 | 60 | 63 | 66 | 69 | 70 | 71 | 74 | 77 | 80 | 81 | 82 | 85 | 88 | 91 | 92 | 93 |
51 | balance 52 | 54 | name 55 |
61 | 0 62 | 64 | 100 65 | 67 | Alice 68 |
72 | 1 73 | 75 | 200 76 | 78 | Bob 79 |
83 | 2 84 | 86 | 300 87 | 89 | Charlie 90 |
94 |
95 | 96 | 97 | -------------------------------------------------------------------------------- /examples/text.md: -------------------------------------------------------------------------------- 1 | Title 2 | ===== 3 | 4 | Some prose 5 | 6 | 7 | ```Python 8 | # And some code 9 | >>> x = 1 10 | >>> x + 1 11 | 12 | >>> 2 + 2*x 13 | wrong result 14 | ``` 15 | 16 | We also handle HTML with `__repr_html__` 17 | 18 | ```Python 19 | >>> import pandas as pd 20 | >>> df = pd.DataFrame({'name': ['Alice', 'Bob', 'Charlie'], 21 | ... 'balance': [100, 200, 300]}) 22 | >>> df 23 | ``` 24 | -------------------------------------------------------------------------------- /examples/text.rendered.md: -------------------------------------------------------------------------------- 1 | Title 2 | ===== 3 | 4 | Some prose 5 | 6 | 7 | ```Python 8 | # And some code 9 | 10 | >>> x = 1 11 | >>> x + 1 12 | 2 13 | 14 | 15 | >>> 2 + 2*x 16 | 4 17 | ``` 18 | 19 | We also handle HTML with `__repr_html__` 20 | 21 | ```Python 22 | >>> import pandas as pd 23 | >>> df = pd.DataFrame({'name': ['Alice', 'Bob', 'Charlie'], 24 | ... 'balance': [100, 200, 300]}) 25 | >>> df 26 | ``` 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
balancename
0 100 Alice
1 200 Bob
2 300 Charlie
53 | ```Python 54 | ``` -------------------------------------------------------------------------------- /pymarkdown/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import process 2 | -------------------------------------------------------------------------------- /pymarkdown/compatibility.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info[0] == 3: 4 | from io import StringIO 5 | unicode = str 6 | 7 | if sys.version_info[0] == 2: 8 | from StringIO import StringIO 9 | unicode = unicode 10 | -------------------------------------------------------------------------------- /pymarkdown/core.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | import doctest 4 | import re 5 | from contextlib import contextmanager 6 | from .compatibility import StringIO, unicode 7 | import itertools 8 | import sys 9 | import os 10 | from toolz.curried import pipe, map, filter, concat 11 | 12 | 13 | def process(text): 14 | """ Replace failures in docstring with results """ 15 | parts = pipe(text, parser.parse, 16 | filter(None), 17 | map(separate_fence), 18 | concat, list) 19 | 20 | scope = dict() # scope of variables in our executed environment 21 | state = dict() # state of pymarkdown traversal 22 | 23 | out_parts = list() 24 | for part in parts: 25 | out, scope, state = step(part, scope, state) 26 | out_parts.extend(out) 27 | 28 | head = '\n'.join(sorted(state.get('headers', set()))) 29 | body = pipe(out_parts, map(render_part), 30 | filter(None), 31 | '\n'.join) 32 | foot = '\n\n'.join(state.get('footers', [])) 33 | 34 | return '\n\n'.join([head, body, foot]).strip() 35 | 36 | 37 | def step(part, scope, state): 38 | """ Step through one part of the document 39 | 40 | 1. Prose: pass through 41 | 2. Code fence: record 42 | 3. Code: evaluate 43 | 4. Code with html output: 44 | print source, end code block, print html, start code block 45 | """ 46 | if isinstance(part, (str, unicode)) and iscodefence(part): 47 | if 'code' in state: 48 | del state['code'] 49 | else: 50 | state['code'] = part 51 | return [part], scope, state 52 | if isinstance(part, (str, unicode)): 53 | return [part], scope, state 54 | if isinstance(part, doctest.Example): 55 | if valid_statement('_last = ' + part.source): 56 | code = compile('_last = ' + part.source, '', 'single') 57 | exec(code, scope) 58 | result = scope.pop('_last') 59 | else: 60 | with swap_stdout() as s: 61 | code = compile(part.source, '', 'single') 62 | exec(code, scope) 63 | result = s.read().rstrip().strip("'") 64 | 65 | if isassignment(part.source): 66 | out = [doctest.Example(part.source, '')] 67 | elif type_key(result) in custom_renderers: 68 | func = custom_renderers[type_key(type(result))] 69 | out = [doctest.Example(part.source, '')] + func(result, state) 70 | elif hasattr(result, '__repr_html__'): 71 | out = [doctest.Example(part.source, ''), 72 | closing_fence(state['code']), 73 | result.__repr_html__(), 74 | state['code']] 75 | elif hasattr(result, 'to_html'): 76 | out = [doctest.Example(part.source, ''), 77 | closing_fence(state['code']), 78 | result.to_html(), 79 | state['code']] 80 | else: 81 | if not isinstance(result, str): 82 | result = repr(result) 83 | out = [doctest.Example(part.source, result)] 84 | del scope['__builtins__'] 85 | return out, scope, state 86 | 87 | raise NotImplementedError() 88 | 89 | 90 | def iscodefence(line): 91 | return (line.startswith('```') 92 | or line.startswith('~~~') 93 | or line.startswith('{%') and 'highlight' in line 94 | or line.startswith('{%') and 'syntax' in line) 95 | 96 | 97 | def closing_fence(opener): 98 | """ Closing pair an an opening fence 99 | 100 | >>> closing_fence('```Python') 101 | '```' 102 | """ 103 | if '```' in opener: 104 | return '```' 105 | if '~~~' in opener: 106 | return '~~~' 107 | if 'highlight' in opener: 108 | return '{% endhighlight %}' 109 | if 'syntax' in opener: 110 | return '{% endsyntax %}' 111 | 112 | 113 | def separate_fence(part, endl='\n'): 114 | """ Separate code fences from prose or example sections 115 | 116 | >> separate_fence('Hello\n```python') 117 | ['Hello', '```python'] 118 | 119 | >> separate_fence(doctest.Example('1 + 1', '2\n```')) 120 | [Example('1 + 1', '2'), '```'] 121 | """ 122 | if isinstance(part, (str, unicode)): 123 | lines = part.split('\n') 124 | groups = itertools.groupby(lines, iscodefence) 125 | return ['\n'.join(group) for _, group in groups] 126 | if isinstance(part, doctest.Example): 127 | lines = part.want.rstrip().split('\n') 128 | fences = list(map(iscodefence, lines)) 129 | if any(fences): 130 | i = fences.index(True) 131 | return [doctest.Example(part.source, '\n'.join(lines[:i])), 132 | lines[i], 133 | '\n'.join(lines[i+1:])] 134 | else: 135 | return [part] 136 | 137 | 138 | def prompt(text): 139 | """ Add >>> and ... prefixes back into code 140 | 141 | prompt("x + 1") # doctest: +SKIP 142 | '>>> x + 1' 143 | prompt("for i in seq:\n print(i)") 144 | '>>> for i in seq:\n... print(i)' 145 | """ 146 | return '>>> ' + text.rstrip().replace('\n', '\n... ') 147 | 148 | 149 | parser = doctest.DocTestParser() 150 | 151 | def render_part(part): 152 | """ Render a part into text """ 153 | if isinstance(part, (str, unicode)): 154 | return part 155 | if isinstance(part, doctest.Example): 156 | result = prompt(part.source) 157 | if part.want: 158 | result = result + '\n' + part.want 159 | return result.rstrip() 160 | 161 | def isassignment(line): 162 | return not not re.match('^\w+\s*=', line) 163 | 164 | 165 | def valid_statement(source): 166 | """ Is source a valid statement? 167 | 168 | >>> valid_statement('x = 1') 169 | True 170 | >>> valid_statement('x = print foo') 171 | False 172 | """ 173 | try: 174 | compile(source, '', 'single') 175 | return True 176 | except SyntaxError: 177 | return False 178 | 179 | 180 | @contextmanager 181 | def swap_stdout(): 182 | """ Swap sys.stdout with a StringIO object 183 | 184 | Yields the StringIO object and cleans up afterwards 185 | 186 | >>> with swap_stdout() as s: 187 | ... print("Hello!") 188 | >>> s.read() 189 | 'Hello!\\n' 190 | """ 191 | s = StringIO() 192 | old = sys.stdout 193 | sys.stdout = s 194 | 195 | try: 196 | yield s 197 | finally: 198 | s.pos = 0 199 | sys.stdout = old 200 | 201 | 202 | def type_key(typ): 203 | if not isinstance(typ, type): 204 | typ = type(typ) 205 | return '%s.%s' % (typ.__module__, typ.__name__) 206 | 207 | 208 | def render_bokeh_figure(result, state): 209 | from bokeh.resources import CDN 210 | if 'headers' not in state: 211 | state['headers'] = set() 212 | state['headers'].update([ 213 | '' % CDN.js_files[0], 214 | '' % CDN.css_files[0] 215 | ]) 216 | 217 | from bokeh.embed import components 218 | script, div = components(result, CDN) 219 | if 'footers' not in state: 220 | state['footers'] = list() 221 | state['footers'].append(script) 222 | return [closing_fence(state['code']), 223 | div, 224 | state['code']] 225 | 226 | 227 | def render_matplotlib_figure(result, state, directory=None): 228 | if directory is None: 229 | directory = os.path.join(os.getcwd(), 'images') 230 | else: 231 | directory = os.path.join(directory, 'images') 232 | import matplotlib.pyplot as plt 233 | fn = os.path.join(directory, str(abs(hash(result))) + '.png') 234 | if not os.path.exists(directory): 235 | os.mkdir(directory) 236 | result.savefig(fn) 237 | 238 | if 'images' not in state: 239 | state['images'] = list() 240 | state['images'].append(fn) 241 | 242 | return [closing_fence(state['code']), 243 | '![](%s)' % fn, 244 | state['code']] 245 | 246 | 247 | custom_renderers = {'bokeh.plotting.Figure': render_bokeh_figure, 248 | 'matplotlib.figure.Figure': render_matplotlib_figure} 249 | -------------------------------------------------------------------------------- /pymarkdown/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrocklin/pymarkdown/22ad745e3dd55c5f3b968d310376833020657668/pymarkdown/tests/__init__.py -------------------------------------------------------------------------------- /pymarkdown/tests/test_core.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import os 3 | 4 | from pymarkdown.core import (process, parser, step, separate_fence, 5 | render_bokeh_figure, render_matplotlib_figure) 6 | 7 | 8 | text = """ 9 | Title 10 | ===== 11 | 12 | Some prose 13 | 14 | ``` 15 | >>> x = 1 16 | >>> x + 1 17 | ``` 18 | """.strip() 19 | 20 | desired = """ 21 | Title 22 | ===== 23 | 24 | Some prose 25 | 26 | ``` 27 | >>> x = 1 28 | >>> x + 1 29 | 2 30 | ``` 31 | """.strip() 32 | 33 | 34 | def test_process(): 35 | assert process(text) == desired 36 | 37 | 38 | 39 | class Shout(object): 40 | def __init__(self, data): 41 | self.data = data 42 | 43 | def __repr_html__(self): 44 | return "

%s

" % self.data 45 | 46 | def test_step(): 47 | out, scope, state = step("prose", {'x': 1}, {}) 48 | assert (out, scope, state) == (["prose"], {'x': 1}, {}) 49 | 50 | out, scope, state = step("```Python", {'x': 1}, {}) 51 | assert (out, scope, state) == (["```Python"], {'x': 1}, {'code': '```Python'}) 52 | 53 | # Remove code state 54 | out, scope, state = step("```", {'x': 1}, {'code': '```Python'}) 55 | assert (out, scope, state) == (["```"], {'x': 1}, {}) 56 | 57 | a = doctest.Example("x + 1", "3") 58 | b = doctest.Example("x + 1", "2") 59 | out, scope, state = step(a, {'x': 1}, {'code': '```Python'}) 60 | assert (out, scope, state) == ([b], {'x': 1}, {'code': '```Python'}) 61 | 62 | a = doctest.Example("y = x + 1", "") 63 | out, scope, state = step(a, {'x': 1}, {'code': '```Python'}) 64 | assert (out, scope, state) == ([a], {'x': 1, 'y': 2}, {'code': '```Python'}) 65 | 66 | a = doctest.Example("Shout('Hello!')", '') 67 | out, scope, state = step(a, {'Shout': Shout}, {'code': '```Python'}) 68 | assert out == [a, '```', Shout('Hello!').__repr_html__(), '```Python'] 69 | assert state == {'code': '```Python'} 70 | 71 | 72 | a = doctest.Example("print(5)", '') 73 | b = doctest.Example("print(5)", '5') 74 | out, scope, state = step(a, {}, {'code': '```Python'}) 75 | assert (out, scope, state) == ([b], {}, {'code': '```Python'}) 76 | 77 | 78 | def test_separate_fence(): 79 | assert separate_fence('Hello\n```python') == ['Hello', '```python'] 80 | 81 | assert separate_fence(doctest.Example('1 + 1', '2\n```')) ==\ 82 | [doctest.Example('1 + 1', '2'), '```', ''] 83 | 84 | 85 | def test_render_bokeh(): 86 | try: 87 | from bokeh.plotting import figure 88 | except ImportError: 89 | return 90 | p = figure(title='My Title') 91 | p.line([1, 2, 3], [1, 4, 9]) 92 | 93 | state = {'code': '```'} 94 | out = render_bokeh_figure(p, state) 95 | 96 | assert out[0] == '```' 97 | assert out[1].startswith('