Panflute is a Python package to easily write Pandoc filters.
13 | It is pythonic and comes with batteries included.
14 |
15 | `_)
7 | """
8 |
9 | from .utils import debug
10 |
11 | from .containers import ListContainer, DictContainer
12 |
13 | from .base import Element, Block, Inline, MetaValue
14 |
15 | # These elements are not part of pandoc-types
16 | from .elements import (
17 | Doc, Citation, ListItem,
18 | DefinitionItem, Definition, LineItem)
19 |
20 | from .elements import (
21 | Null, HorizontalRule, Space, SoftBreak, LineBreak, Str,
22 | Code, BlockQuote, Note, Div, Plain, Para, Emph, Strong, Underline,
23 | Strikeout, Superscript, Subscript, SmallCaps, Span, RawBlock, RawInline,
24 | Math, CodeBlock, Link, Image, BulletList, OrderedList, DefinitionList,
25 | LineBlock, Header, Quoted, Cite)
26 |
27 | from .table_elements import (
28 | Table, TableHead, TableFoot, TableBody,
29 | TableRow, TableCell, Caption)
30 |
31 | from .elements import (
32 | MetaList, MetaMap, MetaString, MetaBool, MetaInlines, MetaBlocks)
33 |
34 | from .io import load, dump, run_filter, run_filters
35 | from .io import toJSONFilter, toJSONFilters # Wrappers
36 |
37 | from .tools import (
38 | stringify, yaml_filter, shell, run_pandoc, convert_text, get_option)
39 |
40 | from .autofilter import main, panfl, get_filter_dirs, stdio
41 |
42 | from .version import __version__
43 |
--------------------------------------------------------------------------------
/tests/test_stringify.py:
--------------------------------------------------------------------------------
1 | import panflute as pf
2 |
3 |
4 | def validate(markdown_text, expected_text, verbose=False):
5 | doc = pf.convert_text(markdown_text, input_format='markdown', output_format='panflute', standalone=True)
6 | output_text = pf.stringify(doc)
7 | if verbose:
8 | print('<<<< EXPECTED <<<<')
9 | print(expected_text)
10 | print('<<<< OUTPUT <<<<')
11 | print(output_text)
12 | print('>>>>>>>>>>>>>>>>')
13 | assert expected_text == output_text
14 |
15 |
16 | def test_simple():
17 | markdown_text = '''Hello **world**! *How* are ~you~ doing?'''
18 | expected_text = '''Hello world! How are you doing?\n\n'''
19 | validate(markdown_text, expected_text)
20 |
21 |
22 | def test_cite():
23 | markdown_text = '[@abc, p.23]'
24 | expected_text = '[@abc, p.23]\n\n'
25 | validate(markdown_text, expected_text)
26 |
27 |
28 | def test_definition_list():
29 | markdown_text = '''Term 1\n: Definition 1\n\nTerm 2 with *inline markup*\n\n: Definition 2'''
30 | expected_text = '''- Term 1: Definition 1\n- Term 2 with inline markup: Definition 2\n\n'''
31 | validate(markdown_text, expected_text)
32 |
33 |
34 | def test_definition_list_complex():
35 | markdown_text = '''Term 1\n~ Definition 1\n\nTerm 2\n~ Definition 2a\n~ Definition 2b'''
36 | expected_text = '''- Term 1: Definition 1\n- Term 2: Definition 2a; Definition 2b'''
37 | validate(markdown_text, expected_text)
38 |
39 |
40 | if __name__ == "__main__":
41 | test_simple()
42 | test_cite()
43 | test_definition_list()
44 | test_definition_list_complex()
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Sergio Correia
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification,
6 | are permitted provided that the following conditions are met:
7 |
8 | * Redistributions of source code must retain the above copyright notice,
9 | this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 | * Neither the name of Sergio Correia 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 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/examples/input/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, John MacFarlane
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,
8 | this list of conditions and the following disclaimer.
9 |
10 | - Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | - Neither the name of John Macfarlane nor the names of its contributors may
15 | be used to endorse or promote products derived from this software without
16 | 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/pandocfilters/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, John MacFarlane
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,
8 | this list of conditions and the following disclaimer.
9 |
10 | - Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | - Neither the name of John Macfarlane nor the names of its contributors may
15 | be used to endorse or promote products derived from this software without
16 | 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/pandocfilters/graphviz.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Pandoc filter to process code blocks with class "graphviz" into
5 | graphviz-generated images.
6 | """
7 |
8 | import pygraphviz
9 | import hashlib
10 | import os
11 | import sys
12 | from pandocfilters import toJSONFilter, Str, Para, Image
13 |
14 |
15 | def sha1(x):
16 | return hashlib.sha1(x.encode(sys.getfilesystemencoding())).hexdigest()
17 |
18 | imagedir = "graphviz-images"
19 |
20 |
21 | def graphviz(key, value, format, meta):
22 | if key == 'CodeBlock':
23 | [[ident, classes, keyvals], code] = value
24 | caption = "caption"
25 | if "graphviz" in classes:
26 | G = pygraphviz.AGraph(string=code)
27 | G.layout()
28 | filename = sha1(code)
29 | if format == "html":
30 | filetype = "png"
31 | elif format == "latex":
32 | filetype = "pdf"
33 | else:
34 | filetype = "png"
35 | alt = Str(caption)
36 | src = imagedir + '/' + filename + '.' + filetype
37 | if not os.path.isfile(src):
38 | try:
39 | os.mkdir(imagedir)
40 | sys.stderr.write('Created directory ' + imagedir + '\n')
41 | except OSError:
42 | pass
43 | G.draw(src)
44 | sys.stderr.write('Created image ' + src + '\n')
45 | tit = ""
46 | return Para([Image(['', [], []], [alt], [src, tit])])
47 |
48 | if __name__ == "__main__":
49 | toJSONFilter(graphviz)
50 |
--------------------------------------------------------------------------------
/examples/panflute/abc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Pandoc filter to process code blocks with class "abc" containing
5 | ABC notation into images. Assumes that abcm2ps and ImageMagick's
6 | convert are in the path. Images are put in the abc-images directory.
7 | """
8 |
9 | import hashlib
10 | import os
11 | import sys
12 | from panflute import toJSONFilter, Para, Image, CodeBlock
13 | from subprocess import Popen, PIPE, call
14 |
15 | imagedir = "abc-images"
16 |
17 |
18 | def sha1(x):
19 | return hashlib.sha1(x.encode(sys.getfilesystemencoding())).hexdigest()
20 |
21 |
22 | def abc2eps(abc, filetype, outfile):
23 | p = Popen(["abcm2ps", "-O", outfile + '.eps', "-"], stdin=PIPE)
24 | p.stdin.write(abc)
25 | p.communicate()
26 | p.stdin.close()
27 | call(["convert", outfile + '.eps', outfile + '.' + filetype])
28 |
29 |
30 | def abc(elem, doc):
31 | if type(elem) == CodeBlock and 'abc' in elem.classes:
32 | code = elem.text
33 | outfile = os.path.join(imagedir, sha1(code))
34 | filetype = {'html': 'png', 'latex': 'pdf'}.get(doc.format, 'png')
35 | src = outfile + '.' + filetype
36 | if not os.path.isfile(src):
37 | try:
38 | os.mkdir(imagedir)
39 | sys.stderr.write('Created directory ' + imagedir + '\n')
40 | except OSError:
41 | pass
42 | abc2eps(code.encode("utf-8"), filetype, outfile)
43 | sys.stderr.write('Created image ' + src + '\n')
44 | return Para(Image(url=src))
45 |
46 |
47 | if __name__ == "__main__":
48 | toJSONFilter(abc)
49 |
--------------------------------------------------------------------------------
/docs/source/_static/example.md:
--------------------------------------------------------------------------------
1 | ---
2 | author: someone
3 | cmd: 'pandoc --to markdown example.md -F ./remove-tables.py'
4 | toc-depth: 2
5 | ---
6 |
7 | # Header 1
8 |
9 | Some *emphasized*, **bold** and ~~striken out~~ text.
10 |
11 | ## Header 2
12 |
13 | $include(../ch2/invalid)
14 |
15 | $include this
16 | $include is invalid
17 |
18 | $include valid_file
19 |
20 | !include ../ch2/invalid
21 |
22 | $include ../ch2/a random valid file.md
23 |
24 | $include "quotes here"
25 |
26 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
27 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
28 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
29 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
30 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
31 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
32 |
33 | ### Header 3
34 |
35 | | Variable | Mean |
36 | |----------|------|
37 | | Price | 10 |
38 | | Weight | 12 |
39 |
40 | lorem lorem..
41 |
42 | | Variable | Mean |
43 | |----------|------|
44 | | Price | 10 |
45 | | Price | 10 |
46 | | Price | 10 |
47 | | Weight | 12 |
48 |
49 | # Another header 1
50 |
51 | ## Tables go here
52 |
53 | $tables
54 |
55 | ## Fenced tables...
56 |
57 | ~~~ csv
58 | title: Some Title
59 | has-header: True
60 | ---
61 | Col1, Col2, Col3
62 | 1, 2, 3
63 | 10, 20, 30
64 | ~~~
65 |
66 | ## Something else here
67 |
68 | [Stack Overflow](wiki://)
69 |
70 | [pizza](wiki://)
71 |
72 | What is Pandoc? [Pandoc](wiki://)
73 |
74 | the end..
75 |
76 |
--------------------------------------------------------------------------------
/tests/test_get_metadata.py:
--------------------------------------------------------------------------------
1 | import panflute as pf
2 | from pathlib import Path
3 |
4 |
5 | def test():
6 | # chcp 65001 --> might be required if running from cmd on Windows
7 |
8 | fn = Path("./tests/sample_files/heavy_metadata/example.md")
9 | print(f'\n - Loading markdown "{fn}"')
10 | with fn.open(encoding='utf-8') as f:
11 | markdown_text = f.read()
12 | print(' - Converting Markdown to JSON')
13 | json_pandoc = pf.convert_text(markdown_text, input_format='markdown', output_format='json', standalone=True)
14 | print(' - Constructing Doc() object')
15 | doc = pf.convert_text(json_pandoc, input_format='json', output_format='panflute', standalone=True)
16 |
17 | print(' - Verifying that we can access metadata correctly')
18 |
19 | meta = doc.get_metadata('title')
20 | assert meta == "Lorem Ipsum: Title"
21 |
22 | meta = doc.get_metadata('title', builtin=False)
23 | assert type(meta) == pf.MetaInlines
24 |
25 | # foobar key doesn't exist
26 | meta = doc.get_metadata('foobar', True)
27 | assert meta == True
28 |
29 | meta = doc.get_metadata('foobar', 123)
30 | assert meta == 123
31 |
32 | meta = doc.get_metadata('abstract')
33 | assert meta.startswith('Bring to the table win-win')
34 |
35 | meta = doc.get_metadata('key1.key1-1')
36 | assert meta == ['value1-1-1', 'value1-1-2']
37 |
38 | meta = doc.get_metadata('amsthm.plain')
39 | assert type(meta) == list
40 | assert meta[0]['Theorem'] == 'Lemma'
41 |
42 | meta = doc.get_metadata('')
43 | assert len(meta) > 10
44 |
45 | print(' - Done!')
46 |
47 | if __name__ == "__main__":
48 | test()
49 |
--------------------------------------------------------------------------------
/docs/source/_static/include.py:
--------------------------------------------------------------------------------
1 | """
2 | Panflute filter to allow file includes
3 |
4 | Each include statement has its own line and has the syntax:
5 |
6 | $include ../somefolder/somefile
7 |
8 | Each include statement must be in its own paragraph. That is, in its own line
9 | and separated by blank lines.
10 |
11 | If no extension was given, ".md" is assumed.
12 | """
13 |
14 | import os
15 | import panflute as pf
16 |
17 |
18 | def is_include_line(elem):
19 | if len(elem.content) < 3:
20 | return False
21 | elif not all (isinstance(x, (pf.Str, pf.Space)) for x in elem.content):
22 | return False
23 | elif elem.content[0].text != '$include':
24 | return False
25 | elif type(elem.content[1]) != pf.Space:
26 | return False
27 | else:
28 | return True
29 |
30 |
31 | def get_filename(elem):
32 | fn = pf.stringify(elem, newlines=False).split(maxsplit=1)[1]
33 | if not os.path.splitext(fn)[1]:
34 | fn += '.md'
35 | return fn
36 |
37 |
38 | def action(elem, doc):
39 | if isinstance(elem, pf.Para) and is_include_line(elem):
40 |
41 | fn = get_filename(elem)
42 | if not os.path.isfile(fn):
43 | return
44 |
45 | with open(fn) as f:
46 | raw = f.read()
47 |
48 | new_elems = pf.convert_text(raw)
49 |
50 | # Alternative A:
51 | return new_elems
52 | # Alternative B:
53 | # div = pf.Div(*new_elems, attributes={'source': fn})
54 | # return div
55 |
56 |
57 | def main(doc=None):
58 | return pf.run_filter(action, doc=doc)
59 |
60 |
61 | if __name__ == '__main__':
62 | main()
63 |
--------------------------------------------------------------------------------
/tests/test_examples_run.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from shutil import which
4 | from subprocess import Popen, PIPE, call
5 |
6 |
7 | def shell(args, msg=None):
8 | # Fix Windows error if passed a string
9 | proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
10 | out, err = proc.communicate(input=msg)
11 | exitcode = proc.returncode
12 | if exitcode!=0:
13 | print('\n------', file=sys.stderr)
14 | print(err.decode('utf-8'), file=sys.stderr, end='')
15 | print('------\n', file=sys.stderr)
16 | raise IOError
17 | return out
18 |
19 |
20 | def build_cmd(fn):
21 | pandoc_path = which('pandoc')
22 | input_fn = './examples/input/' + os.path.splitext(fn)[0] + '-sample.md'
23 | filter_fn = './examples/{}/{}'.format('panflute', fn)
24 | return [pandoc_path, '-F', filter_fn, input_fn]
25 |
26 |
27 | def test_filters_run():
28 | print('Verify that all panflute actually filters run')
29 | panflute_filters = os.listdir('./examples/panflute')
30 |
31 | # GABC, etc requires installing miktex packages...
32 | excluded = ['abc.py', 'plantuml.py', 'tikz', 'gabc.py', 'graphviz.py']
33 |
34 | # Lilypond, etc. have bugs
35 | excluded.extend(['lilypond.py'])
36 |
37 | # These have no "**-sample.md" file
38 | excluded.extend(['headers.py', 'table-better.py', 'table.py'])
39 |
40 | for fn in panflute_filters:
41 | if not fn.startswith('__') and fn not in excluded:
42 | print(' - Testing', fn)
43 | panflute_cmd = build_cmd(fn)
44 |
45 | print(' [CMD]', ' '.join(panflute_cmd))
46 | panflute = shell(panflute_cmd).decode('utf-8')
47 | print()
48 |
49 |
50 | if __name__ == '__main__':
51 | test_filters_run()
52 |
--------------------------------------------------------------------------------
/tests/input/awesome-c/test.py:
--------------------------------------------------------------------------------
1 | import panflute as pf
2 |
3 | input_fn = 'benchmark.json'
4 | output_fn = 'panflute.json'
5 |
6 |
7 | def empty_test(element, doc):
8 | return
9 |
10 | def test_filter(element, doc):
11 | if type(element)==pf.Header:
12 | return []
13 | if type(element)==pf.Str:
14 | element.text = element.text + '!!'
15 | return element
16 |
17 |
18 | print('\nLoading JSON...')
19 |
20 | with open(input_fn, encoding='utf-8') as f:
21 | doc = pf.load(f)
22 |
23 | print('Dumping JSON...')
24 | with open(output_fn, mode='w', encoding='utf-8') as f:
25 | pf.dump(doc, f)
26 | f.write('\n')
27 |
28 | print(' - Done!')
29 |
30 |
31 | print('\nComparing...')
32 |
33 | with open(input_fn, encoding='utf-8') as f:
34 | input_data = f.read()
35 |
36 | with open(output_fn, encoding='utf-8') as f:
37 | output_data = f.read()
38 |
39 | print('Are both files the same?')
40 | print(' - Length:', len(input_data) == len(output_data), len(input_data), len(output_data))
41 | print(' - Content:', input_data == output_data)
42 |
43 | print('\nApplying trivial filter...')
44 | doc = doc.walk(action=empty_test, doc=doc)
45 | print(' - Done!')
46 |
47 | print(' - Dumping JSON...')
48 | with open(output_fn, mode='w', encoding='utf-8') as f:
49 | pf.dump(doc, f)
50 | f.write('\n')
51 | print(' - Done!')
52 | print(' - Comparing...')
53 | with open(input_fn, encoding='utf-8') as f:
54 | input_data = f.read()
55 | with open(output_fn, encoding='utf-8') as f:
56 | output_data = f.read()
57 | print(' - Are both files the same?')
58 | print(' - Length:', len(input_data) == len(output_data), len(input_data), len(output_data))
59 | print(' - Content:', input_data == output_data)
60 |
61 |
62 | assert input_data == output_data
63 |
--------------------------------------------------------------------------------
/tests/input/barcode/test.py:
--------------------------------------------------------------------------------
1 | import panflute as pf
2 |
3 | input_fn = 'benchmark.json'
4 | output_fn = 'panflute.json'
5 |
6 |
7 | def empty_test(element, doc):
8 | return
9 |
10 | def test_filter(element, doc):
11 | if type(element)==pf.Header:
12 | return []
13 | if type(element)==pf.Str:
14 | element.text = element.text + '!!'
15 | return element
16 |
17 |
18 | print('\nLoading JSON...')
19 |
20 | with open(input_fn, encoding='utf-8') as f:
21 | doc = pf.load(f)
22 |
23 | print('Dumping JSON...')
24 | with open(output_fn, mode='w', encoding='utf-8') as f:
25 | pf.dump(doc, f)
26 | f.write('\n')
27 |
28 | print(' - Done!')
29 |
30 |
31 | print('\nComparing...')
32 |
33 | with open(input_fn, encoding='utf-8') as f:
34 | input_data = f.read()
35 |
36 | with open(output_fn, encoding='utf-8') as f:
37 | output_data = f.read()
38 |
39 | print('Are both files the same?')
40 | print(' - Length:', len(input_data) == len(output_data), len(input_data), len(output_data))
41 | print(' - Content:', input_data == output_data)
42 |
43 | print('\nApplying trivial filter...')
44 | doc = doc.walk(action=empty_test, doc=doc)
45 | print(' - Done!')
46 |
47 | print(' - Dumping JSON...')
48 | with open(output_fn, mode='w', encoding='utf-8') as f:
49 | pf.dump(doc, f)
50 | f.write('\n')
51 | print(' - Done!')
52 | print(' - Comparing...')
53 | with open(input_fn, encoding='utf-8') as f:
54 | input_data = f.read()
55 | with open(output_fn, encoding='utf-8') as f:
56 | output_data = f.read()
57 | print(' - Are both files the same?')
58 | print(' - Length:', len(input_data) == len(output_data), len(input_data), len(output_data))
59 | print(' - Content:', input_data == output_data)
60 |
61 |
62 | assert input_data == output_data
63 |
--------------------------------------------------------------------------------
/tests/input/portugal/test.py:
--------------------------------------------------------------------------------
1 | import panflute as pf
2 |
3 | input_fn = 'benchmark.json'
4 | output_fn = 'panflute.json'
5 |
6 |
7 | def empty_test(element, doc):
8 | return
9 |
10 | def test_filter(element, doc):
11 | if type(element)==pf.Header:
12 | return []
13 | if type(element)==pf.Str:
14 | element.text = element.text + '!!'
15 | return element
16 |
17 |
18 | print('\nLoading JSON...')
19 |
20 | with open(input_fn, encoding='utf-8') as f:
21 | doc = pf.load(f)
22 |
23 | print('Dumping JSON...')
24 | with open(output_fn, mode='w', encoding='utf-8') as f:
25 | pf.dump(doc, f)
26 | f.write('\n')
27 |
28 | print(' - Done!')
29 |
30 |
31 | print('\nComparing...')
32 |
33 | with open(input_fn, encoding='utf-8') as f:
34 | input_data = f.read()
35 |
36 | with open(output_fn, encoding='utf-8') as f:
37 | output_data = f.read()
38 |
39 | print('Are both files the same?')
40 | print(' - Length:', len(input_data) == len(output_data), len(input_data), len(output_data))
41 | print(' - Content:', input_data == output_data)
42 |
43 | print('\nApplying trivial filter...')
44 | doc = doc.walk(action=empty_test, doc=doc)
45 | print(' - Done!')
46 |
47 | print(' - Dumping JSON...')
48 | with open(output_fn, mode='w', encoding='utf-8') as f:
49 | pf.dump(doc, f)
50 | f.write('\n')
51 | print(' - Done!')
52 | print(' - Comparing...')
53 | with open(input_fn, encoding='utf-8') as f:
54 | input_data = f.read()
55 | with open(output_fn, encoding='utf-8') as f:
56 | output_data = f.read()
57 | print(' - Are both files the same?')
58 | print(' - Length:', len(input_data) == len(output_data), len(input_data), len(output_data))
59 | print(' - Content:', input_data == output_data)
60 |
61 |
62 | assert input_data == output_data
63 |
--------------------------------------------------------------------------------
/examples/panflute/build-table.py:
--------------------------------------------------------------------------------
1 | from panflute import *
2 |
3 |
4 | def add_table_without_header(options, data, element, doc):
5 | cells = ['', 'Quality', 'Age', 'Sex', 'Memo']
6 | cells = [TableCell(Plain(Str(cell))) for cell in cells]
7 | row = TableRow(*cells)
8 |
9 | body = TableBody(row)
10 |
11 | width = [0.16, 0.16, 0.16, 0.16, 0.16]
12 | alignment = ['AlignDefault'] * len(width)
13 | caption = 'This table should not have a header'
14 | caption = Caption(Para(Str(caption)))
15 | return Div(Table(body, colspec=zip(alignment, width), caption=caption))
16 |
17 |
18 | def add_table_with_only_header(options, data, element, doc):
19 | cells = ['', 'Quality', 'Age', 'Sex', 'Memo']
20 | cells = [TableCell(Plain(Str(cell))) for cell in cells]
21 | row = TableRow(*cells)
22 | head = TableHead(row)
23 | width = [0.16, 0.16, 0.16, 0.16, 0.16]
24 | alignment = ['AlignDefault'] * len(width)
25 | caption = 'This table should only have a header; and no rows'
26 | caption = Caption(Plain(Str(caption)))
27 | return Div(Table(head=head, colspec=zip(alignment, width), caption=caption))
28 |
29 |
30 | def finalize(doc):
31 | doc.walk(view_table_info, doc=doc)
32 |
33 |
34 | def view_table_info(e, doc):
35 | if isinstance(e, Table):
36 | debug('[TABLE]')
37 | debug('Cols: ', e.cols)
38 | debug('Head :', e.head is not None)
39 | debug('Foot :', e.foot is not None)
40 | debug('Caption:', stringify(e.caption))
41 | debug()
42 |
43 |
44 | def main(doc=None):
45 | d = {'table_without_header': add_table_without_header,
46 | 'table_only_header': add_table_with_only_header}
47 | return run_filter(yaml_filter, tags=d, doc=doc, finalize=finalize)
48 |
49 |
50 | if __name__ == '__main__':
51 | main()
52 |
--------------------------------------------------------------------------------
/examples/pandocfilters/abc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Pandoc filter to process code blocks with class "abc" containing
5 | ABC notation into images. Assumes that abcm2ps and ImageMagick's
6 | convert are in the path. Images are put in the abc-images directory.
7 | """
8 |
9 | import hashlib
10 | import os
11 | import sys
12 | from pandocfilters import toJSONFilter, Para, Image
13 | from subprocess import Popen, PIPE, call
14 |
15 | imagedir = "abc-images"
16 |
17 |
18 | def sha1(x):
19 | return hashlib.sha1(x.encode(sys.getfilesystemencoding())).hexdigest()
20 |
21 |
22 | def abc2eps(abc, filetype, outfile):
23 | p = Popen(["abcm2ps", "-O", outfile + '.eps', "-"], stdin=PIPE)
24 | p.stdin.write(abc)
25 | p.communicate()
26 | p.stdin.close()
27 | call(["convert", outfile + '.eps', outfile + '.' + filetype])
28 |
29 |
30 | def abc(key, value, format, meta):
31 | if key == 'CodeBlock':
32 | [[ident, classes, keyvals], code] = value
33 | if "abc" in classes:
34 | outfile = imagedir + '/' + sha1(code)
35 | if format == "html":
36 | filetype = "png"
37 | elif format == "latex":
38 | filetype = "pdf"
39 | else:
40 | filetype = "png"
41 | src = outfile + '.' + filetype
42 | if not os.path.isfile(src):
43 | try:
44 | os.mkdir(imagedir)
45 | sys.stderr.write('Created directory ' + imagedir + '\n')
46 | except OSError:
47 | pass
48 | abc2eps(code.encode("utf-8"), filetype, outfile)
49 | sys.stderr.write('Created image ' + src + '\n')
50 | return Para([Image(['', [], []], [], [src, ""])])
51 |
52 | if __name__ == "__main__":
53 | toJSONFilter(abc)
54 |
--------------------------------------------------------------------------------
/tests/sample_files/fenced/example.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Some title
3 | author: Some author
4 | note: this is standard markdown metadata
5 | ---
6 |
7 | This file contains fenced code blocks that can be used
8 | to test the panflute code that deals with YAML code blocks,
9 | as discussed [here](http://scorreia.com/software/panflute/guide.html#yaml-code-blocks)
10 |
11 | # Standard examples
12 |
13 | ## Just raw data
14 |
15 | ~~~ spam
16 | ---
17 | raw text
18 | ~~~
19 |
20 | ~~~ spam
21 | ...
22 | raw text
23 | ~~~
24 |
25 | ## Just YAML
26 |
27 | ~~~ spam
28 | foo: bar
29 | bacon: True
30 | ~~~
31 |
32 | ## Both
33 |
34 | ~~~ spam
35 | foo: bar
36 | bacon: True
37 | ---
38 | raw text
39 | ~~~
40 |
41 | ~~~ spam
42 | foo: bar
43 | bacon: True
44 | ...
45 | raw text
46 | ~~~
47 |
48 | ## Longer delimiters
49 |
50 | ~~~ spam
51 | foo: bar
52 | bacon: True
53 | .......
54 | raw text
55 | ~~~
56 |
57 | # Strict-YAML examples
58 |
59 | ## Just raw data
60 |
61 | ~~~ eggs
62 | raw text
63 | ~~~
64 |
65 | ~~~ eggs
66 | ---
67 | ...
68 | raw text
69 | ~~~
70 |
71 | ~~~ eggs
72 | raw text
73 | ---
74 | ---
75 | ~~~
76 |
77 | ~~~ eggs
78 | ---
79 | ...
80 | raw text
81 | ---
82 | ...
83 | ~~~
84 |
85 | ## Just YAML
86 |
87 | ~~~ eggs
88 | ---
89 | foo: bar
90 | bacon: True
91 | ~~~
92 |
93 | ~~~ eggs
94 | ---
95 | foo: bar
96 | bacon: True
97 | ...
98 | ~~~
99 |
100 | ## Both
101 |
102 | ~~~ eggs
103 | ---
104 | foo: bar
105 | bacon: True
106 | ---
107 | raw text
108 | ~~~
109 |
110 | ~~~ eggs
111 | ---
112 | foo: bar
113 | bacon: True
114 | ...
115 | raw text
116 | ~~~
117 |
118 | ## Longer delimiters
119 |
120 | ~~~ eggs
121 | ---
122 | foo: bar
123 | bacon: True
124 | -----
125 | raw text
126 | ~~~
127 |
128 | ## Both; metadata interlinked
129 |
130 | ~~~ eggs
131 | raw1
132 | ---
133 | foo: bar
134 | ...
135 | raw2
136 | ---
137 | spam: eggs
138 | ---
139 | this
140 | ...
141 | is
142 | ...
143 | all raw
144 | ~~~
145 |
--------------------------------------------------------------------------------
/tests/test_fenced.py:
--------------------------------------------------------------------------------
1 | # To create the JSON file, run
2 | # pandoc --smart --parse-raw --to=json fenced/input.md > fenced/input.json
3 |
4 | import panflute as pf
5 | from pathlib import Path
6 |
7 |
8 | def fenced_action(options, data, element, doc):
9 | bar = options.get('foo')
10 | assert bar is None or bar == 'bar'
11 | assert not data or data == 'raw text' or \
12 | data == """raw1\nraw2\nthis\n...\nis\n...\nall raw"""
13 | # assert bar or data, (bar,data)
14 | return
15 |
16 |
17 | def test_all():
18 |
19 | fn = Path("./tests/sample_files/fenced/example.md")
20 | print(f'\n - Loading markdown "{fn}"')
21 | with fn.open(encoding='utf-8') as f:
22 | markdown_text = f.read()
23 | print(' - Converting Markdown to JSON')
24 | json_pandoc = pf.convert_text(markdown_text, input_format='markdown', output_format='json', standalone=True)
25 | print(' - Constructing Doc() object')
26 | doc = pf.convert_text(json_pandoc, input_format='json', output_format='panflute', standalone=True)
27 |
28 | print(' - Applying YAML filter...')
29 | pf.run_filter(pf.yaml_filter, tag='spam', function=fenced_action, doc=doc)
30 | json_panflute = pf.convert_text(doc, input_format='panflute', output_format='json', standalone=True)
31 | print(' Are both JSON files equal?')
32 | print(f' - Length: {len(json_pandoc) == len(json_panflute)} ({len(json_pandoc)} vs {len(json_panflute)})')
33 | print(f' - Content: {json_pandoc == json_panflute}')
34 | assert json_pandoc == json_panflute
35 |
36 | print(' - Applying Strict YAML filter...')
37 | pf.run_filter(pf.yaml_filter, tag='eggs', function=fenced_action, doc=doc, strict_yaml=True)
38 | json_panflute = pf.convert_text(doc, input_format='panflute', output_format='json', standalone=True)
39 | print(' Are both JSON files equal?')
40 | print(f' - Length: {len(json_pandoc) == len(json_panflute)} ({len(json_pandoc)} vs {len(json_panflute)})')
41 | print(f' - Content: {json_pandoc == json_panflute}')
42 | assert json_pandoc == json_panflute
43 |
44 | print(' - Done!')
45 |
46 |
47 | if __name__ == "__main__":
48 | test_all()
49 |
--------------------------------------------------------------------------------
/examples/pandocfilters/plantuml.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Pandoc filter to process code blocks with class "plantuml" into
5 | plant-generated images.
6 | """
7 |
8 | import hashlib
9 | import os
10 | import sys
11 | from pandocfilters import toJSONFilter, Str, Para, Image
12 | from subprocess import call
13 |
14 |
15 | imagedir = "plantuml-images"
16 |
17 | def sha1(x):
18 | return hashlib.sha1(x.encode(sys.getfilesystemencoding())).hexdigest()
19 |
20 |
21 | def filter_keyvalues(kv):
22 | res = []
23 | caption = []
24 | for k,v in kv:
25 | if k == u"caption":
26 | caption = [ Str(v) ]
27 | else:
28 | res.append( [k,v] )
29 |
30 | return caption, "fig:" if caption else "", res
31 |
32 |
33 | def plantuml(key, value, format, meta):
34 | if key == 'CodeBlock':
35 | [[ident, classes, keyvals], code] = value
36 |
37 | if "plantuml" in classes:
38 | caption, typef, keyvals = filter_keyvalues(keyvals)
39 |
40 | filename = sha1(code)
41 | if format == "html":
42 | filetype = "svg"
43 | elif format == "latex":
44 | filetype = "eps"
45 | else:
46 | filetype = "png"
47 | src = os.path.join(imagedir, filename + '.uml')
48 | dest = os.path.join(imagedir, filename + '.' + filetype)
49 |
50 | if not os.path.isfile(dest):
51 | try:
52 | os.mkdir(imagedir)
53 | sys.stderr.write('Created directory ' + imagedir + '\n')
54 | except OSError:
55 | pass
56 |
57 | txt = code.encode("utf-8")
58 | if not txt.startswith("@start"):
59 | txt = "@startuml\n" + txt + "\n@enduml\n"
60 | with open(src, "w") as f:
61 | f.write(txt)
62 |
63 | call(["java", "-jar", "plantuml.jar", "-t"+filetype, src])
64 |
65 | sys.stderr.write('Created image ' + dest + '\n')
66 |
67 | return Para([Image([ident, [], keyvals], caption, [dest, typef])])
68 |
69 | if __name__ == "__main__":
70 | toJSONFilter(plantuml)
71 |
--------------------------------------------------------------------------------
/examples/panflute/tikz.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Pandoc filter to process raw latex tikz environments into images.
5 | Assumes that pdflatex is in the path, and that the standalone
6 | package is available. Also assumes that ImageMagick's convert
7 | is in the path. Images are put in the tikz-images directory.
8 | """
9 |
10 | import hashlib
11 | import re
12 | import os
13 | import sys
14 | import shutil
15 | from panflute import toJSONFilter, Para, Image, RawBlock
16 | from subprocess import Popen, PIPE, call
17 | from tempfile import mkdtemp
18 |
19 | imagedir = "tikz-images"
20 |
21 |
22 | def sha1(x):
23 | return hashlib.sha1(x.encode(sys.getfilesystemencoding())).hexdigest()
24 |
25 |
26 | def tikz2image(tikz, filetype, outfile):
27 | tmpdir = mkdtemp()
28 | olddir = os.getcwd()
29 | os.chdir(tmpdir)
30 | f = open('tikz.tex', 'w')
31 | f.write("""\\documentclass{standalone}
32 | \\usepackage{tikz}
33 | \\begin{document}
34 | """)
35 | f.write(tikz)
36 | f.write("\n\\end{document}\n")
37 | f.close()
38 | p = call(["pdflatex", 'tikz.tex'], stdout=sys.stderr)
39 | os.chdir(olddir)
40 | if filetype == 'pdf':
41 | shutil.copyfile(tmpdir + '/tikz.pdf', outfile + '.pdf')
42 | else:
43 | call(["convert", tmpdir + '/tikz.pdf', outfile + '.' + filetype])
44 | shutil.rmtree(tmpdir)
45 |
46 |
47 | def tikz(elem, doc):
48 | if type(elem) == RawBlock and elem.format == "latex":
49 | code = elem.text
50 | if re.match("\\\\begin{tikzpicture}", code):
51 | outfile = imagedir + '/' + sha1(code)
52 | filetype = {'html': 'png', 'latex': 'pdf'}.get(doc.format, 'png')
53 | src = outfile + '.' + filetype
54 | if not os.path.isfile(src):
55 | try:
56 | os.mkdir(imagedir)
57 | sys.stderr.write('Created directory ' + imagedir + '\n')
58 | except OSError:
59 | pass
60 | tikz2image(code, filetype, outfile)
61 | sys.stderr.write('Created image ' + src + '\n')
62 | return Para(Image(url=src))
63 |
64 |
65 | if __name__ == "__main__":
66 | toJSONFilter(tikz)
67 |
--------------------------------------------------------------------------------
/docs/source/example.html:
--------------------------------------------------------------------------------
1 |
2 | Some emphasized, bold and striken out text.
3 |
4 | $include(../ch2/invalid)
5 | $include this $include is invalid
6 |
7 | This will be included to the root element
8 | !include ../ch2/invalid
9 | $include ../ch2/a random valid file.md
10 | $include "quotes here"
11 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 | | Price |
23 | 10 |
24 |
25 |
26 | | Weight |
27 | 12 |
28 |
29 |
30 |
31 | lorem lorem..
32 |
33 |
34 |
38 |
39 |
40 |
41 | | Price |
42 | 10 |
43 |
44 |
45 | | Price |
46 | 10 |
47 |
48 |
49 | | Price |
50 | 10 |
51 |
52 |
53 | | Weight |
54 | 12 |
55 |
56 |
57 |
58 |
59 | Tables go here
60 | $tables
61 | Fenced tables...
62 | title: Some Title
63 | has-header: True
64 | ---
65 | Col1, Col2, Col3
66 | 1, 2, 3
67 | 10, 20, 30
68 | Something else here
69 | Stack Overflow
70 | pizza
71 | What is Pandoc? Pandoc
72 | the end..
73 |
--------------------------------------------------------------------------------
/DISTRIBUTION.md:
--------------------------------------------------------------------------------
1 | # PyPI Distribution Instructions
2 |
3 | ## Guides
4 |
5 | - https://packaging.python.org/distributing/#upload-your-distributions
6 | - http://peterdowns.com/posts/first-time-with-pypi.html
7 |
8 |
9 | ## Instructions
10 |
11 | First you may want to test a local install and test it:
12 |
13 | ```
14 | python setup.py install
15 | py.test
16 | ```
17 |
18 | Then,
19 |
20 | 1. Copy .pypirc file from backup (if required, as it's not synced to git)
21 | 2. Test it: `python setup.py sdist upload -r pypitest`
22 | 3. Run it live:
23 |
24 | ```
25 | pandoc README.md --output=README.rst && python setup.py sdist upload -r pypi
26 | ```
27 |
28 | ## Documentation
29 |
30 | To run *and* update docs and website, run:
31 |
32 | ```
33 | cd docs && make.bat html && cd .. && cd ../website && jekyll build && s3_website push && cd ../panflute
34 | ```
35 |
36 | ## PDF Documentation
37 |
38 | To build the pdf version (slow), install miktex or similar and run:
39 |
40 | ```
41 | cd docs && make.bat latex && cd build && cd latex && Makefile && cd
42 | ```
43 |
44 | (On Windows, replace `Makefile` with a few runs of `pdflatex Panflute.tex`)
45 |
46 | Then copy the resulting PDF into the Panflute folder of the website.
47 |
48 |
49 | ## Unit Tests and Code Coverage
50 |
51 | To run unit tests locally and check code coverage, run:
52 |
53 | ```
54 | pytest --cov=panflute tests && coverage html && cd htmlcov && index.html && cd ..
55 | ```
56 |
57 | This requires a development environment,
58 | which can be installed from the repository's root directory using:
59 |
60 | ```shell
61 | pip install --editable ".[dev]"
62 | ```
63 |
64 |
65 | ## Pushing to PyPI through Twine
66 |
67 | First, ensure that you have `twine` installed and the checks pass:
68 |
69 | ```bash
70 | cls && python setup.py sdist && twine check dist/*
71 | ```
72 |
73 | Then try the test PyPI repository:
74 |
75 | ```bash
76 | twine upload --repository-url https://test.pypi.org/legacy/ dist/* --verbose
77 | ```
78 |
79 | Finally update to the official repo:
80 |
81 | ```bash
82 | twine upload dist/*
83 | ```
84 |
85 | ### Possible errors
86 |
87 | #### `warning: `long_description_content_type` missing. defaulting to `text/x-rst`.`
88 |
89 | Solution: ensure that files have Unix line endings (not Windows)
90 |
--------------------------------------------------------------------------------
/examples/pandocfilters/tikz.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Pandoc filter to process raw latex tikz environments into images.
5 | Assumes that pdflatex is in the path, and that the standalone
6 | package is available. Also assumes that ImageMagick's convert
7 | is in the path. Images are put in the tikz-images directory.
8 | """
9 |
10 | import hashlib
11 | import re
12 | import os
13 | import sys
14 | import shutil
15 | from pandocfilters import toJSONFilter, Para, Image
16 | from subprocess import Popen, PIPE, call
17 | from tempfile import mkdtemp
18 |
19 | imagedir = "tikz-images"
20 |
21 |
22 | def sha1(x):
23 | return hashlib.sha1(x.encode(sys.getfilesystemencoding())).hexdigest()
24 |
25 |
26 | def tikz2image(tikz, filetype, outfile):
27 | tmpdir = mkdtemp()
28 | olddir = os.getcwd()
29 | os.chdir(tmpdir)
30 | f = open('tikz.tex', 'w')
31 | f.write("""\\documentclass{standalone}
32 | \\usepackage{tikz}
33 | \\begin{document}
34 | """)
35 | f.write(tikz)
36 | f.write("\n\\end{document}\n")
37 | f.close()
38 | p = call(["pdflatex", 'tikz.tex'], stdout=sys.stderr)
39 | os.chdir(olddir)
40 | if filetype == 'pdf':
41 | shutil.copyfile(tmpdir + '/tikz.pdf', outfile + '.pdf')
42 | else:
43 | call(["convert", tmpdir + '/tikz.pdf', outfile + '.' + filetype])
44 | shutil.rmtree(tmpdir)
45 |
46 |
47 | def tikz(key, value, format, meta):
48 | if key == 'RawBlock':
49 | [fmt, code] = value
50 | if fmt == "latex" and re.match("\\\\begin{tikzpicture}", code):
51 | outfile = imagedir + '/' + sha1(code)
52 | if format == "html":
53 | filetype = "png"
54 | elif format == "latex":
55 | filetype = "pdf"
56 | else:
57 | filetype = "png"
58 | src = outfile + '.' + filetype
59 | if not os.path.isfile(src):
60 | try:
61 | os.mkdir(imagedir)
62 | sys.stderr.write('Created directory ' + imagedir + '\n')
63 | except OSError:
64 | pass
65 | tikz2image(code, filetype, outfile)
66 | sys.stderr.write('Created image ' + src + '\n')
67 | return Para([Image(['', [], []], [], [src, ""])])
68 |
69 | if __name__ == "__main__":
70 | toJSONFilter(tikz)
71 |
--------------------------------------------------------------------------------
/tests/sample_files/native/students.native:
--------------------------------------------------------------------------------
1 | [Table ("students",[],[("source","mdn")]) (Caption Nothing
2 | [Para [Str "List",Space,Str "of",Space,Str "Students"]])
3 | [(AlignLeft,ColWidth 0.5)
4 | ,(AlignLeft,ColWidth 0.5)]
5 | (TableHead ("",[],[])
6 | [Row ("",[],[])
7 | [Cell ("",[],[]) AlignCenter (RowSpan 1) (ColSpan 1)
8 | [Plain [Str "Student",Space,Str "ID"]]
9 | ,Cell ("",[],[]) AlignCenter (RowSpan 1) (ColSpan 1)
10 | [Plain [Str "Name"]]]])
11 | [(TableBody ("",["souvereign-states"],[]) (RowHeadColumns 0)
12 | [Row ("",[],[])
13 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 2)
14 | [Plain [Str "Computer",Space,Str "Science"]]]]
15 | [Row ("",[],[])
16 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
17 | [Plain [Str "3741255"]]
18 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
19 | [Plain [Str "Jones,",Space,Str "Martha"]]]
20 | ,Row ("",[],[])
21 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
22 | [Plain [Str "4077830"]]
23 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
24 | [Plain [Str "Pierce,",Space,Str "Benjamin"]]]
25 | ,Row ("",[],[])
26 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
27 | [Plain [Str "5151701"]]
28 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
29 | [Plain [Str "Kirk,",Space,Str "James"]]]])
30 | ,(TableBody ("",[],[]) (RowHeadColumns 0)
31 | [Row ("",[],[])
32 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 2)
33 | [Plain [Str "Russian",Space,Str "Literature"]]]]
34 | [Row ("",[],[])
35 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
36 | [Plain [Str "3971244"]]
37 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
38 | [Plain [Str "Nim,",Space,Str "Victor"]]]])
39 | ,(TableBody ("",[],[]) (RowHeadColumns 0)
40 | [Row ("",[],[])
41 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 2)
42 | [Plain [Str "Astrophysics"]]]]
43 | [Row ("",[],[])
44 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
45 | [Plain [Str "4100332"]]
46 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
47 | [Plain [Str "Petrov,",Space,Str "Alexandra"]]]
48 | ,Row ("",[],[])
49 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
50 | [Plain [Str "4100332"]]
51 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
52 | [Plain [Str "Toyota,",Space,Str "Hiroko"]]]])]
53 | (TableFoot ("",[],[])
54 | [])]
55 |
--------------------------------------------------------------------------------
/tests/sample_files/heavy_metadata/example.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Lorem Ipsum: Title"
3 | author:
4 | - name: John Smith
5 | affiliation: Some University, Some School
6 | email: example@example.com
7 | date: Dec 2016
8 | thanks: Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.
9 | keywords: [Tag1, Tag2, Tag3]
10 | jel: [G1, G2, G3]
11 |
12 | abstract: |
13 | Bring to *the* **table** win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.
14 | published: Draft
15 | comments:
16 | - Foo
17 | - Bar
18 | toc: false
19 | lof: false
20 | lot: false
21 | format:
22 | cmdline: 'somestring'
23 | show-titlepage: true
24 | show-published: true
25 | elegant-title: true
26 | thin-margins: true
27 | show-frame: false
28 | show-media: true
29 | media-in-back: false # true
30 | media-pagebreak: true
31 | linestretch: 1.213
32 | estimates:
33 | input-path: "../../../out/Regression"
34 | output-path: "../estimates-appendix"
35 | update-tex: false
36 | classoption:
37 | - 12pt
38 | numbersections: true
39 | bibliography: '../../References/all.bib'
40 | csl: chicago-author-date
41 | key1:
42 | key1-1:
43 | - value1-1-1
44 | - value1-1-2
45 | key1-2:
46 | - value1-2-1
47 | - value1-2-2
48 | key2:
49 | value1-2
50 | amsthm:
51 | plain:
52 | - Theorem: Lemma
53 | - With Space*
54 | definition:
55 | - Definition
56 | remark:
57 | - Case
58 | parentcounter:
59 | - chapter
60 | cmd: pandoc --smart --parse-raw --to=json index.md > benchmark.json
61 | ...
62 |
63 | # Title
64 |
65 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
66 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
67 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
68 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
69 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
70 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
71 |
--------------------------------------------------------------------------------
/examples/input/lilypond-score.ly:
--------------------------------------------------------------------------------
1 | \version "2.18"
2 | \language "français"
3 |
4 | \header {
5 | tagline = ""
6 | composer = ""
7 | }
8 |
9 | MetriqueArmure = {
10 | \tempo 2.=50
11 | \time 6/4
12 | \key sib \major
13 | }
14 |
15 | italique = { \override Score . LyricText #'font-shape = #'italic }
16 |
17 | roman = { \override Score . LyricText #'font-shape = #'roman }
18 |
19 | MusiqueCouplet = \relative do' {
20 | \partial 2. re4\p re^"Solo" re
21 | sol2. la2 la4
22 | sib2 sib4 \breathe
23 | la4 sib la
24 | sol2. \acciaccatura {la16[ sol]} fad2 sol4
25 | la2 r4 re,2 re4
26 | sol2 sol4 la\< sol la
27 | sib2\! \acciaccatura {la16[ sol]} fa4 \breathe sib2 do4
28 | re2 do4 sol2 la4
29 | sib2. ~ sib2 \bar "||"
30 | }
31 |
32 | MusiqueRefrainI = \relative do'' {
33 | re4\f^"Chœur"
34 | re2 do4 sib2 la4
35 | sol2. fad2 \breathe re4
36 | sol2 la4 sib2 do4
37 | re2.~ re4 \oneVoice r \voiceOne re\f
38 | re2 do4 sib2 la4
39 | sol2. fad2 \oneVoice r4 \voiceOne
40 | sol2 la4\< sib la sol\!
41 | la2. sib2( la4)
42 | sol2.\fermata \bar "|."
43 | }
44 |
45 | MusiqueRefrainII = \relative do'' {
46 | sib4
47 | sib2 la4 sol2 re4
48 | mib4 re dod re2 do4
49 | sib2 re4 sol2 sol4
50 | fad2.~ fad4 s sib4
51 | sib2 la4 sol2 re4
52 | mib4 re dod re2 s4
53 | sib2 do4 re do sib
54 | do2. re2( do4)
55 | sib2.
56 | }
57 |
58 | ParolesCouplet = \lyricmode {
59 | Le soir é -- tend sur la Ter -- re
60 | Son grand man -- teau de ve -- lours,
61 | Et le camp, calme et so -- li -- tai -- re,
62 | Se re -- cueille en ton a -- mour.
63 | }
64 |
65 | ParolesRefrain = \lyricmode {
66 | \italique
67 | Ô Vier -- ge de lu -- miè -- re,
68 | É -- toi -- le de nos cœurs,
69 | En -- tends no -- tre pri -- è -- re,
70 | No -- tre_- Da -- me des É -- clai -- reurs_!
71 | }
72 |
73 | \score{
74 | <<
75 | \new Staff <<
76 | \set Staff.midiInstrument = "flute"
77 | \set Staff.autoBeaming = ##f
78 | \new Voice = "couplet" {
79 | \override Score.PaperColumn #'keep-inside-line = ##t
80 | \MetriqueArmure
81 | \MusiqueCouplet
82 | \voiceOne
83 | \MusiqueRefrainI
84 | }
85 | \new Voice = "refrainII" {
86 | s4*50
87 | \voiceTwo
88 | \MusiqueRefrainII
89 | }
90 | >>
91 | \new Lyrics \lyricsto couplet {
92 | \ParolesCouplet
93 | \ParolesRefrain
94 | }
95 | >>
96 | \layout{}
97 | \midi{}
98 | }
99 |
--------------------------------------------------------------------------------
/examples/make.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from shutil import which
4 | from subprocess import Popen, PIPE, call
5 |
6 |
7 | def shell(args, msg=None):
8 | # Fix Windows error if passed a string
9 | proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
10 | out, err = proc.communicate(input=msg)
11 | exitcode = proc.returncode
12 | if exitcode!=0:
13 | print('\n------', file=sys.stderr)
14 | print(err.decode('utf-8'), file=sys.stderr, end='')
15 | print('------\n', file=sys.stderr)
16 | raise IOError
17 | return out
18 |
19 |
20 | def build_cmds(fn):
21 | pandoc_path = which('pandoc')
22 | input_fn = './input/' + os.path.splitext(fn)[0] + '-sample.md'
23 | cmds = []
24 | for path in ('pandocfilters', 'panflute'):
25 | filter_fn = './{}/{}'.format(path, fn)
26 | cmds.append([pandoc_path, '-F', filter_fn, input_fn])
27 | return cmds
28 |
29 |
30 | def main():
31 | print('Verify that the panflute filters are the same as'
32 | 'those in pandocfilters.py:')
33 |
34 | pandoc_filters = os.listdir('./pandocfilters')
35 | panflute_filters = os.listdir('./panflute')
36 | excluded = ('abc.py', 'plantuml.py', 'tikz', 'gabc') # GABC requires installing miktex packages...
37 |
38 | for fn in pandoc_filters:
39 | if fn in panflute_filters and not fn.startswith('__') and fn not in excluded:
40 | print(' - Testing', fn)
41 | benchmark_cmd, panflute_cmd = build_cmds(fn)
42 |
43 | print(' [CMD]', ' '.join(benchmark_cmd))
44 | benchmark = shell(benchmark_cmd).decode('utf-8')
45 | print(' [CMD]', ' '.join(panflute_cmd))
46 | panflute = shell(panflute_cmd).decode('utf-8')
47 |
48 | print(' are both files the same?')
49 | print(' ... length?', len(benchmark) == len(panflute),
50 | len(benchmark), len(panflute))
51 | print(' ... content?', benchmark == panflute)
52 |
53 | if benchmark != panflute:
54 | with open('benchmark_output.html', encoding='utf-8', mode='w') as f:
55 | f.write(benchmark)
56 | with open('panflute_output.html', encoding='utf-8', mode='w') as f:
57 | f.write(panflute)
58 |
59 | print('\n\n!!! Not equal.. check why!\n')
60 | if fn not in ('metavars.py',):
61 | raise Exception
62 |
63 | print()
64 |
65 |
66 | if __name__ == '__main__':
67 | main()
68 |
--------------------------------------------------------------------------------
/tests/test_walk.py:
--------------------------------------------------------------------------------
1 | """
2 | Test how Element.walk() behaves with different return types of action functions
3 | """
4 |
5 | import panflute as pf
6 |
7 |
8 | def compare_docs(doc_a, doc_b):
9 | doc_a_json = pf.convert_text(doc_a,
10 | input_format='panflute',
11 | output_format='json',
12 | standalone=True)
13 | doc_b_json = pf.convert_text(doc_b,
14 | input_format='panflute',
15 | output_format='json',
16 | standalone=True)
17 | return doc_a_json == doc_b_json
18 |
19 |
20 | """
21 | Action functions to use in testing
22 | """
23 |
24 |
25 | # Action that always returns None, changing nothing
26 | def do_nothing(elem, doc):
27 | return None
28 |
29 |
30 | # Action that returns an empty list, deleting pf.Str elements
31 | def remove_elem(elem, doc):
32 | if isinstance(elem, pf.Str):
33 | return []
34 |
35 |
36 | # Action that returns a single inline element, writing over pf.Str elements
37 | def inline_replace_elem(elem, doc):
38 | if isinstance(elem, pf.Str):
39 | return pf.Str("b")
40 |
41 |
42 | # Action that returns a list of inline elements, writing over pf.Str elements
43 | def inline_replace_list(elem, doc):
44 | if isinstance(elem, pf.Str):
45 | return [pf.Str("a"), pf.Space, pf.Str("b")]
46 |
47 |
48 | # Action that returns a single inline element, writing over pf.Para elements
49 | def block_replace_elem(elem, doc):
50 | if isinstance(elem, pf.Para):
51 | return pf.CodeBlock("b")
52 |
53 |
54 | # Action that returns a list of block elements, writing over pf.Para elements
55 | def block_replace_list(elem, doc):
56 | if isinstance(elem, pf.Para):
57 | return [pf.Para(pf.Str("a")), pf.Para(pf.Str("b"))]
58 |
59 |
60 | """
61 | Test functions for above action functions
62 | """
63 |
64 |
65 | def test_none():
66 | in_doc = expected_doc = pf.Doc(pf.Para(pf.Str("a")))
67 | in_doc.walk(do_nothing)
68 | assert compare_docs(in_doc, expected_doc)
69 |
70 |
71 | def test_empty_list():
72 | in_doc = pf.Doc(pf.Para(pf.Str("a"), pf.Space))
73 | in_doc.walk(remove_elem)
74 | expected_doc = pf.Doc(pf.Para(pf.Space))
75 | assert compare_docs(in_doc, expected_doc)
76 |
77 | def test_inline_elem():
78 | in_doc = pf.Doc(pf.Para(pf.Str("a")))
79 | in_doc.walk(inline_replace_elem)
80 | expected_doc = pf.Doc(pf.Para(pf.Str("b")))
81 | assert compare_docs(in_doc, expected_doc)
82 |
83 | def test_inline_list():
84 | in_doc = pf.Doc(pf.Para(pf.Str("a")))
85 | in_doc.walk(inline_replace_list)
86 | expected_doc = pf.Doc(pf.Para(pf.Str("a"), pf.Space, pf.Str("b")))
87 | assert compare_docs(in_doc, expected_doc)
88 |
89 |
90 | def test_block_elem():
91 | in_doc = pf.Doc(pf.Para(pf.Str("a")))
92 | in_doc.walk(block_replace_elem)
93 | expected_doc = pf.Doc(pf.CodeBlock("b"))
94 | assert compare_docs(in_doc, expected_doc)
95 |
96 |
97 | def test_block_list():
98 | in_doc = pf.Doc(pf.Para(pf.Str("c")))
99 | in_doc.walk(block_replace_list)
100 | expected_doc = pf.Doc(pf.Para(pf.Str("a")), pf.Para(pf.Str("b")))
101 | assert compare_docs(in_doc, expected_doc)
102 |
--------------------------------------------------------------------------------
/tests/sample_files/native/nordics.native:
--------------------------------------------------------------------------------
1 | [Table ("nordics",[],[("source","wikipedia")]) (Caption (Just [Str "Nordic",Space,Str "countries"])
2 | [Para [Str "States",Space,Str "belonging",Space,Str "to",Space,Str "the",Space,Emph [Str "Nordics."]]])
3 | [(AlignCenter,ColWidth 0.3)
4 | ,(AlignLeft,ColWidth 0.3)
5 | ,(AlignLeft,ColWidth 0.2)
6 | ,(AlignLeft,ColWidth 0.2)]
7 | (TableHead ("",["simple-head"],[])
8 | [Row ("",[],[])
9 | [Cell ("",[],[]) AlignCenter (RowSpan 1) (ColSpan 1)
10 | [Plain [Str "Name"]]
11 | ,Cell ("",[],[]) AlignCenter (RowSpan 1) (ColSpan 1)
12 | [Plain [Str "Capital"]]
13 | ,Cell ("",[],[]) AlignCenter (RowSpan 1) (ColSpan 1)
14 | [Plain [Str "Population",LineBreak,Str "(in",Space,Str "2018)"]]
15 | ,Cell ("",[],[]) AlignCenter (RowSpan 1) (ColSpan 1)
16 | [Plain [Str "Area",LineBreak,Str "(in",Space,Str "km",Superscript [Str "2"],Str ")"]]]])
17 | [(TableBody ("",["souvereign-states"],[]) (RowHeadColumns 1)
18 | []
19 | [Row ("",["country"],[])
20 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
21 | [Plain [Str "Denmark"]]
22 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
23 | [Plain [Str "Copenhagen"]]
24 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
25 | [Plain [Str "5,809,502"]]
26 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
27 | [Plain [Str "43,094"]]]
28 | ,Row ("",["country"],[])
29 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
30 | [Plain [Str "Finland"]]
31 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
32 | [Plain [Str "Helsinki"]]
33 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
34 | [Plain [Str "5,537,364"]]
35 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
36 | [Plain [Str "338,145"]]]
37 | ,Row ("",["country"],[])
38 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
39 | [Plain [Str "Iceland"]]
40 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
41 | [Plain [Str "Reykjavik"]]
42 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
43 | [Plain [Str "343,518"]]
44 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
45 | [Plain [Str "103,000"]]]
46 | ,Row ("",["country"],[])
47 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
48 | [Plain [Str "Norway"]]
49 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
50 | [Plain [Str "Oslo"]]
51 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
52 | [Plain [Str "5,372,191"]]
53 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
54 | [Plain [Str "323,802"]]]
55 | ,Row ("",["country"],[])
56 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
57 | [Plain [Str "Sweden"]]
58 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
59 | [Plain [Str "Stockholm"]]
60 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
61 | [Plain [Str "10,313,447"]]
62 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
63 | [Plain [Str "450,295"]]]])]
64 | (TableFoot ("",[],[])
65 | [Row ("summary",[],[])
66 | [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
67 | [Plain [Str "Total"]]
68 | ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
69 | []
70 | ,Cell ("total-population",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
71 | [Plain [Str "27,376,022"]]
72 | ,Cell ("total-area",[],[]) AlignDefault (RowSpan 1) (ColSpan 1)
73 | [Plain [Str "1,258,336"]]]])]
74 |
--------------------------------------------------------------------------------
/tests/fenced/input.json:
--------------------------------------------------------------------------------
1 | {"pandoc-api-version":[1,17,0,4],"meta":{"author":{"t":"MetaInlines","c":[{"t":"Str","c":"Some"},{"t":"Space"},{"t":"Str","c":"author"}]},"title":{"t":"MetaInlines","c":[{"t":"Str","c":"Some"},{"t":"Space"},{"t":"Str","c":"title"}]},"note":{"t":"MetaInlines","c":[{"t":"Str","c":"this"},{"t":"Space"},{"t":"Str","c":"is"},{"t":"Space"},{"t":"Str","c":"standard"},{"t":"Space"},{"t":"Str","c":"markdown"},{"t":"Space"},{"t":"Str","c":"metadata"}]}},"blocks":[{"t":"Para","c":[{"t":"Str","c":"This"},{"t":"Space"},{"t":"Str","c":"file"},{"t":"Space"},{"t":"Str","c":"contains"},{"t":"Space"},{"t":"Str","c":"fenced"},{"t":"Space"},{"t":"Str","c":"code"},{"t":"Space"},{"t":"Str","c":"blocks"},{"t":"Space"},{"t":"Str","c":"that"},{"t":"Space"},{"t":"Str","c":"can"},{"t":"Space"},{"t":"Str","c":"be"},{"t":"Space"},{"t":"Str","c":"used"},{"t":"SoftBreak"},{"t":"Str","c":"to"},{"t":"Space"},{"t":"Str","c":"test"},{"t":"Space"},{"t":"Str","c":"the"},{"t":"Space"},{"t":"Str","c":"panflute"},{"t":"Space"},{"t":"Str","c":"code"},{"t":"Space"},{"t":"Str","c":"that"},{"t":"Space"},{"t":"Str","c":"deals"},{"t":"Space"},{"t":"Str","c":"with"},{"t":"Space"},{"t":"Str","c":"YAML"},{"t":"Space"},{"t":"Str","c":"code"},{"t":"Space"},{"t":"Str","c":"blocks,"},{"t":"SoftBreak"},{"t":"Str","c":"as"},{"t":"Space"},{"t":"Str","c":"discussed"},{"t":"Space"},{"t":"Link","c":[["",[],[]],[{"t":"Str","c":"here"}],["http://scorreia.com/software/panflute/guide.html#yaml-code-blocks",""]]}]},{"t":"Header","c":[1,["standard-examples",[],[]],[{"t":"Str","c":"Standard"},{"t":"Space"},{"t":"Str","c":"examples"}]]},{"t":"Header","c":[2,["just-raw-data",[],[]],[{"t":"Str","c":"Just"},{"t":"Space"},{"t":"Str","c":"raw"},{"t":"Space"},{"t":"Str","c":"data"}]]},{"t":"CodeBlock","c":[["",["spam"],[]],"---\nraw text"]},{"t":"CodeBlock","c":[["",["spam"],[]],"...\nraw text"]},{"t":"Header","c":[2,["just-yaml",[],[]],[{"t":"Str","c":"Just"},{"t":"Space"},{"t":"Str","c":"YAML"}]]},{"t":"CodeBlock","c":[["",["spam"],[]],"foo: bar\nbacon: True"]},{"t":"Header","c":[2,["both",[],[]],[{"t":"Str","c":"Both"}]]},{"t":"CodeBlock","c":[["",["spam"],[]],"foo: bar\nbacon: True\n---\nraw text"]},{"t":"CodeBlock","c":[["",["spam"],[]],"foo: bar\nbacon: True\n...\nraw text"]},{"t":"Header","c":[2,["longer-delimiters",[],[]],[{"t":"Str","c":"Longer"},{"t":"Space"},{"t":"Str","c":"delimiters"}]]},{"t":"CodeBlock","c":[["",["spam"],[]],"foo: bar\nbacon: True\n.......\nraw text"]},{"t":"Header","c":[1,["strict-yaml-examples",[],[]],[{"t":"Str","c":"Strict-YAML"},{"t":"Space"},{"t":"Str","c":"examples"}]]},{"t":"Header","c":[2,["just-raw-data-1",[],[]],[{"t":"Str","c":"Just"},{"t":"Space"},{"t":"Str","c":"raw"},{"t":"Space"},{"t":"Str","c":"data"}]]},{"t":"CodeBlock","c":[["",["eggs"],[]],"raw text"]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\n...\nraw text"]},{"t":"CodeBlock","c":[["",["eggs"],[]],"raw text\n---\n---"]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\n...\nraw text\n---\n..."]},{"t":"Header","c":[2,["just-yaml-1",[],[]],[{"t":"Str","c":"Just"},{"t":"Space"},{"t":"Str","c":"YAML"}]]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\nfoo: bar\nbacon: True"]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\nfoo: bar\nbacon: True\n..."]},{"t":"Header","c":[2,["both-1",[],[]],[{"t":"Str","c":"Both"}]]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\nfoo: bar\nbacon: True\n---\nraw text"]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\nfoo: bar\nbacon: True\n...\nraw text"]},{"t":"Header","c":[2,["longer-delimiters-1",[],[]],[{"t":"Str","c":"Longer"},{"t":"Space"},{"t":"Str","c":"delimiters"}]]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\nfoo: bar\nbacon: True\n-----\nraw text"]},{"t":"Header","c":[2,["both-metadata-interlinked",[],[]],[{"t":"Str","c":"Both;"},{"t":"Space"},{"t":"Str","c":"metadata"},{"t":"Space"},{"t":"Str","c":"interlinked"}]]},{"t":"CodeBlock","c":[["",["eggs"],[]],"raw1\n---\nfoo: bar\n...\nraw2\n---\nspam: eggs\n---\nthis\n...\nis\n...\nall raw"]}]}
2 |
--------------------------------------------------------------------------------
/tests/fenced/output.json:
--------------------------------------------------------------------------------
1 | {"pandoc-api-version":[1,17,0,4],"meta":{"author":{"t":"MetaInlines","c":[{"t":"Str","c":"Some"},{"t":"Space"},{"t":"Str","c":"author"}]},"title":{"t":"MetaInlines","c":[{"t":"Str","c":"Some"},{"t":"Space"},{"t":"Str","c":"title"}]},"note":{"t":"MetaInlines","c":[{"t":"Str","c":"this"},{"t":"Space"},{"t":"Str","c":"is"},{"t":"Space"},{"t":"Str","c":"standard"},{"t":"Space"},{"t":"Str","c":"markdown"},{"t":"Space"},{"t":"Str","c":"metadata"}]}},"blocks":[{"t":"Para","c":[{"t":"Str","c":"This"},{"t":"Space"},{"t":"Str","c":"file"},{"t":"Space"},{"t":"Str","c":"contains"},{"t":"Space"},{"t":"Str","c":"fenced"},{"t":"Space"},{"t":"Str","c":"code"},{"t":"Space"},{"t":"Str","c":"blocks"},{"t":"Space"},{"t":"Str","c":"that"},{"t":"Space"},{"t":"Str","c":"can"},{"t":"Space"},{"t":"Str","c":"be"},{"t":"Space"},{"t":"Str","c":"used"},{"t":"SoftBreak"},{"t":"Str","c":"to"},{"t":"Space"},{"t":"Str","c":"test"},{"t":"Space"},{"t":"Str","c":"the"},{"t":"Space"},{"t":"Str","c":"panflute"},{"t":"Space"},{"t":"Str","c":"code"},{"t":"Space"},{"t":"Str","c":"that"},{"t":"Space"},{"t":"Str","c":"deals"},{"t":"Space"},{"t":"Str","c":"with"},{"t":"Space"},{"t":"Str","c":"YAML"},{"t":"Space"},{"t":"Str","c":"code"},{"t":"Space"},{"t":"Str","c":"blocks,"},{"t":"SoftBreak"},{"t":"Str","c":"as"},{"t":"Space"},{"t":"Str","c":"discussed"},{"t":"Space"},{"t":"Link","c":[["",[],[]],[{"t":"Str","c":"here"}],["http://scorreia.com/software/panflute/guide.html#yaml-code-blocks",""]]}]},{"t":"Header","c":[1,["standard-examples",[],[]],[{"t":"Str","c":"Standard"},{"t":"Space"},{"t":"Str","c":"examples"}]]},{"t":"Header","c":[2,["just-raw-data",[],[]],[{"t":"Str","c":"Just"},{"t":"Space"},{"t":"Str","c":"raw"},{"t":"Space"},{"t":"Str","c":"data"}]]},{"t":"CodeBlock","c":[["",["spam"],[]],"---\nraw text"]},{"t":"CodeBlock","c":[["",["spam"],[]],"...\nraw text"]},{"t":"Header","c":[2,["just-yaml",[],[]],[{"t":"Str","c":"Just"},{"t":"Space"},{"t":"Str","c":"YAML"}]]},{"t":"CodeBlock","c":[["",["spam"],[]],"foo: bar\nbacon: True"]},{"t":"Header","c":[2,["both",[],[]],[{"t":"Str","c":"Both"}]]},{"t":"CodeBlock","c":[["",["spam"],[]],"foo: bar\nbacon: True\n---\nraw text"]},{"t":"CodeBlock","c":[["",["spam"],[]],"foo: bar\nbacon: True\n...\nraw text"]},{"t":"Header","c":[2,["longer-delimiters",[],[]],[{"t":"Str","c":"Longer"},{"t":"Space"},{"t":"Str","c":"delimiters"}]]},{"t":"CodeBlock","c":[["",["spam"],[]],"foo: bar\nbacon: True\n.......\nraw text"]},{"t":"Header","c":[1,["strict-yaml-examples",[],[]],[{"t":"Str","c":"Strict-YAML"},{"t":"Space"},{"t":"Str","c":"examples"}]]},{"t":"Header","c":[2,["just-raw-data-1",[],[]],[{"t":"Str","c":"Just"},{"t":"Space"},{"t":"Str","c":"raw"},{"t":"Space"},{"t":"Str","c":"data"}]]},{"t":"CodeBlock","c":[["",["eggs"],[]],"raw text"]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\n...\nraw text"]},{"t":"CodeBlock","c":[["",["eggs"],[]],"raw text\n---\n---"]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\n...\nraw text\n---\n..."]},{"t":"Header","c":[2,["just-yaml-1",[],[]],[{"t":"Str","c":"Just"},{"t":"Space"},{"t":"Str","c":"YAML"}]]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\nfoo: bar\nbacon: True"]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\nfoo: bar\nbacon: True\n..."]},{"t":"Header","c":[2,["both-1",[],[]],[{"t":"Str","c":"Both"}]]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\nfoo: bar\nbacon: True\n---\nraw text"]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\nfoo: bar\nbacon: True\n...\nraw text"]},{"t":"Header","c":[2,["longer-delimiters-1",[],[]],[{"t":"Str","c":"Longer"},{"t":"Space"},{"t":"Str","c":"delimiters"}]]},{"t":"CodeBlock","c":[["",["eggs"],[]],"---\nfoo: bar\nbacon: True\n-----\nraw text"]},{"t":"Header","c":[2,["both-metadata-interlinked",[],[]],[{"t":"Str","c":"Both;"},{"t":"Space"},{"t":"Str","c":"metadata"},{"t":"Space"},{"t":"Str","c":"interlinked"}]]},{"t":"CodeBlock","c":[["",["eggs"],[]],"raw1\n---\nfoo: bar\n...\nraw2\n---\nspam: eggs\n---\nthis\n...\nis\n...\nall raw"]}]}
2 |
--------------------------------------------------------------------------------
/examples/panflute/lilypond.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | """
4 | Pandoc filter to process code blocks with class "ly" containing
5 | Lilypond notation. Assumes that Lilypond and Ghostscript are
6 | installed, plus [lyluatex](https://github.com/jperon/lyluatex) package for
7 | LaTeX, with LuaLaTeX.
8 | """
9 |
10 | import os
11 | from sys import getfilesystemencoding, stderr
12 | from subprocess import Popen, call, PIPE
13 | from hashlib import sha1
14 | from panflute import toJSONFilter, Para, Image, RawInline, RawBlock, Code, CodeBlock
15 |
16 | IMAGEDIR = "tmp_ly"
17 | LATEX_DOC = """\\documentclass{article}
18 | \\usepackage{libertine}
19 | \\usepackage{lyluatex}
20 | \\pagestyle{empty}
21 | \\begin{document}
22 | %s
23 | \\end{document}
24 | """
25 |
26 |
27 | def sha(x):
28 | return sha1(x.encode(getfilesystemencoding())).hexdigest()
29 |
30 |
31 | def latex(code):
32 | """LaTeX inline"""
33 | return RawInline(code, format='latex')
34 |
35 |
36 | def latexblock(code):
37 | """LaTeX block"""
38 | return RawBlock(code, format='latex')
39 |
40 |
41 | def ly2png(lily, outfile, staffsize):
42 | p = Popen([
43 | "lilypond",
44 | "-dno-point-and-click",
45 | "-dbackend=eps",
46 | "-djob-count=2",
47 | "-ddelete-intermediate-files",
48 | "-o", outfile,
49 | "-"
50 | ], stdin=PIPE, stdout=-3)
51 | p.stdin.write(("\\paper{\n"
52 | "indent=0\\mm\n"
53 | "oddFooterMarkup=##f\n"
54 | "oddHeaderMarkup=##f\n"
55 | "bookTitleMarkup = ##f\n"
56 | "scoreTitleMarkup = ##f\n"
57 | "}\n"
58 | "#(set-global-staff-size %s)\n" % staffsize +
59 | lily).encode("utf-8"))
60 | p.communicate()
61 | p.stdin.close()
62 | call([
63 | "gs",
64 | "-sDEVICE=pngalpha",
65 | "-r144",
66 | "-sOutputFile=" + outfile + '.png',
67 | outfile + '.pdf',
68 | ], stdout=-3)
69 |
70 |
71 | def png(contents, staffsize):
72 | """Creates a png if needed."""
73 | outfile = os.path.join(IMAGEDIR, sha(contents + str(staffsize)))
74 | src = outfile + '.png'
75 | if not os.path.isfile(src):
76 | try:
77 | os.mkdir(IMAGEDIR)
78 | stderr.write('Created directory ' + IMAGEDIR + '\n')
79 | except OSError:
80 | pass
81 | ly2png(contents, outfile, staffsize)
82 | stderr.write('Created image ' + src + '\n')
83 | return src
84 |
85 |
86 | def lily(elem, doc):
87 | if type(elem) == Code and 'ly' in elem.classes:
88 | staffsize = int(elem.attributes.get('staffsize', '20'))
89 | if doc.format == "latex":
90 | if elem.identifier == "":
91 | label = ""
92 | else:
93 | label = '\\label{' + elem.identifier + '}'
94 | return latex(
95 | '\\includely[staffsize=%s]{%s}' % (staffsize, contents) +
96 | label
97 | )
98 | else:
99 | infile = contents + (
100 | '.ly' if '.ly' not in contents else ''
101 | )
102 | with open(infile, 'r') as doc:
103 | code = doc.read()
104 | return Image(url=png(code, staffsize))
105 |
106 | if type(elem) == CodeBlock and 'ly' in elem.classes:
107 | staffsize = int(elem.attributes.get('staffsize', '20'))
108 | if doc.format == "latex":
109 | if elem.identifier == "":
110 | label = ""
111 | else:
112 | label = '\\label{' + elem.identifier + '}'
113 | return latexblock(
114 | '\\lily[staffsize=%s]{%s}' % (staffsize, code) +
115 | label
116 | )
117 | else:
118 | return Para(Image(url=png(code, staffsize)))
119 |
120 | if __name__ == "__main__":
121 | toJSONFilter(lily)
122 |
--------------------------------------------------------------------------------
/tests/sample_files/pandoc-2.11/example.md:
--------------------------------------------------------------------------------
1 | Test api changes for python 2.10
2 |
3 | To run (on a windows install), go to the `examples\panflute` folder and type:
4 |
5 | ```
6 | cls & pandoc --filter=pandoc-2.10.py ../input/pandoc-2.10.md
7 | ```
8 |
9 | - See: https://github.com/jgm/pandoc/releases/tag/2.10
10 | - See: https://pandoc.org/lua-filters.html (diff it)
11 |
12 | # Support underline tag
13 |
14 | [this text will be underlined]{.ul}
15 |
16 | *This text will not be underlined* but **this text will be**.
17 |
18 |
19 | # Tables
20 |
21 | Will add examples from [https://pandoc.org/MANUAL.html#tables]
22 |
23 | ## Simple tables
24 |
25 | Example:
26 |
27 | | Variable | Mean |
28 | |----------|------|
29 | | Price | 10 |
30 | | Weight | 12 |
31 |
32 |
33 |
34 | ## Tables with alignment
35 |
36 | Right Left Center Default
37 | ------- ------ ---------- -------
38 | 12 12 12 12
39 | 123 123 123 123
40 | 1 1 1 1
41 |
42 | Table: Demonstration of simple table syntax.
43 |
44 |
45 | ## Table 2
46 |
47 | ------- ------ ---------- -------
48 | 12 12 12 12
49 | 123 123 123 123
50 | 1 1 1 1
51 | ------- ------ ---------- -------
52 |
53 |
54 |
55 | ## Multiline table
56 |
57 | -------------------------------------------------------------
58 | Centered Default Right Left
59 | Header Aligned Aligned Aligned
60 | ----------- ------- --------------- -------------------------
61 | First row 12.0 Example of a row that
62 | spans multiple lines.
63 |
64 | Second row 5.0 Here's another one. Note
65 | the blank line between
66 | rows.
67 | -------------------------------------------------------------
68 |
69 | Table: Here's the caption. It, too, may span
70 | multiple lines.
71 |
72 |
73 | ## Multiline without a header
74 |
75 | ----------- ------- --------------- -------------------------
76 | First row 12.0 Example of a row that
77 | spans multiple lines.
78 |
79 | Second row 5.0 Here's another one. Note
80 | the blank line between
81 | rows.
82 | ----------- ------- --------------- -------------------------
83 |
84 | : Here's a multiline table without a header.
85 |
86 |
87 | ## Grid table
88 |
89 | : Sample grid table.
90 |
91 | +---------------+---------------+--------------------+
92 | | Fruit | Price | Advantages |
93 | +===============+===============+====================+
94 | | Bananas | $1.34 | - built-in wrapper |
95 | | | | - bright color |
96 | +---------------+---------------+--------------------+
97 | | Oranges | $2.10 | - cures scurvy |
98 | | | | - tasty |
99 | +---------------+---------------+--------------------+
100 |
101 |
102 | ## Aligned grid table
103 |
104 | +---------------+---------------+--------------------+
105 | | Right | Left | Centered |
106 | +==============:+:==============+:==================:+
107 | | Bananas | $1.34 | built-in wrapper |
108 | +---------------+---------------+--------------------+
109 |
110 | ## Aligned headerless table
111 |
112 | +--------------:+:--------------+:------------------:+
113 | | Right | Left | Centered |
114 | +---------------+---------------+--------------------+
115 |
116 | ## Pipe table
117 |
118 | | Right | Left | Default | Center |
119 | |------:|:-----|---------|:------:|
120 | | 12 | 12 | 12 | 12 |
121 | | 123 | 123 | 123 | 123 |
122 | | 1 | 1 | 1 | 1 |
123 |
124 | : Demonstration of pipe table syntax.
125 |
126 |
--------------------------------------------------------------------------------
/tests/test_basics.py:
--------------------------------------------------------------------------------
1 | '''
2 | Test that running panflute through a markdown file has no effect on the output
3 |
4 | For each markdown test file, this runs:
5 | a) pandoc --> json
6 | b) json --> panflute --> json
7 |
8 | And verifies that the outputs of a) and b) are the same
9 | '''
10 |
11 |
12 | import json
13 | from pathlib import Path
14 | import panflute as pf
15 |
16 |
17 | def test_idempotence():
18 | example_files = list(Path('./tests/sample_files').glob('*/example.md'))
19 | print(f'Testing idempotence ({len(example_files)} files):')
20 |
21 | for fn in example_files:
22 | print(f'\n - Loading markdown "{fn}"')
23 | with fn.open(encoding='utf-8') as f:
24 | markdown_text = f.read()
25 |
26 | print(' - Converting markdown to JSON')
27 | json_pandoc = pf.convert_text(markdown_text, input_format='markdown', output_format='json', standalone=True)
28 |
29 | print(' - Constructing Doc() object')
30 | doc = pf.convert_text(json_pandoc, input_format='json', output_format='panflute', standalone=True)
31 |
32 | print(' - Converting Doc() to JSON...')
33 | json_panflute = pf.convert_text(doc, input_format='panflute', output_format='json', standalone=True)
34 |
35 | print(' - Are both JSON files equal?')
36 | print(f' - Length: {len(json_pandoc) == len(json_panflute)} ({len(json_pandoc)} vs {len(json_panflute)})')
37 | print(f' - Content: {json_pandoc == json_panflute}')
38 | assert json_pandoc == json_panflute
39 |
40 | print(' - Running filter that does nothing...')
41 | doc = doc.walk(action=empty_test, doc=doc)
42 | json_panflute = pf.convert_text(doc, input_format='panflute', output_format='json', standalone=True)
43 | print(' - Are both JSON files equal?')
44 | print(f' - Length: {len(json_pandoc) == len(json_panflute)} ({len(json_pandoc)} vs {len(json_panflute)})')
45 | print(f' - Content: {json_pandoc == json_panflute}')
46 | assert json_pandoc == json_panflute
47 |
48 |
49 | def test_idempotence_of_native():
50 | example_files = list(Path('./tests/sample_files/native').glob('*.native'))
51 | print(f'Testing idempotence ({len(example_files)} native files):')
52 |
53 | for fn in example_files:
54 | print(f'\n - Loading native files "{fn}"')
55 | with fn.open(encoding='utf-8') as f:
56 | markdown_text = f.read()
57 |
58 | print(' - Converting native to JSON')
59 | json_pandoc = pf.convert_text(markdown_text, input_format='native', output_format='json', standalone=True)
60 |
61 | print(' - Constructing Doc() object')
62 | doc = pf.convert_text(json_pandoc, input_format='json', output_format='panflute', standalone=True)
63 |
64 | print(' - Converting Doc() to JSON...')
65 | json_panflute = pf.convert_text(doc, input_format='panflute', output_format='json', standalone=True)
66 |
67 | print(' - Are both JSON files equal?')
68 | print(f' - Length: {len(json_pandoc) == len(json_panflute)} ({len(json_pandoc)} vs {len(json_panflute)})')
69 | print(f' - Content: {json_pandoc == json_panflute}')
70 | assert json_pandoc == json_panflute
71 |
72 | print(' - Running filter that does nothing...')
73 | doc = doc.walk(action=empty_test, doc=doc)
74 | json_panflute = pf.convert_text(doc, input_format='panflute', output_format='json', standalone=True)
75 | print(' - Are both JSON files equal?')
76 | print(f' - Length: {len(json_pandoc) == len(json_panflute)} ({len(json_pandoc)} vs {len(json_panflute)})')
77 | print(f' - Content: {json_pandoc == json_panflute}')
78 | assert json_pandoc == json_panflute
79 |
80 |
81 | def empty_test(element, doc):
82 | return
83 |
84 |
85 | def test_stringify():
86 | markdown_text = '''Hello **world**! *How* are ~you~ doing?'''
87 | expected_text = '''Hello world! How are you doing?\n\n'''
88 |
89 | doc = pf.convert_text(markdown_text, input_format='markdown', output_format='panflute', standalone=True)
90 | output_text = pf.stringify(doc)
91 |
92 | assert expected_text == output_text
93 |
94 |
95 |
96 | if __name__ == "__main__":
97 | test_idempotence_of_native()
98 | test_idempotence()
99 | test_stringify()
100 |
--------------------------------------------------------------------------------
/examples/pandocfilters/lilypond.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | """
4 | Pandoc filter to process code blocks with class "ly" containing
5 | Lilypond notation. Assumes that Lilypond and Ghostscript are
6 | installed, plus [lyluatex](https://github.com/jperon/lyluatex) package for
7 | LaTeX, with LuaLaTeX.
8 | """
9 |
10 | import os
11 | from sys import getfilesystemencoding, stderr
12 | from subprocess import Popen, call, PIPE
13 | from hashlib import sha1
14 | from pandocfilters import toJSONFilter, Para, Image, RawInline, RawBlock
15 |
16 | IMAGEDIR = "tmp_ly"
17 | LATEX_DOC = """\\documentclass{article}
18 | \\usepackage{libertine}
19 | \\usepackage{lyluatex}
20 | \\pagestyle{empty}
21 | \\begin{document}
22 | %s
23 | \\end{document}
24 | """
25 |
26 |
27 | def sha(x):
28 | return sha1(x.encode(getfilesystemencoding())).hexdigest()
29 |
30 |
31 | def latex(code):
32 | """LaTeX inline"""
33 | return RawInline('latex', code)
34 |
35 |
36 | def latexblock(code):
37 | """LaTeX block"""
38 | return RawBlock('latex', code)
39 |
40 |
41 | def ly2png(lily, outfile, staffsize):
42 | p = Popen([
43 | "lilypond",
44 | "-dno-point-and-click",
45 | "-dbackend=eps",
46 | "-djob-count=2",
47 | "-ddelete-intermediate-files",
48 | "-o", outfile,
49 | "-"
50 | ], stdin=PIPE, stdout=-3)
51 | p.stdin.write(("\\paper{\n"
52 | "indent=0\\mm\n"
53 | "oddFooterMarkup=##f\n"
54 | "oddHeaderMarkup=##f\n"
55 | "bookTitleMarkup = ##f\n"
56 | "scoreTitleMarkup = ##f\n"
57 | "}\n"
58 | "#(set-global-staff-size %s)\n" % staffsize +
59 | lily).encode("utf-8"))
60 | p.communicate()
61 | p.stdin.close()
62 | call([
63 | "gs",
64 | "-sDEVICE=pngalpha",
65 | "-r144",
66 | "-sOutputFile=" + outfile + '.png',
67 | outfile + '.pdf',
68 | ], stdout=-3)
69 |
70 |
71 | def png(contents, staffsize):
72 | """Creates a png if needed."""
73 | outfile = os.path.join(IMAGEDIR, sha(contents + str(staffsize)))
74 | src = outfile + '.png'
75 | if not os.path.isfile(src):
76 | try:
77 | os.mkdir(IMAGEDIR)
78 | stderr.write('Created directory ' + IMAGEDIR + '\n')
79 | except OSError:
80 | pass
81 | ly2png(contents, outfile, staffsize)
82 | stderr.write('Created image ' + src + '\n')
83 | return src
84 |
85 |
86 | def lily(key, value, fmt, meta):
87 | if key == 'Code':
88 | [[ident, classes, kvs], contents] = value # pylint:disable=I0011,W0612
89 | kvs = {key: value for key, value in kvs}
90 | if "ly" in classes:
91 | staffsize = kvs['staffsize'] if 'staffsize' in kvs else 20
92 | if fmt == "latex":
93 | if ident == "":
94 | label = ""
95 | else:
96 | label = '\\label{' + ident + '}'
97 | return latex(
98 | '\\includely[staffsize=%s]{%s}' % (staffsize, contents) +
99 | label
100 | )
101 | else:
102 | infile = contents + (
103 | '.ly' if '.ly' not in contents else ''
104 | )
105 | with open(infile, 'r') as doc:
106 | code = doc.read()
107 | return [
108 | Image(['', [], []], [], [png(code, staffsize), ""])
109 | ]
110 | if key == 'CodeBlock':
111 | [[ident, classes, kvs], code] = value
112 | kvs = {key: value for key, value in kvs}
113 | if "ly" in classes:
114 | staffsize = kvs['staffsize'] if 'staffsize' in kvs else 20
115 | if fmt == "latex":
116 | if ident == "":
117 | label = ""
118 | else:
119 | label = '\\label{' + ident + '}'
120 | return latexblock(
121 | '\\lily[staffsize=%s]{%s}' % (staffsize, code) +
122 | label
123 | )
124 | else:
125 | return Para([Image(['', [], []], [], [png(code, staffsize), ""])])
126 |
127 | if __name__ == "__main__":
128 | toJSONFilter(lily)
129 |
--------------------------------------------------------------------------------
/misc/CLI_Wrapper.md:
--------------------------------------------------------------------------------
1 | # Some notes on CLI wrappers for Pandoc
2 |
3 | There are three active and one inactive CLI wrappers for pandoc:
4 |
5 | 1. [`pandocomatic`](https://heerdebeer.org/Software/markdown/pandocomatic/) (Ruby)
6 | 2. [`panrun`](https://github.com/mb21/panrun) (Ruby; powers [panwriter](https://panwriter.com/))
7 | 3. [`rmarkdown`](https://rmarkdown.rstudio.com/) (R; powers [bookdown](https://bookdown.org/) and RStudio)
8 | 4. [`panzer`](https://github.com/msprev/panzer) (Python; inactive)
9 |
10 | They mostly have two goals:
11 |
12 | 1. Avoid having to type *long* Pandoc command line calls that need to be remembered every time, and instead replace them with the equivalent of `make`. This means that all the options need to be either stored within the YAML metadata of the document, or in a separate YAML file, so it can be used through multiple documents.
13 | 2. Extending Pandoc by adding preprocessing, custom filters, postprocessing, etc.
14 |
15 | Is it worth it to add a new one? To avoid the [standard proliferation problem](https://xkcd.com/927/), we need to know if there is something we need that can't be done with the three active tools, or by maintaining other ones such as `panzer`. Also, even if we add a new CLI wrapper, it would be good to avoid reinventing the wheel altogether.
16 |
17 | Also, pandoc constantly adds [features](https://github.com/jgm/pandoc/issues/5870) that reduce the need of CLI wrappers.
18 |
19 |
20 | # Current tools
21 |
22 | *(disclaimer: this is a relatively shallow evaluation of the current tools for my personal purposes, so take it with ~~one~~ two grains of salt)*
23 |
24 | ## `pandocomatic`
25 |
26 | - Supports pre/post processors
27 | - Supports common yaml files. **(ISSUE?)** Why are the files so nested? (`templates` contains `templatename`, which contains `pandoc`, which contains the actual key-value metadata)
28 | - Supports adding settings to yaml header. **(ISSUE?)** again, settings need to be nested within `pandocmatic_`-`pandoc`, and seem verbose (why `use-template` instead of `template`? also templates can be confused with the Pandoc templates)
29 | - Supports running Pandoc on many files (e.g. in a website), including running all files in a folder, only modified files, etc. However, I have no need for that.
30 | - Pandocomatic itself can be configured through a YAML file to avoid typing its command line arguments. Seems useful, but perhaps a bit too meta.
31 |
32 | ## `panrun`
33 |
34 | - Minimalistic; aims to be "a simple script", so it's not too complex to understand but doesn't support pre/post processors.
35 | - Seems to use the `output` key (instead of `pandocmatic_`) and within it there are subkeys for each output type (html, latex, etc.)
36 | - The first metadata key (e.g. html) will be used as default output format, but this can be changed through the `--to` option.
37 | - Pandoc options are passed-through, but others are silently ignored. **(ISSUE?)** What about typos?
38 |
39 | ## `panzer`
40 |
41 | - Worked through the `style` metadata field; multiple styles allowed.
42 | - Allowed much more than just pandoc command line arguments: pre/post processing, latex/beamer pre/post flight, cleanup, inheritance through `parent` field
43 |
44 |
45 | ## `rmarkdown`
46 |
47 | - See: https://bookdown.org/yihui/rmarkdown/pdf-document.html
48 | - Essentially the key feature is interleaving of R code and prose.
49 | - Not very useful in my case, where code can be long (100s of lines) and take long to run (hours)
50 |
51 |
52 | # Best of both worlds approach
53 |
54 |
55 | ## YAML blocks
56 |
57 | - Inheritance? not through YAML anchors (too complicated, no one knows how to use them) but through a `extends` field (better than `parent` or `inherits`)
58 | - Sample YAML blocks:
59 |
60 | ```yaml
61 | author: John Smith
62 | title: Some Title
63 | panflute:
64 | filters: ['fix-tables', 'include-files']
65 | style: arxiv
66 | ```
67 |
68 | ```yaml
69 | author: John Smith
70 | title: Some Title
71 | panflute:
72 | filters: ['fix-tables', 'include-files']
73 | pandoc:
74 | - include-in-header: xyz.tex
75 | - preserve-tabs: true
76 | ```
77 |
78 |
79 | ## CLI Options
80 |
81 | - `view`: show in PDF viewer
82 | - `watch`: watch the file and re-run as needed
83 | - `verbose`: display debugging information
84 | - `tex`: save tex file in addition of the PDF output
85 |
86 | ## Defaults:
87 |
88 | - By default, `standalone` will be true, output will be PDF
89 |
90 |
91 |
92 | ## Misc:
93 |
94 | - Should we have an output: format for the default output type? like rmarkdown
95 | - The rmarkdown extensions are also quite useful: https://bookdown.org/yihui/rmarkdown/bookdown-markdown.html#bookdown-markdown
96 | - Theorem YAML codeblocks
97 | - Cross-referencing
98 |
99 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. panflute documentation master file, created by
2 | sphinx-quickstart on Tue Apr 26 22:17:57 2016.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | .. automodule:: panflute
7 |
8 | *(Documentation last updated for panflute |version|)*
9 |
10 | It is a pythonic alternative to John MacFarlane's
11 | `pandocfilters `_,
12 | from which it is heavily inspired.
13 |
14 | To use it, write a function that works on Pandoc elements
15 | and call it through `run_filter `_::
16 |
17 | from panflute import *
18 |
19 | def increase_header_level(elem, doc):
20 | if type(elem) == Header:
21 | if elem.level < 6:
22 | elem.level += 1
23 | else:
24 | return [] # Delete headers already in level 6
25 |
26 | def main(doc=None):
27 | return run_filter(increase_header_level, doc=doc)
28 |
29 | if __name__ == "__main__":
30 | main()
31 |
32 |
33 | Motivation
34 | ====================================
35 |
36 | Our goal is to make writing pandoc filters *as simple and clear as possible*. Starting from pandocfilters, we make it pythonic, add error and type checking, and include batteries for common tasks. In more detail:
37 |
38 | 1. Pythonic
39 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40 |
41 | - Elements are easier to modify. For instance, to change the level of a
42 | header, you can do ``header.level += 1`` instead of ``header['c'][0] += 1``.
43 | To change the identifier, do ``header.identifier = 'spam'`` instead of
44 | ``header['c'][1][1] = 'spam'``
45 | - Elements are easier to create. Thus, to create a header you can do
46 | ``Header(Str(The), Space, Str(Title), level=1, identifier=foo)``
47 | instead of ``Header([1,["foo",[],[]],[{"t":"Str","c":"The"},{"t":"Space","c":[]},{"t":"Str","c":"Title"}])``
48 | - You can navigate across elements. Thus, you can check if ``isinstance(elem.parent, Inline)`` or if ``type(elem.next) == Space``
49 |
50 | 2. Detects common mistakes
51 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
52 |
53 | - Check that the elements contain the correct types. Trying to create
54 | `Para('text')` will give you the error "Para() element must contain Inlines
55 | but received a str()", instead of just failing silently when running the
56 | filter.
57 |
58 | 3. Comes with batteries included
59 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
60 |
61 | - Convert markdown and other formatted strings into python objects or
62 | other formats, with the
63 | `convert_text(text, input_format, output_format)` function (which calls
64 | Pandoc internally)
65 | - Use code blocks to hold YAML options and other data (such as CSV) with
66 | `yaml_filter(element, doc, tag, function)`.
67 | - Called external programs to fetch results with `shell()`.
68 | - Modifying the entire document (e.g. moving all the figures and tables to the
69 | back of a PDF) is easy, thanks to the `prepare` and `finalize`
70 | options of `run_filter`, and to the `replace_keyword` function
71 | - Convenience elements such as `TableRow` and `TableCell` allow for easier
72 | filters.
73 | - Panflute can be run as a filter itself, in which case it will run all
74 | filters listed in the metadata field `panflute-filters`.
75 | - Can use metadata as a dict of builtin-values instead of Panflute objects,
76 | with `doc.get_metadata()`.
77 |
78 | Examples of panflute filters
79 | ====================================
80 |
81 | Ports of existing pandocfilter modules are in the `github repo `_; additional and more advanced examples are in a `separate repository `_.
82 |
83 | Also, a comprehensive list of filters and other Pandoc extras should be
84 | available `here `_ in the future.
85 |
86 | Alternative: filters based on pandocfilters
87 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
88 |
89 | - For a guide to pandocfilters, see the
90 | `repository `_
91 | and the `tutorial `_.
92 | - The repo includes `sample filters
93 | `_.
94 | - The wiki lists useful `third party filters
95 | `_.
96 |
97 | Contents:
98 | ====================================
99 |
100 | .. toctree::
101 | :maxdepth: 3
102 |
103 | guide
104 | install
105 | code
106 | about
107 |
108 |
109 | Indices and tables
110 | ====================================
111 |
112 | * :ref:`genindex`
113 | * :ref:`modindex`
114 | * :ref:`search`
115 |
116 |
--------------------------------------------------------------------------------
/.github/workflows/run_tests.yml:
--------------------------------------------------------------------------------
1 | # Documentation:
2 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
3 |
4 | # Available software:
5 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners
6 |
7 | # Useful info:
8 | # https://stackoverflow.com/a/57549440/3977107
9 |
10 | # Pandoc info:
11 | # https://github.com/pandoc/dockerfiles#available-images
12 | # https://github.com/leolabs/bachelor/blob/master/.github/workflows/main.yml
13 | # https://github.com/maxheld83/pandoc/blob/master/.github/workflows/main.yml
14 |
15 | name: CI Tests
16 | on:
17 | push:
18 | pull_request:
19 | schedule:
20 | - cron: '47 23 * * 0'
21 | jobs:
22 | build:
23 | runs-on: ubuntu-latest
24 | strategy:
25 | fail-fast: false
26 | max-parallel: 7
27 | matrix:
28 | # see setup.py for supported versions
29 | # here instead of having a matrix to test against 7 * 7 * 3 * 3 combinations
30 | # we only test 7 combinations in a round-robin fashion
31 | # make sure the versions are monotonic increasing w.r.t. each other
32 | # other wise e.g. an older version of a dependency may not work well with a newer version of Python
33 | include:
34 | - python-version: "pypy-3.6"
35 | pandoc-version: "2.12"
36 | click-version: "click>=6,<7"
37 | pyyaml-version: "pyyaml>=3,<4"
38 | - python-version: "3.7"
39 | pandoc-version: "2.13"
40 | click-version: "click>=7,<8"
41 | pyyaml-version: "pyyaml>=5,<6"
42 | - python-version: "pypy-3.7"
43 | pandoc-version: "2.14.2"
44 | click-version: "click>=7,<8"
45 | pyyaml-version: "pyyaml>=5,<6"
46 | - python-version: "3.8"
47 | pandoc-version: "2.15"
48 | click-version: "click>=8,<9"
49 | pyyaml-version: "pyyaml>=6,<7"
50 | - python-version: "3.9"
51 | pandoc-version: "2.16.2"
52 | click-version: "click>=8,<9"
53 | pyyaml-version: "pyyaml>=6,<7"
54 | - python-version: "3.10"
55 | pandoc-version: "latest"
56 | click-version: "click>=8,<9"
57 | pyyaml-version: "pyyaml>=6,<7"
58 | yamlloader-version: "yamlloader>=1,<2"
59 | - python-version: "3.11-dev"
60 | pandoc-version: "latest"
61 | click-version: "click>=8,<9"
62 | pyyaml-version: "pyyaml>=6,<7"
63 | yamlloader-version: "yamlloader>=1,<2"
64 | steps:
65 | - uses: actions/checkout@v2
66 | - name: Set up Python ${{ matrix.python-version }}
67 | uses: actions/setup-python@v2
68 | with:
69 | python-version: ${{ matrix.python-version }}
70 | - name: Install dependencies
71 | run: |
72 | python -m pip install --upgrade pip
73 | python -m pip install "${{ matrix.click-version }}" "${{ matrix.pyyaml-version }}"
74 | python -m pip install ".[dev]"
75 | - name: Install yamlloader
76 | if: ${{ matrix.yamlloader-version }}
77 | run: python -m pip install "${{ matrix.yamlloader-version }}"
78 | - name: Lint with flake8
79 | run: |
80 | # stop the build if there are Python syntax errors or undefined names
81 | flake8 ./panflute --count --select=E9,F63,F7,F82 --show-source --statistics
82 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
83 | flake8 ./panflute --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
84 | - name: Download Pandoc
85 | run: |
86 | [[ ${{ matrix.pandoc-version }} == "latest" ]] && url="https://github.com/jgm/pandoc/releases/latest" || url="https://github.com/jgm/pandoc/releases/tag/${{ matrix.pandoc-version }}"
87 | url=$(curl -L $url | grep -o 'https://[a-zA-Z/.]*expanded_assets/[0-9.]*')
88 | downloadUrl="https://github.com$(curl -L $url | grep -o '/jgm/pandoc/releases/download/.*-amd64\.deb')"
89 | wget --no-verbose "$downloadUrl"
90 | sudo dpkg -i "${downloadUrl##*/}"
91 | pandoc --version
92 | - name: Test with pytest
93 | run: pytest --color=yes
94 | - name: Test by running existing filters
95 | run: |
96 | mkdir -p $HOME/.local/share/pandoc/filters
97 | find ./examples/panflute ./docs/source/_static -iname '*.py' -exec cp {} $HOME/.local/share/pandoc/filters \;
98 | find . -iname '*.md' -print0 | xargs -0 -i -n1 -P4 bash -c 'pandoc -t native -F panflute -o $0.native $0' {}
99 | - name: Test panfl cli
100 | run: panfl --help
101 |
102 | # put filters in $DATADIR for panflute's autofilter
103 | # running all available .md files through panflute
104 |
--------------------------------------------------------------------------------
/tests/test_panfl.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for the -autofilter- option, where pandoc is called as:
3 | > pandoc -F panf
4 |
5 | And the metadata of the python file has the keys:
6 | panflute-filters : a string or list
7 | panflute-path : optional folder that contains filters
8 |
9 | Additionally, there are two extra keys:
10 | panflute-verbose : True to display debugging info
11 | panflute-echo : Message to display on success
12 | """
13 |
14 | # ---------------------------
15 | # Imports
16 | # ---------------------------
17 |
18 |
19 | import os
20 | import io
21 | import sys
22 | import subprocess
23 | from pathlib import Path
24 |
25 | import panflute as pf
26 |
27 |
28 | # ---------------------------
29 | # Setup
30 | # ---------------------------
31 |
32 | # Change path to the root panflute folder
33 | os.chdir(str(Path(__file__).parents[1]))
34 |
35 |
36 | # ---------------------------
37 | # Tests
38 | # ---------------------------
39 |
40 | def test_get_filter_dirs():
41 | assert sorted(pf.get_filter_dirs()) == sorted(pf.get_filter_dirs(hardcoded=False))
42 |
43 |
44 | def test_metadata():
45 | """
46 | panfl can receive filter lists either as a metadata argument or in the YAML block
47 | This tests that both work
48 |
49 | test_filter.py is a simple filter that edits a math expression by replacing "-" with "+"
50 | and attaching the document type (html, markdown, etc.)
51 | """
52 |
53 | def to_json(text):
54 | return pf.convert_text(text, 'markdown', 'json')
55 |
56 | def assert_equal(*extra_args, input_text, output_text):
57 | """
58 | Default values for extra_args:
59 | filters=None, search_dirs=None, data_dir=True, sys_path=True, panfl_=False
60 | """
61 |
62 | # Set --from=markdown
63 | sys.argv[1:] = []
64 | sys.argv.append('markdown')
65 |
66 | _stdout = io.StringIO()
67 | pf.stdio(*extra_args, input_stream=io.StringIO(input_text), output_stream=_stdout)
68 | _stdout = pf.convert_text(_stdout.getvalue(), 'json', 'markdown')
69 | assert _stdout == output_text
70 |
71 | md_contents = "$1-1$"
72 | md_document = """---
73 | panflute-filters: test_filter
74 | panflute-path: ./tests/test_panfl/bar
75 | ...
76 | {}
77 | """.format(md_contents)
78 | expected_output = '$1+1markdown$'
79 |
80 | json_contents = to_json(md_contents)
81 | json_document = to_json(md_document)
82 |
83 | # Filter in YAML block; try `panf_` true and false (this is a minor option that changes how the path gets built)
84 | assert_equal(None, None, True, True, True, input_text=json_document, output_text=expected_output)
85 | assert_equal(None, None, True, True, False, input_text=json_document, output_text=expected_output)
86 |
87 | # Open the filter as a standalone python script within a folder
88 | assert_equal(['test_filter.py'], ['./tests/test_panfl/bar'], True, True, False, input_text=json_contents, output_text=expected_output)
89 |
90 | # Open the filter with the exact abs. path (no need for folder)
91 | assert_equal([os.path.abspath('./tests/test_panfl/bar/test_filter.py')], [], False, True, True, input_text=json_contents, output_text=expected_output)
92 |
93 | # Open the filter as part of a package (packagename.module)
94 | assert_equal(['foo.test_filter'], ['./tests/test_panfl'], False, True, True, input_text=json_contents, output_text=expected_output)
95 | assert_equal(['test_filter'], ['./tests/test_panfl/foo'], False, True, True, input_text=json_contents, output_text=expected_output)
96 |
97 |
98 | def test_pandoc_call():
99 | """
100 | This is a more difficult test as it also relies on Pandoc calling Panflute
101 | """
102 |
103 | def run_proc(*args, stdin):
104 | #assert not args, args
105 | proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=stdin, encoding='utf-8', cwd=os.getcwd())
106 | _stdout, _stderr = proc.stdout, proc.stderr
107 | return (_stdout if _stdout else '').strip(), (_stderr if _stderr else '').strip()
108 |
109 | md_contents = "$1-1$"
110 | expected_output = '$1+1markdown$'
111 |
112 | stdout = run_proc('pandoc', '--filter=panfl', '--to=markdown',
113 | '--metadata=panflute-verbose:True',
114 | '--metadata=panflute-filters:' + os.path.abspath('./tests/test_panfl/bar/test_filter.py'),
115 | stdin=md_contents)[0]
116 | assert stdout == expected_output
117 |
118 | stdout = run_proc('pandoc', '--filter=panfl', '--to=markdown',
119 | '--metadata=panflute-filters:test_filter',
120 | '--metadata=panflute-path:./tests/test_panfl/bar',
121 | stdin=md_contents)[0]
122 | assert stdout == expected_output
123 |
124 |
125 | if __name__ == "__main__":
126 | test_get_filter_dirs()
127 | test_metadata()
128 | test_pandoc_call()
--------------------------------------------------------------------------------
/tests/test_standalone.py:
--------------------------------------------------------------------------------
1 | '''
2 | Test panflute independently of Pandoc,
3 | by creating elements (instead of loading them from Pandoc)
4 | and then modifying them.
5 | '''
6 |
7 | from panflute import *
8 |
9 |
10 | def test_standalone():
11 |
12 | # Create document
13 | x = Para(Str('Hello'), Space, Str('world!'))
14 | m = {'a': True, 'b': 123.4, 'c': MetaBlocks(Para(Str('!')))}
15 | doc = Doc(x, metadata=m)
16 |
17 | # Interact with content
18 | print(repr(stringify(doc)))
19 | #assert stringify(doc) == 'Hello world!', stringify(doc) # Do we want metadata in stringify?
20 | assert 'Hello world!' in stringify(doc)
21 |
22 | doc.content.append(Para(Str('More')))
23 | print(repr(stringify(doc)))
24 | assert 'Hello world!\n\nMore' in stringify(doc)
25 |
26 | # Interact with metadata
27 | doc.metadata['d'] = False
28 | doc.metadata['e'] = MetaBool(True)
29 | doc.metadata['f'] = {'A': 1233435353, 'B': 456}
30 | doc.metadata['f']['g'] = [1,2,3,4,5]
31 | doc.metadata['g'] = 123
32 |
33 | assert isinstance(doc.get_metadata('d'), bool)
34 | assert isinstance(doc.get_metadata('e'), bool)
35 | assert isinstance(doc.get_metadata('f.B'), str)
36 |
37 | assert doc.get_metadata('e') == True, repr(doc.get_metadata('e'))
38 | assert doc.get_metadata('f.A') == '1233435353', repr(doc.get_metadata('f.A'))
39 | assert doc.get_metadata('f.e') == None
40 | assert doc.get_metadata('f.g') == ['1','2','3','4','5'], repr(doc.get_metadata('f.g'))
41 |
42 | p = doc.content[0]
43 | p.content.append(Str('3434'))
44 | print(p)
45 | #print(stringify(p))
46 | s = p.content[0]
47 |
48 | assert s.offset(0) is s
49 |
50 | print(s.next)
51 | print(s.next.next)
52 | print(s.offset(2))
53 | print(s.offset(-1))
54 | print(s.parent.next)
55 | print(s.ancestor(2))
56 | print(s.ancestor(3))
57 | print(s.parent.parent)
58 | #print(s.parent.parent.parent.parent) # Fail
59 |
60 | s.parent.content.append(Space())
61 | s.parent.content.append(Space)
62 |
63 | x = Space()
64 | x.parent
65 |
66 | print(s.parent)
67 | print(doc.content)
68 |
69 |
70 | a = Str('a')
71 | a.parent
72 |
73 | title = [Str('Monty'), Space, Str('Python')]
74 | header = Header(*title, level=2, identifier='toc')
75 | header.level += 1
76 | header.to_json()
77 |
78 | div = Div(p, p, classes=['a','b'])
79 | span = Span(Emph(Str('hello')))
80 |
81 | div.content.append(Plain(span))
82 |
83 |
84 | c = Citation('foo', prefix=[Str('A')])
85 | c.hash = 100
86 | c.suffix= p.content
87 |
88 | cite = Cite(Str('asdasd'), citations=[c])
89 | print(cite)
90 |
91 |
92 | li1 = ListItem(Para(Str('a')))
93 | li2 = ListItem(Null())
94 | lx1 = [li1, li2]
95 | lx2 = [[Para(Str('b')), Null()], [Header(Str('foo'))]]
96 |
97 | asd = [ListItem(*x) for x in lx2]
98 | lx2 = BulletList(*asd)
99 |
100 | ul = BulletList(*lx1)
101 | ul.content.extend(lx2.content)
102 |
103 | print(header)
104 |
105 | di1 = DefinitionItem([Str('a'), Space], [Definition(p)])
106 | di2 = DefinitionItem([Str('b'), Space], [Definition(p)])
107 |
108 | dl = DefinitionList(di1, di2)
109 |
110 |
111 | term = [Str('Spam')]
112 | def1 = Definition(Para(Str('...emails')))
113 | def2 = Definition(Para(Str('...meat')))
114 | spam = DefinitionItem(term, [def1, def2])
115 |
116 | term = [Str('Spanish'), Space, Str('Inquisition')]
117 | def1 = Definition(Para(Str('church'), Space, Str('court')))
118 | inquisition = DefinitionItem(term=term, definitions=[def1])
119 | dl = DefinitionList(spam, inquisition)
120 |
121 | print(dl)
122 |
123 | print('------')
124 | print(dl.content[0])
125 |
126 | x = dl.content[0].definitions[0]
127 | print(x.parent)
128 | print('--')
129 | print(x.offset(0))
130 | print(x.next)
131 | print(x.parent.next.term)
132 | print(type(x.parent.next.term))
133 |
134 |
135 | x = [Para(Str('Something')), Para(Space, Str('else'))]
136 | c1 = TableCell(*x)
137 | c2 = TableCell(Header(Str('Title')))
138 |
139 | rows = [TableRow(c1, c2)]
140 | table_head = TableHead(TableRow(c2,c1))
141 | body = TableBody(*rows)
142 | table = Table(body, head=table_head, caption=Caption())
143 |
144 | print(table)
145 |
146 |
147 | # REPLACE KEYWORD FUNCTION
148 | p1 = Para(Str('Spam'), Space, Emph(Str('and'), Space, Str('eggs')))
149 | p2 = Para(Str('eggs'))
150 | p3 = Plain(Emph(Str('eggs')))
151 | doc = Doc(p1, p2, p3)
152 |
153 | print(doc.content.list)
154 | print(stringify(doc))
155 |
156 | print('-'*20)
157 |
158 | doc.replace_keyword(keyword='eggs', replacement=Str('salad'))
159 | print('<', stringify(doc), '>')
160 |
161 | print('-'*20)
162 |
163 | doc.replace_keyword(keyword='salad', replacement=Para(Str('PIZZA')))
164 | print(doc.content.list)
165 | print('<', stringify(doc), '>')
166 |
167 |
168 |
169 |
170 |
171 | # CONVERT TEXT (MD, ETC)
172 | md = 'Some *markdown* **text** ~xyz~'
173 | tex = r'Some $x^y$ or $x_n = \sqrt{a + b}$ \textit{a}'
174 | print(convert_text(md))
175 | print(convert_text(tex))
176 |
177 |
178 |
179 | if __name__ == "__main__":
180 | test_standalone()
181 |
--------------------------------------------------------------------------------
/tests/test_convert_text.py:
--------------------------------------------------------------------------------
1 | import io
2 | import panflute as pf
3 |
4 | def test_all():
5 | md = 'Some *markdown* **text** ~xyz~'
6 | c_md = pf.convert_text(md)
7 | b_md = [pf.Para(pf.Str("Some"), pf.Space,
8 | pf.Emph(pf.Str("markdown")), pf.Space,
9 | pf.Strong(pf.Str("text")), pf.Space,
10 | pf.Subscript(pf.Str("xyz")))]
11 |
12 | print("Benchmark MD:")
13 | print(b_md)
14 | print("Converted MD:")
15 | print(c_md)
16 | assert repr(c_md) == repr(b_md)
17 |
18 | with io.StringIO() as f:
19 | doc = pf.Doc(*c_md)
20 | pf.dump(doc, f)
21 | c_md_dump = f.getvalue()
22 |
23 | with io.StringIO() as f:
24 | doc = pf.Doc(*b_md)
25 | pf.dump(doc, f)
26 | b_md_dump = f.getvalue()
27 |
28 | assert c_md_dump == b_md_dump
29 |
30 | # ----------------------
31 | print()
32 |
33 | tex = r'Some $x^y$ or $x_n = \sqrt{a + b}$ \textit{a}'
34 | c_tex = pf.convert_text(tex)
35 | b_tex = [pf.Para(pf.Str("Some"), pf.Space,
36 | pf.Math("x^y", format='InlineMath'), pf.Space,
37 | pf.Str("or"), pf.Space,
38 | pf.Math(r"x_n = \sqrt{a + b}", format='InlineMath'),
39 | pf.Space, pf.RawInline(r"\textit{a}", format='tex'))]
40 |
41 | print("Benchmark TEX:")
42 | print(b_tex)
43 | print("Converted TEX:")
44 | print(c_tex)
45 | assert repr(c_tex) == repr(b_tex)
46 |
47 | with io.StringIO() as f:
48 | doc = pf.Doc(*c_tex)
49 | pf.dump(doc, f)
50 | c_tex_dump = f.getvalue()
51 |
52 | with io.StringIO() as f:
53 | doc = pf.Doc(*b_tex)
54 | pf.dump(doc, f)
55 | b_tex_dump = f.getvalue()
56 |
57 | assert c_tex_dump == b_tex_dump
58 |
59 |
60 | print("\nBack and forth conversions... md->json->md")
61 | md = 'Some *markdown* **text** ~xyz~'
62 | print("[MD]", md)
63 | md2json = pf.convert_text(md, input_format='markdown', output_format='json')
64 | print("[JSON]", md2json)
65 | md2json2md = pf.convert_text(md2json, input_format='json', output_format='markdown')
66 | print("[MD]", md2json2md)
67 | assert md == md2json2md
68 |
69 |
70 | print("\nBack and forth conversions... md->panflute->md")
71 | md = 'Some *markdown* **text** ~xyz~'
72 | print("[MD]", md)
73 | md2panflute = pf.convert_text(md, input_format='markdown', output_format='panflute')
74 | print("[PANFLUTE]", md2panflute)
75 | md2panflute2md = pf.convert_text(md2panflute, input_format='panflute', output_format='markdown')
76 | print("[MD]", md2panflute2md)
77 | assert md == md2panflute2md
78 |
79 | print("\nBack and forth conversions... md->panflute(standalone)->md")
80 | md = 'Some *markdown* **text** ~xyz~'
81 | print("[MD]", md)
82 | md2panflute = pf.convert_text(md, input_format='markdown', output_format='panflute', standalone=True)
83 | print("[PANFLUTE]", md2panflute)
84 | md2panflute2md = pf.convert_text(md2panflute, input_format='panflute', output_format='markdown')
85 | print("[MD]", md2panflute2md)
86 | assert md == md2panflute2md
87 |
88 | print("\nBack and forth conversions... md table -> json(standalone) -> md table")
89 | md = """lorem
90 |
91 | --- ---
92 | x y
93 | --- ---
94 |
95 | ipsum"""
96 | print("[MD]", repr(md))
97 | md2json = pf.convert_text(md, input_format='markdown', output_format='json', standalone=True)
98 | print("[json]", md2json)
99 | md2json2md = pf.convert_text(md2json, input_format='json', output_format='markdown')
100 | print("[MD]", repr(md2json2md))
101 | assert md == md2json2md
102 |
103 |
104 | print("\nBack and forth conversions... md table -> panflute(standalone) -> md table")
105 | print("[MD]", repr(md))
106 | md2panflute = pf.convert_text(md, input_format='markdown', output_format='panflute', standalone=True)
107 | print("[PANFLUTE]", md2panflute)
108 | md2panflute2md = pf.convert_text(md2panflute, input_format='panflute', output_format='markdown')
109 | print("[MD]", repr(md2panflute2md))
110 | assert md == md2panflute2md
111 |
112 | print("\nBack and forth conversions... gfm table (empty) -> json(standalone) -> gfm table (empty)")
113 | md = """lorem
114 |
115 | | x | y |
116 | |-----|-----|
117 |
118 | ipsum"""
119 | print("[MD]", repr(md))
120 | md2json = pf.convert_text(md, input_format='gfm', output_format='json', standalone=True)
121 | print("[json]", md2json)
122 | md2json2md = pf.convert_text(md2json, input_format='json', output_format='gfm')
123 | print("[MD]", repr(md2json2md))
124 | assert md == md2json2md
125 |
126 |
127 | print("\nBack and forth conversions... gfm table (empty) -> panflute(standalone) -> gfm table (empty)")
128 | print("[MD]", repr(md))
129 | md2panflute = pf.convert_text(md, input_format='gfm', output_format='panflute', standalone=True)
130 | print("[PANFLUTE]", md2panflute)
131 | md2panflute2md = pf.convert_text(md2panflute, input_format='panflute', output_format='gfm')
132 | print("[MD]", repr(md2panflute2md))
133 | assert md == md2panflute2md
134 |
135 |
136 | if __name__ == "__main__":
137 | test_all()
138 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """A pythonic alternative to pandocfilters
2 |
3 | See:
4 | https://github.com/sergiocorreia/panflute
5 | """
6 |
7 | from setuptools import setup, find_packages
8 | from os import path
9 |
10 | here = path.abspath(path.dirname(__file__))
11 |
12 | # Get the long description from the README file
13 | with open(path.join(here, 'README.md'), encoding='utf-8') as f:
14 | long_description = f.read()
15 |
16 | # Import version number
17 | version = {}
18 | with open("panflute/version.py") as fp:
19 | exec(fp.read(), version)
20 | version = version['__version__']
21 |
22 | setup(
23 | name='panflute',
24 |
25 | # Versions should comply with PEP440. For a discussion on single-sourcing
26 | # the version across setup.py and the project code, see
27 | # https://packaging.python.org/en/latest/single_source_version.html
28 | version=version,
29 |
30 | description='Pythonic Pandoc filters',
31 | long_description=long_description,
32 | long_description_content_type='text/markdown',
33 |
34 | # The project's main homepage.
35 | url='https://github.com/sergiocorreia/panflute',
36 | project_urls={
37 | "Source": "https://github.com/sergiocorreia/panflute",
38 | "Documentation": "http://scorreia.com/software/panflute/",
39 | "Tracker": "https://github.com/sergiocorreia/panflute/issues",
40 | },
41 |
42 | # Author details
43 | author="Sergio Correia",
44 | author_email='sergio.correia@gmail.com',
45 |
46 | # Choose your license
47 | license='BSD3',
48 |
49 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
50 | classifiers=[
51 | # How mature is this project? Common values are
52 | # 3 - Alpha
53 | # 4 - Beta
54 | # 5 - Production/Stable
55 | 'Development Status :: 5 - Production/Stable',
56 |
57 | 'Environment :: Console',
58 |
59 | # Indicate who your project is intended for
60 | 'Intended Audience :: End Users/Desktop',
61 | 'Intended Audience :: Developers',
62 | 'Topic :: Software Development :: Build Tools',
63 |
64 | # Pick your license as you wish (should match "license" above)
65 | 'License :: OSI Approved :: BSD License',
66 |
67 | 'Operating System :: OS Independent',
68 | 'Topic :: Text Processing :: Filters',
69 |
70 | # Specify the Python versions you support here. In particular, ensure
71 | # that you indicate whether you support Python 2, Python 3 or both.
72 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers
73 | 'Programming Language :: Python :: 3.6',
74 | 'Programming Language :: Python :: 3.7',
75 | 'Programming Language :: Python :: 3.8',
76 | 'Programming Language :: Python :: 3.9',
77 | 'Programming Language :: Python :: 3.10',
78 | 'Programming Language :: Python :: Implementation :: CPython',
79 | 'Programming Language :: Python :: Implementation :: PyPy'
80 | ],
81 |
82 | # What does your project relate to?
83 | keywords='pandoc pandocfilters markdown latex',
84 |
85 | # You can just specify the packages manually here if your project is
86 | # simple. Or you can use find_packages().
87 | packages=find_packages(exclude=['contrib', 'docs', 'tests', 'examples']),
88 |
89 | python_requires='>=3.6',
90 | # List run-time dependencies here. These will be installed by pip when
91 | # your project is installed. For an analysis of "install_requires" vs pip's
92 | # requirements files see:
93 | # https://packaging.python.org/en/latest/requirements.html
94 | install_requires=[
95 | 'click >=6,<9',
96 | 'pyyaml >=3,<7',
97 | ],
98 |
99 | # List additional groups of dependencies here (e.g. development
100 | # dependencies). You can install these using the following syntax,
101 | # for example:
102 | # $ pip install -e .[dev,pypi]
103 | extras_require={
104 | 'dev': [
105 | 'configparser',
106 | 'coverage',
107 | 'flake8',
108 | 'pandocfilters',
109 | 'pytest-cov',
110 | 'pytest',
111 | 'requests',
112 | ],
113 | 'pypi': [
114 | 'docutils',
115 | 'Pygments',
116 | 'twine',
117 | 'wheel',
118 | ],
119 | 'extras': [
120 | "yamlloader>=1,<2",
121 | ],
122 | },
123 |
124 | # If there are data files included in your packages that need to be
125 | # installed, specify them here. If using Python 2.6 or less, then these
126 | # have to be included in MANIFEST.in as well.
127 | #package_data={
128 | # 'sample': ['package_data.dat'],
129 | #},
130 |
131 | # Although 'package_data' is the preferred approach, in some case you may
132 | # need to place data files outside of your packages. See:
133 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa
134 | # In this case, 'data_file' will be installed into '/my_data'
135 | #data_files=[('my_data', ['data/data_file'])],
136 |
137 | # To provide executable scripts, use entry points in preference to the
138 | # "scripts" keyword. Entry points provide cross-platform support and allow
139 | # pip to create the appropriate form of executable for the target platform.
140 | entry_points={
141 | 'console_scripts': [
142 | 'panflute=panflute:main',
143 | 'panfl=panflute:panfl',
144 | ],
145 | },
146 | )
147 |
--------------------------------------------------------------------------------
/examples/panflute/gabc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Pandoc filter to convert code blocks with class "gabc" to LaTeX
4 | \\gabcsnippet commands in LaTeX output, and to images in HTML output.
5 | Assumes Ghostscript, LuaLaTeX, [Gregorio](http://gregorio-project.github.io/)
6 | and a reasonable selection of LaTeX packages are installed.
7 | """
8 |
9 | import os
10 | from sys import getfilesystemencoding, stderr
11 | from subprocess import Popen, call, PIPE, DEVNULL
12 | from hashlib import sha1
13 | from panflute import toJSONFilter, RawBlock, RawInline, Para, Image, Code, CodeBlock
14 |
15 |
16 | IMAGEDIR = "tmp_gabc"
17 | LATEX_DOC = """\\documentclass{article}
18 | \\usepackage{libertine}
19 | \\usepackage[autocompile]{gregoriotex}
20 | \\pagestyle{empty}
21 | \\begin{document}
22 | %s
23 | \\end{document}
24 | """
25 |
26 |
27 | def sha(code):
28 | """Returns sha1 hash of the code"""
29 | return sha1(code.encode(getfilesystemencoding())).hexdigest()
30 |
31 |
32 | def latex(code):
33 | """LaTeX inline"""
34 | return RawInline(code, format='latex')
35 |
36 |
37 | def latexblock(code):
38 | """LaTeX block"""
39 | return RawBlock(code, format='latex')
40 |
41 |
42 | def htmlblock(code):
43 | """Html block"""
44 | return RawBlock(code, format='html')
45 |
46 |
47 | def latexsnippet(code, kvs):
48 | """Take in account key/values"""
49 | snippet = ''
50 | staffsize = int(kvs['staffsize']) if 'staffsize' in kvs else 17
51 | annotationsize = .56 * staffsize
52 | if 'mode' in kvs:
53 | snippet = (
54 | "\\greannotation{{\\fontsize{%s}{%s}\\selectfont{}%s}}\n" %
55 | (annotationsize, annotationsize, kvs['mode'])
56 | ) + snippet
57 | if 'annotation' in kvs:
58 | snippet = (
59 | "\\grechangedim{annotationseparation}{%s mm}{0}\n"
60 | "\\greannotation{{\\fontsize{%s}{%s}\\selectfont{}%s}}\n" %
61 | (staffsize / 34, annotationsize, annotationsize, kvs['annotation'])
62 | ) + snippet
63 | snippet = (
64 | "\\grechangestaffsize{%s}\n" % staffsize +
65 | "\\def\\greinitialformat#1{{\\fontsize{%s}{%s}\\selectfont{}#1}}" %
66 | (2.75 * staffsize, 2.75 * staffsize)
67 | ) + snippet
68 | snippet = "\\setlength{\\parskip}{0pt}\n" + snippet + code
69 | return snippet
70 |
71 |
72 | def latex2png(snippet, outfile):
73 | """Compiles a LaTeX snippet to png"""
74 | pngimage = os.path.join(IMAGEDIR, outfile + '.png')
75 | environment = os.environ
76 | environment['openout_any'] = 'a'
77 | environment['shell_escape_commands'] = \
78 | "bibtex,bibtex8,kpsewhich,makeindex,mpost,repstopdf,gregorio"
79 | proc = Popen(
80 | ["lualatex", '-output-directory=' + IMAGEDIR],
81 | stdin=PIPE,
82 | stdout=DEVNULL,
83 | env=environment
84 | )
85 | proc.stdin.write(
86 | (
87 | LATEX_DOC % (snippet)
88 | ).encode("utf-8")
89 | )
90 | proc.communicate()
91 | proc.stdin.close()
92 | call(["pdfcrop", os.path.join(IMAGEDIR, "texput.pdf")], stdout=DEVNULL)
93 | call(
94 | [
95 | "gs",
96 | "-sDEVICE=pngalpha",
97 | "-r144",
98 | "-sOutputFile=" + pngimage,
99 | os.path.join(IMAGEDIR, "texput-crop.pdf"),
100 | ],
101 | stdout=DEVNULL,
102 | )
103 |
104 |
105 | def png(contents, latex_command):
106 | """Creates a png if needed."""
107 | outfile = sha(contents + latex_command)
108 | src = os.path.join(IMAGEDIR, outfile + '.png')
109 | if not os.path.isfile(src):
110 | try:
111 | os.mkdir(IMAGEDIR)
112 | stderr.write('Created directory ' + IMAGEDIR + '\n')
113 | except OSError:
114 | pass
115 | latex2png(latex_command + "{" + contents + "}", outfile)
116 | stderr.write('Created image ' + src + '\n')
117 | return src
118 |
119 |
120 | def gabc(elem, doc):
121 | """Handle gabc file inclusion and gabc code block."""
122 | if type(elem) == Code and "gabc" in elem.classes:
123 | if doc.format == "latex":
124 | if elem.identifier == "":
125 | label = ""
126 | else:
127 | label = '\\label{' + elem.identifier + '}'
128 | return latex(
129 | "\n\\smallskip\n{%\n" +
130 | latexsnippet('\\gregorioscore{' + elem.text + '}', elem.attributes) +
131 | "%\n}" +
132 | label
133 | )
134 | else:
135 | infile = elem.text + (
136 | '.gabc' if '.gabc' not in elem.text else ''
137 | )
138 | with open(infile, 'r') as doc:
139 | code = doc.read().split('%%\n')[1]
140 | return Image(png(
141 | elem.text,
142 | latexsnippet('\\gregorioscore', elem.attributes)
143 | ))
144 | elif type(elem) == CodeBlock and "gabc" in elem.classes:
145 | if doc.format == "latex":
146 | if elem.identifier == "":
147 | label = ""
148 | else:
149 | label = '\\label{' + elem.identifier + '}'
150 | return latexblock(
151 | "\n\\smallskip\n{%\n" +
152 | latexsnippet('\\gabcsnippet{' + elem.text + '}', elem.attributes) +
153 | "%\n}" +
154 | label
155 | )
156 | else:
157 | return Para(Image(url=png(elem.text, latexsnippet('\\gabcsnippet', elem.attributes))))
158 |
159 |
160 | if __name__ == "__main__":
161 | toJSONFilter(gabc)
162 |
--------------------------------------------------------------------------------
/panflute/utils.py:
--------------------------------------------------------------------------------
1 | """
2 | Auxiliary functions that have no dependencies
3 | """
4 |
5 | # ---------------------------
6 | # Imports
7 | # ---------------------------
8 |
9 | import os
10 | import sys
11 | import json
12 | import os.path as p
13 | from importlib import import_module
14 |
15 |
16 | # ---------------------------
17 | # Functions
18 | # ---------------------------
19 |
20 | def decode_ica(lst):
21 | return {'identifier': lst[0],
22 | 'classes': lst[1],
23 | 'attributes': lst[2]}
24 |
25 |
26 | def debug(*args, **kwargs):
27 | """
28 | Same as print, but prints to ``stderr``
29 | (which is not intercepted by Pandoc).
30 | """
31 | print(file=sys.stderr, *args, **kwargs)
32 |
33 |
34 | def get_caller_name():
35 | '''Get the name of the calling Element
36 |
37 | This is just the name of the Class of the __init__ calling function
38 | '''
39 |
40 | # References:
41 | # https://jugad2.blogspot.com/2015/09/find-caller-and-callers-caller-of.html
42 | # https://stackoverflow.com/a/47956089/3977107
43 | # https://stackoverflow.com/a/11799376/3977107
44 |
45 | pos = 1
46 | while True:
47 | pos += 1
48 | try:
49 | callingframe = sys._getframe(pos)
50 | except ValueError:
51 | return 'Panflute'
52 |
53 | if callingframe.f_code.co_name == '__init__':
54 | class_name = callingframe.f_locals['self'].__class__.__name__
55 | if 'Container' not in class_name:
56 | return class_name
57 |
58 |
59 | def check_type(value, oktypes):
60 | # This allows 'Space' instead of 'Space()'
61 | if callable(value):
62 | value = value()
63 |
64 | if isinstance(value, oktypes):
65 | return value
66 |
67 | # Invalid type
68 | caller = get_caller_name()
69 | tag = type(value).__name__
70 | msg = '\n\nElement "{}" received "{}" but expected {}\n'.format(caller, tag, oktypes)
71 | raise TypeError(msg)
72 |
73 |
74 | def check_group(value, group):
75 | if value not in group:
76 | tag = type(value).__name__
77 | msg = 'element {} not in group {}'.format(tag, repr(group))
78 | raise TypeError(msg)
79 | else:
80 | return value
81 |
82 |
83 | def check_type_or_value(value, oktypes, okvalue):
84 | # This allows 'Space' instead of 'Space()'
85 | if callable(value):
86 | value = value()
87 |
88 | if isinstance(value, oktypes) or (value == okvalue):
89 | return value
90 |
91 | # Invalid type
92 | caller = get_caller_name()
93 | tag = type(value).__name__
94 | msg = '\n\nElement "{}" received "{}" but expected {} or {}\n'.format(caller, tag, oktypes, okvalue)
95 | raise TypeError(msg)
96 |
97 |
98 | def encode_dict(tag, content):
99 | return {
100 | "t": tag,
101 | "c": content,
102 | }
103 |
104 |
105 | def load_pandoc_version():
106 | """
107 | Retrieve Pandoc version tuple from the environment
108 | """
109 | try:
110 | return tuple(int(i) for i in os.environ['PANDOC_VERSION'].split('.'))
111 | except KeyError:
112 | pass
113 | except (AttributeError, ValueError):
114 | debug(f'Environment variable PANDOC_VERSION is malformed, ignoring...')
115 |
116 |
117 | def load_pandoc_reader_options():
118 | """
119 | Retrieve Pandoc Reader options from the environment
120 | """
121 | try:
122 | # TODO? make option names pythonic ('readerIndentedCodeClasses' -> 'indented_code_classes')
123 | # TODO? replace list with set (readerAbbreviations)
124 | options = json.loads(os.environ['PANDOC_READER_OPTIONS'])
125 | return options
126 | except KeyError:
127 | pass
128 | except json.JSONDecodeError:
129 | debug(f'Environment variable PANDOC_READER_OPTIONS is malformed, ignoring...')
130 | return dict()
131 |
132 |
133 | # ---------------------------
134 | # Classes
135 | # ---------------------------
136 |
137 | class ContextImport:
138 | """
139 | Import module context manager.
140 | Temporarily prepends extra dir
141 | to sys.path and imports the module,
142 |
143 | Example:
144 | >>> # /path/dir/fi.py
145 | >>> with ContextImport('/path/dir/fi.py') as module:
146 | >>> # prepends '/path/dir' to sys.path
147 | >>> # module = import_module('fi')
148 | >>> module.main()
149 | >>> with ContextImport('dir.fi', '/path') as module:
150 | >>> # prepends '/path' to sys.path
151 | >>> # module = import_module('dir.fi')
152 | >>> module.main()
153 | """
154 | def __init__(self, module, extra_dir=None):
155 | """
156 | :param module: str
157 | module spec for import or file path
158 | from that only basename without .py is used
159 | :param extra_dir: str or None
160 | extra dir to prepend to sys.path
161 | if module then doesn't change sys.path if None
162 | if file then prepends dir if None
163 | """
164 | def remove_py(s):
165 | return s[:-3] if s.endswith('.py') else s
166 |
167 | self.module = remove_py(p.basename(module))
168 | if (extra_dir is None) and (module != p.basename(module)):
169 | extra_dir = p.dirname(module)
170 | self.extra_dir = extra_dir
171 |
172 | def __enter__(self):
173 | if self.extra_dir is not None:
174 | sys.path.insert(0, self.extra_dir)
175 | return import_module(self.module)
176 |
177 | def __exit__(self, exc_type, exc_value, traceback):
178 | if self.extra_dir is not None:
179 | sys.path.pop(0)
180 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Panflute: Pythonic Pandoc Filters
2 |
3 | [](https://pypi.python.org/pypi/panflute/)
4 | [](https://github.com/sergiocorreia/panflute/actions?query=workflow%3A%22CI+Tests%22)
5 | 
6 | [](https://zenodo.org/badge/latestdoi/55024750)
7 |
8 | [](https://github.com/sergiocorreia/panflute/releases)
9 | [](https://pypi.python.org/pypi/panflute/)
10 | [](https://anaconda.org/conda-forge/panflute)
11 | [](https://pypi.python.org/pypi/panflute/)
12 | [](https://pypi.org/project/panflute)
13 |
14 | [panflute](http://scorreia.com/software/panflute/) is a Python package that makes creating Pandoc filters fun.
15 |
16 | For a detailed user guide, documentation, and installation instructions, see
17 | .
18 | For examples that you can use as starting points, check the [examples repo](https://github.com/sergiocorreia/panflute-filters/tree/master/filters), the [sample template](https://raw.githubusercontent.com/sergiocorreia/panflute/master/docs/source/_static/template.py), or [this github search](https://github.com/search?o=desc&q=%22import+panflute%22+OR+%22from+panflute%22+created%3A%3E2016-01-01+language%3APython+extension%3Apy&s=indexed&type=Code&utf8=%E2%9C%93).
19 | If you want to contribute, head [here](/CONTRIBUTING.md).
20 |
21 | You might also find useful [this presentation](https://github.com/BPLIM/Workshops/raw/master/BPLIM2019/D2_S1_Sergio_Correia_Markdown.pdf) on how I use markdown+pandoc+panflute to write research papers (at the Banco de Portugal 2019 Workshop on Reproductible Research).
22 |
23 |
24 | ## Installation
25 |
26 | ### Pip
27 |
28 | To manage panflute using pip, open the command line and run
29 |
30 | - `pip install panflute` to install
31 | - `pip install "panflute[extras]"` to include extra dependencies (`yamlloader`)
32 | - `pip install -U panflute` to upgrade
33 | - `pip uninstall panflute` to remove
34 |
35 | You need a matching pandoc version for panflute to work flawlessly. See [Supported pandoc versions] for details. Or, use the [Conda] method to install below to have the pandoc version automatically managed for you.
36 |
37 | ### Conda
38 |
39 | To manage panflute with a matching pandoc version, open the command line and run
40 |
41 | - `conda install -c conda-forge pandoc 'panflute>=2.0.5'` to install both
42 | `conda install -c conda-forge pandoc 'panflute>=2.0.5' yamlloader` to include extra dependencies
43 | - `conda update pandoc panflute` to upgrade both
44 | - `conda remove pandoc panflute` to remove both
45 |
46 | You may also replace `conda` by `mamba`, which is basically a drop-in replacement of the conda package manager. See [mamba-org/mamba: The Fast Cross-Platform Package Manager](https://github.com/mamba-org/mamba) for details.
47 |
48 | ### Note on versions
49 |
50 | #### Supported Python versions
51 |
52 | panflute 1.12 or above dropped support of Python 2. When using Python 3, depending on your setup, you may need to use `pip3`/`python3` explicitly. If you need to use panflute in Python 2, install panflute 1.11.x or below.
53 |
54 | Currently supported Python versions: [](https://pypi.python.org/pypi/panflute/). Check `setup.py` for details, which further indicates support of pypy on top of CPython.
55 |
56 | #### Supported pandoc versions
57 |
58 | pandoc versioning semantics is [MAJOR.MAJOR.MINOR.PATCH](https://pvp.haskell.org) and panflute's is MAJOR.MINOR.PATCH. Below we shows matching versions of pandoc that panflute supports, in descending order. Only major version is shown as long as the minor versions doesn't matter.
59 |
60 |
61 |
62 | | panflute version | supported pandoc versions | supported pandoc API versions |
63 | | ---------------- | ------------------------- | ----------------------------- |
64 | | 2.1.3 | 2.11.0.4–2.17.x | 1.22–1.22.1 |
65 | | 2.1 | 2.11.0.4—2.14.x | 1.22 |
66 | | 2.0 | 2.11.0.4—2.11.x | 1.22 |
67 | | not supported | 2.10 | 1.21 |
68 | | 1.12 | 2.7-2.9 | 1.17.5–1.20 |
69 |
70 | Note: pandoc 2.10 is short lived and 2.11 has minor API changes comparing to that, mainly for fixing its shortcomings. Please avoid using pandoc 2.10.
71 |
72 | ## Dev Install
73 |
74 | After cloning the repo and opening the panflute folder, run
75 |
76 | - `python setup.py install` to install the package locally
77 | - `python setup.py develop` to install locally with a symlink so changes are automatically updated
78 |
79 | ## Contributing
80 |
81 | Feel free to submit push requests. For consistency, code should comply with [pep8](https://pypi.python.org/pypi/pep8) (as long as its reasonable), and with the style guides by [@kennethreitz](http://docs.python-guide.org/en/latest/writing/style/) and [google](http://google.github.io/styleguide/pyguide.html). Read more [here](/CONTRIBUTING.md).
82 |
83 | ## License
84 |
85 | BSD3 license (following [`pandocfilters`](https://github.com/jgm/pandocfilters) by @jgm).
86 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Panflute
2 |
3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
4 |
5 | This document contains useful resources and guidelines when contributing to the project:
6 |
7 |
8 | #### Table Of Contents
9 |
10 | - [Style guide](#style-guide)
11 | - [Panflute internals](#panflute-internals)
12 | - [Documentation](#documentation)
13 |
14 |
15 | ## Style Guide
16 |
17 | For consistency, code should try to comply (as much as possible) with [pep8](https://pypi.python.org/pypi/pep8), and with the style guides by [@kennethreitz](http://docs.python-guide.org/en/latest/writing/style/) and [Google](http://google.github.io/styleguide/pyguide.html).
18 |
19 | ### flake8
20 |
21 | ```bash
22 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
23 | ```
24 |
25 | ### Useful tools
26 |
27 | - [`pycodestyle`](https://pypi.org/project/pycodestyle/) (formerly `pep8`): run it with `pycodestyle ./panflute > pycodestyle-report.txt`
28 | - [`pylint`](https://www.pylint.org/): run it with `pylint panflute > pylint-report.txt` from the root folder of the repo
29 | - [Github Actions](https://github.com/sergiocorreia/panflute/blob/master/.github/workflows/run_tests.yml). CI tool that run automatically after pushing code. Amongst other things, it runs pytest on all the `test_*.py` files in the [`\tests`](https://github.com/sergiocorreia/panflute/tree/master/tests) folder.
30 |
31 |
32 | ## Panflute internals
33 |
34 |
35 | ### Program flow
36 |
37 | - Filters usually call panflute with `panflute.run_filter(action)`.
38 | - Note: `run_filter`, `toJSONFilter` and `toJSONFilters` are just thin wrappers for `run_filters`.
39 | - Within `run_filter`,
40 | 1. `doc = panflute.load()` reads the JSON input from stdin and creates a `panflute.Doc` object, which is just a tree containing the entire document.
41 | 2. `doc.walk(action, doc)` will walk through the document tree (top to bottom) and apply the `action` function
42 | 3. `panflute.dump(doc)` will encode the tree into JSON and dump it to stdout, finishing execution
43 |
44 |
45 | ### Modules in the `panflute` package
46 |
47 | - `__init__.py`: loads the functions that will be part of API of the package.
48 | - `utils.py`: contains auxiliary functions that *do not* depend on any other part of `panflute`.
49 | - `version.py`: has the version string.
50 | - `containers.py`: has a `ListContainer` and `DictContainer` classes. These are just wrappers around lists and dicts, that i) allow only certain items of certain types to be added (through `.oktypes`), and ii) keep track of the parent objects to allow for navigation through the tree (so you can do `.parent`, `.prev`, `.next`, etc.).
51 | - Note: there is also a rarely used `._container` property, needed for when the parent object can hold more than one container. For instance, `Doc` holds both standard items in `.content` and also metadata items in `.metadata`, so in order to traverse the tree, we need to know in what container of the parent each item is. This is only used by the `Doc`, `Citation`, `DefinitionItem`, `Quoted` and `Table` objects.
52 | - `base.py`: has the base classes of all Pandoc elements. These are `Element` and its subclasses `Block`, `Inline` and `Metadata`.
53 | - `elements.py`: have all the standard Pandoc elements (`Str`, `Para`, `Space`, etc.). Pandoc elements inherit from one of three base classes (`Block`, `Inline` and `Metadata`), which we use to make sure that an elements does not get placed in another element where it's not allowed.
54 | - Note: there are some elements not present in [pandoc-types](https://github.com/jgm/pandoc-types/blob/master/Text/Pandoc/Definition.hs) that are subclass from `Element` directly. These are `Doc`, `Citation`, `ListItem`, `Definition`, `DefinitionItem`, `TableCell` and `TableRow`. This allow filters to be applied directly to table rows instead of to tables and then looping within each item of the table.
55 | - `elements.py` also contains the function `from_json`, which is essential in converting JSON elements into Pandoc elements.
56 | - `io.py`: holds all the I/O functions (`load`, `dump`, `run_filters`, and wrappers).
57 | - `tools.py`: contain functions that are useful when writing filters (but not essential). These include `stringify`, `yaml_filter`, `convert_string`, etc.
58 | - Note: future enhancements to `panflute` should probably go here.
59 | - `autofilter.py`: has the code that allows panflute to be run as an executable script.
60 | - This allows panflute to be run as a filter (!), in which case it uses the `panflute-...` metadata to conveniently call different filters.
61 |
62 |
63 | ## Documentation
64 |
65 | Panflute uses [Sphinx](http://www.sphinx-doc.org/) for its documentation.
66 | To install it, install Python 3.3+ and then run `pip install sphinx` (or see [here](http://www.sphinx-doc.org/en/1.4.8/install.html)).
67 |
68 | To build the documentation, navigate to the `/docs` folder and type `make html`. The build files will then be placed in `/docs/build/html`, and can be copied into a [website](scorreia.com/software/panflute/)
69 |
70 | The guides are written in [REST](http://www.sphinx-doc.org/en/stable/rest.html) and located in the [/docs/source](https://github.com/sergiocorreia/panflute/tree/master/docs/source) folder.
71 |
72 | The API docs are written as comments in the [source code itself](https://github.com/sergiocorreia/panflute/blob/master/panflute/elements.py#L242) (so e.g. [this](http://scorreia.com/software/panflute/code.html) is autogenerated).
73 |
74 | ### REST guides
75 |
76 | - [REST Primer](http://www.sphinx-doc.org/en/stable/rest.html)
77 | - [REST and Sphinx Cheatsheet](http://openalea.gforge.inria.fr/doc/openalea/doc/_build/html/source/sphinx/rest_syntax.html#restructured-text-rest-and-sphinx-cheatsheet)
78 | - [Sphinx commands](https://pythonhosted.org/an_example_pypi_project/sphinx.html)
79 | - [Sphinx domains](http://www.sphinx-doc.org/en/stable/domains.html). This is used to create links to other python packages and to the stdlib (e.g. ``:py:data:`sys.stdin`` `).
80 |
--------------------------------------------------------------------------------
/docs/source/code.rst:
--------------------------------------------------------------------------------
1 | Panflute API
2 | ============
3 |
4 | .. contents:: Contents:
5 | :local:
6 |
7 |
8 | Base elements
9 | ****************
10 |
11 | .. autoclass:: panflute.base.Element
12 |
13 | .. attribute:: parent
14 |
15 | Element that contains the current one.
16 |
17 | **Note:** the ``.parent`` and related
18 | attributes are not implemented for metadata elements.
19 |
20 |
21 | :rtype: :class:`Element` | ``None``
22 |
23 | .. attribute:: location
24 |
25 | ``None`` unless the element is in a non--standard location of its
26 | parent, such as the ``.caption`` or ``.header`` attributes of a table.
27 |
28 | In those cases, ``.location`` will be equal to a string.
29 |
30 | :rtype: ``str`` | ``None``
31 |
32 | .. automethod:: panflute.base.Element.walk
33 | .. autoattribute:: panflute.base.Element.content
34 | .. autoattribute:: panflute.base.Element.index
35 | .. automethod:: panflute.base.Element.ancestor
36 | .. automethod:: panflute.base.Element.offset
37 | .. autoattribute:: panflute.base.Element.prev
38 | .. autoattribute:: panflute.base.Element.next
39 | .. automethod:: panflute.base.Element.replace_keyword
40 | .. autoattribute:: panflute.base.Element.container
41 |
42 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
43 |
44 | The following elements inherit from :class:`Element`:
45 |
46 | .. automodule:: panflute.base
47 | :members: Block, Inline, MetaValue
48 |
49 | Low-level classes
50 | ~~~~~~~~~~~~~~~~~~
51 |
52 | *(Skip unless you want to understand the internals)*
53 |
54 |
55 | .. automodule:: panflute.containers
56 | :members:
57 |
58 | .. note::
59 | To keep track of every element's parent we do some
60 | class magic. Namely, ``Element.content`` is not a list attribute
61 | but a property accessed via getter and setters. Why?
62 |
63 | >>> e = Para(Str(Hello), Space, Str(World!))
64 |
65 | This creates a ``Para`` element, which stores the three
66 | inline elements (Str, Space and Str) inside an ``.content`` attribute.
67 | If we add ``.parent`` attributes to these elements,
68 | there are three ways they can be made obsolete:
69 |
70 | 1. By replacing specific elements: ``e.content[0] = Str('Bye')``
71 | 2. By replacing the entire list: ``e.contents = other_items``
72 |
73 | We deal with the first problem with wrapping the list of items
74 | with a ListContainer class of type :class:`collections.MutableSequence`.
75 | This class updates the ``.parent`` attribute to elements returned
76 | through ``__getitem__`` calls.
77 |
78 | For the second problem, we use setters and getters which update the
79 | ``.parent`` attribute.
80 |
81 |
82 | Standard elements
83 | ********************************************
84 |
85 | These are the standard Pandoc elements, as described `here `_. Consult the `repo `_ for the latest updates.
86 |
87 | .. note::
88 | The attributes of every element object will be
89 | i) the parameters listed below, plus
90 | ii) the attributes of :class:`Element`.
91 | Example:
92 |
93 | >>> h = Str(text='something')
94 | >>> h.text
95 | 'something'
96 | >>> hasattr(h, 'parent')
97 | True
98 |
99 | **Exception:** the ``.content`` attribute only exists
100 | in elements that take ``*args``
101 | (so we can do ``Para().content`` but not ``Str().content``).
102 |
103 | .. automodule:: panflute.elements
104 | :noindex:
105 | :members: Doc
106 |
107 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
108 |
109 | .. automodule:: panflute.elements
110 | :members:
111 | :exclude-members: Doc
112 |
113 | Table-specific elements
114 | ********************************************
115 |
116 | .. automodule:: panflute.table_elements
117 | :members:
118 |
119 |
120 | Standard functions
121 | ********************************************
122 |
123 | .. currentmodule:: panflute.io
124 |
125 | .. autosummary::
126 |
127 | run_filters
128 | run_filter
129 | toJSONFilter
130 | toJSONFilters
131 | load
132 | dump
133 |
134 | .. currentmodule:: panflute.base
135 |
136 | .. seealso::
137 | The ``walk()`` function has been replaced by the :meth:`.Element.walk`
138 | method of each element. To walk through the entire document,
139 | do ``altered = doc.walk()``.
140 |
141 | .. automodule:: panflute.io
142 | :members:
143 |
144 | .. note::
145 | The *action* functions have a few rules:
146 |
147 | - They are called as ``action(element, doc)`` so they must accept at
148 | least two arguments.
149 | - Additional arguments can be passed through the ``**kwargs**`` of
150 | ``toJSONFilter`` and ``toJSONFilters``.
151 | - They can return either an element, a list, or ``None``.
152 | - If they return ``None``, the document will keep the same element
153 | as before (although it might have been modified).
154 | - If they return another element, it will take the place of the
155 | received element.
156 | - If they return ``[]`` (an empty list), they will be deleted from the
157 | document. Note that you can delete a row from a table or an item from
158 | a list, but you cannot delete the caption from a table (you can
159 | make it empty though).
160 | - If the received element is a block or inline element, they may return
161 | a list of elements of the same base class, which will take the place
162 | of the received element.
163 |
164 | "Batteries included" functions
165 | ******************************
166 |
167 | These are functions commonly used when writing more complex filters
168 |
169 | .. currentmodule:: panflute.tools
170 |
171 | .. autosummary::
172 |
173 | stringify
174 | convert_text
175 | yaml_filter
176 | debug
177 | shell
178 |
179 |
180 | See also ``Doc.get_metadata`` and ``Element.replace_keyword``
181 |
182 | .. automodule:: panflute.tools
183 | :members:
184 |
--------------------------------------------------------------------------------
/examples/pandocfilters/gabc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Pandoc filter to convert code blocks with class "gabc" to LaTeX
4 | \\gabcsnippet commands in LaTeX output, and to images in HTML output.
5 | Assumes Ghostscript, LuaLaTeX, [Gregorio](http://gregorio-project.github.io/)
6 | and a reasonable selection of LaTeX packages are installed.
7 | """
8 |
9 | import os
10 | from sys import getfilesystemencoding, stderr
11 | from subprocess import Popen, call, PIPE, DEVNULL
12 | from hashlib import sha1
13 | from pandocfilters import toJSONFilter, RawBlock, RawInline, Para, Image
14 |
15 |
16 | IMAGEDIR = "tmp_gabc"
17 | LATEX_DOC = """\\documentclass{article}
18 | \\usepackage{libertine}
19 | \\usepackage[autocompile]{gregoriotex}
20 | \\pagestyle{empty}
21 | \\begin{document}
22 | %s
23 | \\end{document}
24 | """
25 |
26 |
27 | def sha(code):
28 | """Returns sha1 hash of the code"""
29 | return sha1(code.encode(getfilesystemencoding())).hexdigest()
30 |
31 |
32 | def latex(code):
33 | """LaTeX inline"""
34 | return RawInline('latex', code)
35 |
36 |
37 | def latexblock(code):
38 | """LaTeX block"""
39 | return RawBlock('latex', code)
40 |
41 |
42 | def htmlblock(code):
43 | """Html block"""
44 | return RawBlock('html', code)
45 |
46 |
47 | def latexsnippet(code, kvs):
48 | """Take in account key/values"""
49 | snippet = ''
50 | staffsize = int(kvs['staffsize']) if 'staffsize' in kvs else 17
51 | annotationsize = .56 * staffsize
52 | if 'mode' in kvs:
53 | snippet = (
54 | "\\greannotation{{\\fontsize{%s}{%s}\\selectfont{}%s}}\n" %
55 | (annotationsize, annotationsize, kvs['mode'])
56 | ) + snippet
57 | if 'annotation' in kvs:
58 | snippet = (
59 | "\\grechangedim{annotationseparation}{%s mm}{0}\n"
60 | "\\greannotation{{\\fontsize{%s}{%s}\\selectfont{}%s}}\n" %
61 | (staffsize / 34, annotationsize, annotationsize, kvs['annotation'])
62 | ) + snippet
63 | snippet = (
64 | "\\grechangestaffsize{%s}\n" % staffsize +
65 | "\\def\\greinitialformat#1{{\\fontsize{%s}{%s}\\selectfont{}#1}}" %
66 | (2.75 * staffsize, 2.75 * staffsize)
67 | ) + snippet
68 | snippet = "\\setlength{\\parskip}{0pt}\n" + snippet + code
69 | return snippet
70 |
71 |
72 | def latex2png(snippet, outfile):
73 | """Compiles a LaTeX snippet to png"""
74 | pngimage = os.path.join(IMAGEDIR, outfile + '.png')
75 | environment = os.environ
76 | environment['openout_any'] = 'a'
77 | environment['shell_escape_commands'] = \
78 | "bibtex,bibtex8,kpsewhich,makeindex,mpost,repstopdf,gregorio"
79 | proc = Popen(
80 | ["lualatex", '-output-directory=' + IMAGEDIR],
81 | stdin=PIPE,
82 | stdout=DEVNULL,
83 | env=environment
84 | )
85 | proc.stdin.write(
86 | (
87 | LATEX_DOC % (snippet)
88 | ).encode("utf-8")
89 | )
90 | proc.communicate()
91 | proc.stdin.close()
92 | call(["pdfcrop", os.path.join(IMAGEDIR, "texput.pdf")], stdout=DEVNULL)
93 | call(
94 | [
95 | "gs",
96 | "-sDEVICE=pngalpha",
97 | "-r144",
98 | "-sOutputFile=" + pngimage,
99 | os.path.join(IMAGEDIR, "texput-crop.pdf"),
100 | ],
101 | stdout=DEVNULL,
102 | )
103 |
104 |
105 | def png(contents, latex_command):
106 | """Creates a png if needed."""
107 | outfile = sha(contents + latex_command)
108 | src = os.path.join(IMAGEDIR, outfile + '.png')
109 | if not os.path.isfile(src):
110 | try:
111 | os.mkdir(IMAGEDIR)
112 | stderr.write('Created directory ' + IMAGEDIR + '\n')
113 | except OSError:
114 | pass
115 | latex2png(latex_command + "{" + contents + "}", outfile)
116 | stderr.write('Created image ' + src + '\n')
117 | return src
118 |
119 |
120 | def gabc(key, value, fmt, meta): # pylint:disable=I0011,W0613
121 | """Handle gabc file inclusion and gabc code block."""
122 | if key == 'Code':
123 | [[ident, classes, kvs], contents] = value # pylint:disable=I0011,W0612
124 | kvs = {key: value for key, value in kvs}
125 | if "gabc" in classes:
126 | if fmt == "latex":
127 | if ident == "":
128 | label = ""
129 | else:
130 | label = '\\label{' + ident + '}'
131 | return latex(
132 | "\n\\smallskip\n{%\n" +
133 | latexsnippet('\\gregorioscore{' + contents + '}', kvs) +
134 | "%\n}" +
135 | label
136 | )
137 | else:
138 | infile = contents + (
139 | '.gabc' if '.gabc' not in contents else ''
140 | )
141 | with open(infile, 'r') as doc:
142 | code = doc.read().split('%%\n')[1]
143 | return [Image(['', [], []], [], [
144 | png(
145 | contents,
146 | latexsnippet('\\gregorioscore', kvs)
147 | ),
148 | ""
149 | ])]
150 | elif key == 'CodeBlock':
151 | [[ident, classes, kvs], contents] = value
152 | kvs = {key: value for key, value in kvs}
153 | if "gabc" in classes:
154 | if fmt == "latex":
155 | if ident == "":
156 | label = ""
157 | else:
158 | label = '\\label{' + ident + '}'
159 | return [latexblock(
160 | "\n\\smallskip\n{%\n" +
161 | latexsnippet('\\gabcsnippet{' + contents + '}', kvs) +
162 | "%\n}" +
163 | label
164 | )]
165 | else:
166 | return Para([Image(['', [], []], [], [
167 | png(
168 | contents,
169 | latexsnippet('\\gabcsnippet', kvs)
170 | ),
171 | ""
172 | ])])
173 |
174 |
175 | if __name__ == "__main__":
176 | toJSONFilter(gabc)
177 |
--------------------------------------------------------------------------------