├── tests ├── __init__.py ├── borrar.md ├── test_panfl │ ├── foo │ │ ├── __init__.py │ │ └── test_filter.py │ └── bar │ │ └── test_filter.py ├── input │ ├── awesome-c │ │ ├── make.txt │ │ ├── source.txt │ │ ├── notes.txt │ │ ├── profiler.py │ │ └── test.py │ ├── barcode │ │ ├── make.txt │ │ ├── source.txt │ │ ├── notes.txt │ │ └── test.py │ └── portugal │ │ ├── make.txt │ │ ├── source.txt │ │ ├── notes.txt │ │ └── test.py ├── requirements.txt ├── sample_files │ ├── native │ │ ├── README.md │ │ ├── students.native │ │ └── nordics.native │ ├── example1 │ │ └── example.bib │ ├── example3 │ │ ├── example.md │ │ └── results.md │ ├── example4 │ │ └── example.md │ ├── example2 │ │ └── example.md │ ├── fenced │ │ └── example.md │ ├── heavy_metadata │ │ └── example.md │ └── pandoc-2.11 │ │ └── example.md ├── dependency │ └── dependency.py ├── temp_benchmark.txt ├── temp_panflute.txt ├── README.md ├── test_get_option.py ├── filters │ ├── do_nothing.py │ ├── underline.py │ └── assert_env.py ├── md2json.txt ├── test_context_import.py ├── autofilter │ └── input.md ├── test_equality.py ├── test_regressions.py ├── test_elements.py ├── test_env.py ├── test_stringify.py ├── test_get_metadata.py ├── test_examples_run.py ├── test_fenced.py ├── test_walk.py ├── fenced │ ├── input.json │ └── output.json ├── test_basics.py ├── test_panfl.py ├── test_standalone.py └── test_convert_text.py ├── requirements.txt ├── MANIFEST.in ├── docs ├── howtobuild.txt ├── source │ ├── _static │ │ ├── valid_file.md │ │ ├── autofilters.md │ │ ├── header-level-1.py │ │ ├── remove-tables.py │ │ ├── css │ │ │ └── panflute.css │ │ ├── emph2strikeout.py │ │ ├── move-tables.py │ │ ├── template.py │ │ ├── emph-last-row.py │ │ ├── toc.py │ │ ├── fenced-template.py │ │ ├── wiki.py │ │ ├── csv-tables.py │ │ ├── example.md │ │ └── include.py │ ├── _templates │ │ ├── layout.html │ │ └── sidebar.html │ ├── about.rst │ ├── install.rst │ ├── example.html │ ├── index.rst │ └── code.rst └── help.txt ├── examples ├── input │ ├── caps-sample.md │ ├── metavars-sample.md │ ├── table-sample.md │ ├── deemph-sample.md │ ├── myemph-sample.md │ ├── build-table-sample.md │ ├── theorem-sample.md │ ├── graphviz-sample.md │ ├── comments-sample.md │ ├── deflists-sample.md │ ├── Makefile │ ├── headers.md │ ├── lilypond-sample.md │ ├── gabc-sample.md │ ├── gabc-score.gabc │ ├── tikz-sample.md │ ├── plantuml-sample.md │ ├── abc-sample.md │ ├── LICENSE.txt │ └── lilypond-score.ly ├── panflute_output.html ├── benchmark_output.html ├── panflute │ ├── headers.py │ ├── table-better.py │ ├── caps.py │ ├── myemph.py │ ├── table.py │ ├── deemph.py │ ├── deflists.py │ ├── comments.py │ ├── metavars.py │ ├── theorem.py │ ├── graphviz.py │ ├── abc.py │ ├── build-table.py │ ├── tikz.py │ ├── lilypond.py │ └── gabc.py ├── pandocfilters │ ├── caps.py │ ├── deemph.py │ ├── myemph.py │ ├── deflists.py │ ├── comments.py │ ├── metavars.py │ ├── theorem.py │ ├── LICENSE.txt │ ├── graphviz.py │ ├── abc.py │ ├── plantuml.py │ ├── tikz.py │ ├── lilypond.py │ └── gabc.py └── make.py ├── panflute ├── version.py ├── borrar.txt ├── __init__.py └── utils.py ├── extra ├── panflute.sublime-snippet └── panflute-fenced.sublime-snippet ├── .github └── workflows │ ├── force-publish.yml │ ├── publish.yml │ └── run_tests.yml ├── .gitignore ├── LICENSE ├── DISTRIBUTION.md ├── misc └── CLI_Wrapper.md ├── setup.py ├── README.md └── CONTRIBUTING.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/borrar.md: -------------------------------------------------------------------------------- 1 | 2 | @asd -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml 2 | click 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /docs/howtobuild.txt: -------------------------------------------------------------------------------- 1 | sphinx-build -b html source build/html -------------------------------------------------------------------------------- /examples/input/caps-sample.md: -------------------------------------------------------------------------------- 1 | 2 | This is the caps sample with lorem. -------------------------------------------------------------------------------- /examples/panflute_output.html: -------------------------------------------------------------------------------- 1 |

THIS IS THE CAPS SAMPLE WITH ÄÜÖ.

2 | -------------------------------------------------------------------------------- /tests/test_panfl/foo/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_filter import main as test_filter -------------------------------------------------------------------------------- /examples/benchmark_output.html: -------------------------------------------------------------------------------- 1 |

THIS IS THE CAPS SAMPLE WITH Äüö.

2 | -------------------------------------------------------------------------------- /panflute/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Panflute version 3 | """ 4 | 5 | __version__ = '2.2.4' 6 | -------------------------------------------------------------------------------- /examples/input/metavars-sample.md: -------------------------------------------------------------------------------- 1 | --- 2 | author: Caleb Hyde 3 | --- 4 | 5 | # %{author} 6 | -------------------------------------------------------------------------------- /tests/input/awesome-c/make.txt: -------------------------------------------------------------------------------- 1 | pandoc --smart --parse-raw --to=json index.md > benchmark.json -------------------------------------------------------------------------------- /tests/input/barcode/make.txt: -------------------------------------------------------------------------------- 1 | pandoc --smart --parse-raw --to=json index.md > benchmark.json -------------------------------------------------------------------------------- /tests/input/portugal/make.txt: -------------------------------------------------------------------------------- 1 | pandoc --smart --parse-raw --to=json index.md > benchmark.json -------------------------------------------------------------------------------- /tests/input/barcode/source.txt: -------------------------------------------------------------------------------- 1 | https://github.com/mskyttner/actg-barcode/blob/gh-pages/index.md -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pandocfilters 2 | configparser 3 | pytest-cov 4 | docutils 5 | Pygments 6 | -------------------------------------------------------------------------------- /docs/source/_static/valid_file.md: -------------------------------------------------------------------------------- 1 | ### Header lvl 3 2 | 3 | This will be *included* to the root **element** -------------------------------------------------------------------------------- /tests/input/awesome-c/source.txt: -------------------------------------------------------------------------------- 1 | https://github.com/uhub/awesome-c/blob/6c2ecc1d5b0dc6d700eb7da592a11eabecfd2003/README.md -------------------------------------------------------------------------------- /docs/source/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | {% set css_files = css_files + [ "_static/css/panflute.css" ] %} 3 | -------------------------------------------------------------------------------- /examples/input/table-sample.md: -------------------------------------------------------------------------------- 1 | Example: 2 | 3 | | Variable | Mean | 4 | |----------|------| 5 | | Price | 10 | 6 | | Weight | 12 | -------------------------------------------------------------------------------- /examples/input/deemph-sample.md: -------------------------------------------------------------------------------- 1 | This is the deemph sample. 2 | 3 | This is *emphasis with UNICODE REMOVED* text which will be shown in caps. 4 | -------------------------------------------------------------------------------- /tests/input/portugal/source.txt: -------------------------------------------------------------------------------- 1 | https://github.com/JJ/github-country-data/blob/9260760e7a7208de5cab6fdf263843ecb2bb1db9/formatted/top-Portugal.md -------------------------------------------------------------------------------- /tests/input/barcode/notes.txt: -------------------------------------------------------------------------------- 1 | Had to modify the markdown file a little: 2 | 3 | - Replace horizontal lines "---" with "----" to avoid confusion with yaml -------------------------------------------------------------------------------- /examples/input/myemph-sample.md: -------------------------------------------------------------------------------- 1 | \newcommand{\myemph}[1]{ START-MYEMPH #1 END-MYEMPH } 2 | 3 | 4 | This is same text with *emphasis with UNICODE REMOVED*. -------------------------------------------------------------------------------- /tests/input/awesome-c/notes.txt: -------------------------------------------------------------------------------- 1 | Had to modify the markdown file a little: 2 | 3 | - Replace horizontal lines "---" with "----" to avoid confusion with yaml -------------------------------------------------------------------------------- /tests/input/portugal/notes.txt: -------------------------------------------------------------------------------- 1 | Had to modify the markdown file a little: 2 | 3 | - Replace horizontal lines "---" with "----" to avoid confusion with yaml -------------------------------------------------------------------------------- /docs/source/_static/autofilters.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Some title 3 | panflute-filters: [remove-tables, include] 4 | panflute-path: 'panflute/docs/source' 5 | ... 6 | 7 | Lorem ipsum 8 | -------------------------------------------------------------------------------- /examples/input/build-table-sample.md: -------------------------------------------------------------------------------- 1 | Beginning of document... 2 | 3 | ```table_without_header 4 | lorem ipsum 5 | ``` 6 | 7 | ```table_only_header 8 | lorem ipsum 9 | ``` 10 | 11 | ...end of document -------------------------------------------------------------------------------- /tests/sample_files/native/README.md: -------------------------------------------------------------------------------- 1 | The files `nordics.native`, `planets.native`, `students.native` are licensed under CC0. 2 | 3 | c.f. https://github.com/sergiocorreia/panflute/pull/172#issuecomment-736252008 4 | -------------------------------------------------------------------------------- /tests/dependency/dependency.py: -------------------------------------------------------------------------------- 1 | 2 | def test_function(): 3 | return True 4 | 5 | class test_class(): 6 | 7 | def __init__(self): 8 | pass 9 | 10 | def test_me(self): 11 | return "I'm a test" 12 | -------------------------------------------------------------------------------- /examples/input/theorem-sample.md: -------------------------------------------------------------------------------- 1 | --- 2 | header-includes: 3 | - \newtheorem{theorem}{Theorem} 4 | --- 5 | 6 | Regular Text. 7 | 8 |
9 | 10 | This is my theorem with UNICODE REMOVED. 11 |
12 | 13 | Regular Text. 14 | -------------------------------------------------------------------------------- /tests/temp_benchmark.txt: -------------------------------------------------------------------------------- 1 | A table without header121212121231231231231111A table with captionAdd-onsCategoryDetailsAmountCategory1Hourly$20Category2Hourly$25Category3Fixed$30Another table with captionDemonstration of simple table syntax.RightLeftCenterDefault121212121231231231231111 -------------------------------------------------------------------------------- /tests/temp_panflute.txt: -------------------------------------------------------------------------------- 1 | A table without header121212121231231231231111A table with captionCategoryDetailsAmountCategory1Hourly$20Category2Hourly$25Category3Fixed$30Add-onsAnother table with captionRightLeftCenterDefault121212121231231231231111Demonstration of simple table syntax. -------------------------------------------------------------------------------- /examples/input/graphviz-sample.md: -------------------------------------------------------------------------------- 1 | Use this 2 | 3 | 4 | ``` 5 | digraph G {Hello->World} 6 | ``` 7 | 8 | to get 9 | 10 | ```graphviz 11 | digraph G {Hello->World} 12 | ``` 13 | 14 | with with UNICODE REMOVED 15 | 16 | ```graphviz 17 | digraph G {Hello->World with UNICODE REMOVED} 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/input/comments-sample.md: -------------------------------------------------------------------------------- 1 | 2 | Regular text with UNICODE REMOVED. 3 | 4 | 5 | 6 | Some text 7 | 8 | 9 | 10 | This is a comment with UNICODE REMOVED 11 | 12 | 13 | 14 | This is regular text again. 15 | 16 | -------------------------------------------------------------------------------- /tests/sample_files/example1/example.bib: -------------------------------------------------------------------------------- 1 | @article{foo, 2 | title={Lorem}, 3 | author={John, Smith}, 4 | journal={Foobar}, 5 | volume={1}, 6 | pages={2} 7 | } 8 | 9 | @article{bar, 10 | title={Lorems}, 11 | author={John, Smith II}, 12 | journal={Foobar}, 13 | volume={1}, 14 | pages={2} 15 | } 16 | -------------------------------------------------------------------------------- /docs/source/about.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ================= 3 | 4 | Feel free to submit push requests. `This guide `_ has some helpful contributing guidelines! 5 | 6 | License 7 | ~~~~~~~~~~~~~~~~~ 8 | 9 | BSD3 license (following pandocfilter by @jgm) 10 | -------------------------------------------------------------------------------- /docs/source/_static/header-level-1.py: -------------------------------------------------------------------------------- 1 | """ 2 | Set all headers to level 1 3 | """ 4 | 5 | from panflute import * 6 | 7 | def action(elem, doc): 8 | if isinstance(elem, Header): 9 | elem.level = 1 10 | 11 | def main(doc=None): 12 | return run_filter(action, doc=doc) 13 | 14 | if __name__ == '__main__': 15 | main() 16 | -------------------------------------------------------------------------------- /docs/source/_static/remove-tables.py: -------------------------------------------------------------------------------- 1 | """ 2 | Remove all tables 3 | """ 4 | 5 | from panflute import * 6 | 7 | 8 | def action(elem, doc): 9 | if isinstance(elem, Table): 10 | return [] 11 | 12 | 13 | def main(doc=None): 14 | return run_filter(action, doc=doc) 15 | 16 | 17 | if __name__ == '__main__': 18 | main() 19 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | * install packages from [`setup.py`](../setup.py) -> `extras_require` -> `'test'`, 2 | (if you use pip instead of conda you can do it right from pip), 3 | * change CWD in terminal to the root of the Panflute repo, 4 | * run: 5 | ``` 6 | py.test --cov=panflute tests && coverage html && cd htmlcov && index.html && cd .. 7 | ``` 8 | -------------------------------------------------------------------------------- /tests/test_panfl/bar/test_filter.py: -------------------------------------------------------------------------------- 1 | import panflute as pf 2 | 3 | 4 | def action(elem, doc): 5 | if isinstance(elem, pf.Math): 6 | elem.text = elem.text.replace('-', '+') + doc.format 7 | 8 | 9 | def main(doc=None): 10 | return pf.run_filter(action, doc=doc) 11 | 12 | 13 | if __name__ == '__main__': 14 | main() 15 | -------------------------------------------------------------------------------- /tests/test_panfl/foo/test_filter.py: -------------------------------------------------------------------------------- 1 | import panflute as pf 2 | 3 | 4 | def action(elem, doc): 5 | if isinstance(elem, pf.Math): 6 | elem.text = elem.text.replace('-', '+') + doc.format 7 | 8 | 9 | def main(doc=None): 10 | return pf.run_filter(action, doc=doc) 11 | 12 | 13 | if __name__ == '__main__': 14 | main() 15 | -------------------------------------------------------------------------------- /examples/input/deflists-sample.md: -------------------------------------------------------------------------------- 1 | Some Definitions 2 | 3 | Term 1 4 | 5 | : Definition 1 6 | 7 | Term 2 with *inline markup* 8 | 9 | : Definition 2 10 | 11 | { some code, part of Definition 2 } 12 | 13 | Third paragraph of definition 2. 14 | 15 | Term with UNICODE REMOVED 16 | 17 | : Definition with UNICODE REMOVED 18 | 19 | 20 | Regular Text. -------------------------------------------------------------------------------- /examples/input/Makefile: -------------------------------------------------------------------------------- 1 | 2 | ALLSAMPLES = $(basename $(wildcard *-sample.md)) 3 | ALLPDF = $(addsuffix .pdf,$(ALLSAMPLES)) 4 | 5 | PYTHON=/usr/bin/python2 6 | 7 | all: ${ALLPDF} 8 | 9 | %.pdf: %.md 10 | echo $(subst -sample.md,.py,$<) 11 | pandoc --filter ./$(subst -sample.md,.py,$<) $< -o $@ 12 | 13 | clean: 14 | $(RM) -f ${ALLPDF} *.pyc 15 | $(RM) -rf *-images 16 | -------------------------------------------------------------------------------- /docs/source/_static/css/panflute.css: -------------------------------------------------------------------------------- 1 | @import url("theme.css"); 2 | 3 | 4 | .xfunction { 5 | border-bottom: 30px solid #ff0000; 6 | padding-bottom: 100px; 7 | padding-top: 10px; 8 | } 9 | 10 | .class { 11 | padding-bottom: 20px; 12 | } 13 | 14 | .wy-nav-content { 15 | max-width: 90%; 16 | } 17 | 18 | div.body 19 | { 20 | max-width: 1200px; 21 | } 22 | -------------------------------------------------------------------------------- /docs/source/_static/emph2strikeout.py: -------------------------------------------------------------------------------- 1 | """ 2 | Replace Emph elements with Strikeout elements 3 | """ 4 | 5 | from panflute import * 6 | 7 | 8 | def action(elem, doc): 9 | if isinstance(elem, Emph): 10 | return Strikeout(*elem.content) 11 | 12 | 13 | def main(doc=None): 14 | return run_filter(action, doc=doc) 15 | 16 | 17 | if __name__ == '__main__': 18 | main() 19 | -------------------------------------------------------------------------------- /examples/panflute/headers.py: -------------------------------------------------------------------------------- 1 | from panflute import run_filter, Header 2 | 3 | 4 | def increase_header_level(elem, doc): 5 | if type(elem)==Header: 6 | if elem.level < 6: 7 | elem.level += 1 8 | else: 9 | return [] 10 | 11 | 12 | def main(doc=None): 13 | return run_filter(increase_header_level, doc=doc) 14 | 15 | 16 | if __name__ == "__main__": 17 | main() -------------------------------------------------------------------------------- /examples/pandocfilters/caps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Pandoc filter to convert all regular text to uppercase. 5 | Code, link URLs, etc. are not affected. 6 | """ 7 | 8 | from pandocfilters import toJSONFilter, Str 9 | 10 | 11 | def caps(key, value, format, meta): 12 | if key == 'Str': 13 | return Str(value.upper()) 14 | 15 | if __name__ == "__main__": 16 | toJSONFilter(caps) 17 | -------------------------------------------------------------------------------- /examples/pandocfilters/deemph.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from pandocfilters import walk, toJSONFilter 3 | from caps import caps 4 | 5 | """ 6 | Pandoc filter that causes emphasized text to be displayed 7 | in ALL CAPS. 8 | """ 9 | 10 | 11 | def deemph(key, val, fmt, meta): 12 | if key == 'Emph': 13 | return walk(val, caps, fmt, meta) 14 | 15 | if __name__ == "__main__": 16 | toJSONFilter(deemph) 17 | -------------------------------------------------------------------------------- /examples/panflute/table-better.py: -------------------------------------------------------------------------------- 1 | from panflute import * 2 | 3 | def add_one(e, doc): 4 | if type(e) == TableCell and stringify(e).isdigit(): 5 | chunk = cell.content[0].content[0] 6 | chunk.text = str(int(chunk.text) + 1) 7 | 8 | 9 | def idea(e, doc): 10 | selector = 'Table Items Rows Cells' 11 | 12 | if any(type(a)==pf.Table for a in e.ancestors()): 13 | pass 14 | 15 | 16 | if __name__ == '__main__': 17 | pf.toJSONFilter(add_one) -------------------------------------------------------------------------------- /examples/input/headers.md: -------------------------------------------------------------------------------- 1 | # A level 1 header 2 | 3 | lorem 4 | 5 | ## Level 2 6 | 7 | ### Level 3 8 | 9 | #### Level 4 10 | 11 | ##### Level 5 12 | 13 | ###### Level 6 14 | 15 | ### Level 3 16 | 17 | ###### Another level 6 18 | 19 | # Level 1 20 | 21 | ## Level 2 22 | 23 | Done. 24 | 25 | To run, type: 26 | 27 | ``` 28 | pandoc input\headers.md --filter=./panflute/headers.py -o headers.html && headers.html 29 | ``` 30 | 31 | (with "panflute/examples" as the working path) -------------------------------------------------------------------------------- /examples/input/lilypond-sample.md: -------------------------------------------------------------------------------- 1 | --- 2 | header-includes: 3 | 4 | - \usepackage{lyluatex} 5 | 6 | --- 7 | 8 | 9 | Use this 10 | 11 | ~~~~~~ 12 | ```ly 13 | \relative c' { c4 d e f g a b c } 14 | ``` 15 | ~~~~~~ 16 | 17 | to get 18 | 19 | ```ly 20 | \relative c' { c4 d e f g a b c } 21 | ``` 22 | 23 | and this 24 | 25 | ~~~~~~ 26 | `lilypond-score`{.ly staffsize=12} 27 | ~~~~~~ 28 | 29 | to get the score in `ly-score.ly` : 30 | 31 | `lilypond-score`{.ly staffsize=12} 32 | -------------------------------------------------------------------------------- /examples/panflute/caps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Pandoc filter to convert all regular text to uppercase. 5 | Code, link URLs, etc. are not affected. 6 | """ 7 | 8 | from panflute import run_filter, Str 9 | 10 | 11 | def caps(elem, doc): 12 | if type(elem) == Str: 13 | elem.text = elem.text.upper() 14 | return elem 15 | 16 | 17 | def main(doc=None): 18 | return run_filter(caps, doc=doc) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /tests/test_get_option.py: -------------------------------------------------------------------------------- 1 | import panflute as pf 2 | 3 | def test_get_variable(): 4 | 5 | doc = pf.Doc(metadata={"a": pf.MetaString("x"), 6 | "b": pf.MetaMap(c=pf.MetaString("y"))}) 7 | 8 | assert pf.get_option(default="a") == "a" 9 | assert pf.get_option({"a": 1}, "a") == 1 10 | assert pf.get_option({"a": None}, "a", default=2) == 2 11 | assert pf.get_option({"a": None}, "a", doc, "a") == "x" 12 | assert pf.get_option(doc=doc, doc_tag="b.c") == "y" 13 | -------------------------------------------------------------------------------- /examples/pandocfilters/myemph.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from pandocfilters import toJSONFilter, RawInline 3 | 4 | """ 5 | Pandoc filter that causes emphasis to be rendered using 6 | the custom macro '\myemph{...}' rather than '\emph{...}' 7 | in latex. Other output formats are unaffected. 8 | """ 9 | 10 | 11 | def latex(s): 12 | return RawInline('latex', s) 13 | 14 | 15 | def myemph(k, v, f, meta): 16 | if k == 'Emph' and f == 'latex': 17 | return [latex('\\myemph{')] + v + [latex('}')] 18 | 19 | if __name__ == "__main__": 20 | toJSONFilter(myemph) 21 | -------------------------------------------------------------------------------- /examples/panflute/myemph.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import panflute as pf 3 | 4 | """ 5 | Pandoc filter that causes emphasis to be rendered using 6 | the custom macro '\myemph{...}' rather than '\emph{...}' 7 | in latex. Other output formats are unaffected. 8 | """ 9 | 10 | 11 | def latex(s): 12 | return pf.RawInline(s, format='latex') 13 | 14 | 15 | def myemph(e, doc): 16 | if type(e)==pf.Emph and doc.format=='latex': 17 | return pf.Span(latex('\\myemph{'), *e.items, latex('}')) 18 | 19 | 20 | if __name__ == "__main__": 21 | pf.toJSONFilter(myemph) 22 | -------------------------------------------------------------------------------- /examples/input/gabc-sample.md: -------------------------------------------------------------------------------- 1 | --- 2 | header-includes: 3 | 4 | - \usepackage{libertine} 5 | - \usepackage[autocompile]{gregoriotex} 6 | 7 | --- 8 | 9 | 10 | Use this 11 | 12 | ~~~~~~ 13 | ```gabc 14 | (c4) A(f)ve(c) Ma(d)rí(dh'!iv)a.(h.) (::) 15 | ``` 16 | ~~~~~~ 17 | 18 | to get 19 | 20 | ```gabc 21 | (c4) A(f)ve(c) Ma(d)rí(dh'!iv)a.(h.) (::) 22 | ``` 23 | 24 | and this 25 | 26 | ~~~~~~ 27 | `gabc-score`{.gabc staffsize=12 annotation=Off. mode=2.} 28 | ~~~~~~ 29 | 30 | to get the score in `gabc-score.gabc` : 31 | 32 | `gabc-score`{.gabc staffsize=12 annotation=Off. mode=2.} 33 | -------------------------------------------------------------------------------- /docs/source/_static/move-tables.py: -------------------------------------------------------------------------------- 1 | """ 2 | Move tables to where the string $tables is. 3 | """ 4 | 5 | from panflute import * 6 | 7 | 8 | def prepare(doc): 9 | doc.backmatter = [] 10 | 11 | 12 | def action(elem, doc): 13 | if isinstance(elem, Table): 14 | doc.backmatter.append(elem) 15 | return [] 16 | 17 | 18 | def finalize(doc): 19 | div = Div(*doc.backmatter) 20 | doc = doc.replace_keyword('$tables', div) 21 | 22 | 23 | def main(doc=None): 24 | return run_filter(action, prepare, finalize, doc=doc) 25 | 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /examples/panflute/table.py: -------------------------------------------------------------------------------- 1 | import panflute as pf 2 | 3 | def add_one(e, doc): 4 | if type(e)==pf.Table: 5 | for row in e.items: 6 | for cell in row: 7 | if len(cell) == 1 and len(cell[0].items)==1: 8 | text = cell[0].items[0].text 9 | if text.isdigit(): 10 | cell[0].items[0].text = str(int(text)+1) 11 | return e 12 | 13 | def idea(e, doc): 14 | selector = 'Table Items Rows Cells' 15 | 16 | if any(type(a)==pf.Table for a in e.ancestors()): 17 | pass 18 | 19 | if __name__ == '__main__': 20 | pf.toJSONFilter(add_one) -------------------------------------------------------------------------------- /examples/pandocfilters/deflists.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Pandoc filter to convert definition lists to bullet 5 | lists with the defined terms in strong emphasis (for 6 | compatibility with standard markdown). 7 | """ 8 | 9 | from pandocfilters import toJSONFilter, BulletList, Para, Strong 10 | 11 | 12 | def deflists(key, value, format, meta): 13 | if key == 'DefinitionList': 14 | return BulletList([tobullet(t, d) for [t, d] in value]) 15 | 16 | 17 | def tobullet(term, defs): 18 | return([Para([Strong(term)])] + [b for d in defs for b in d]) 19 | 20 | 21 | if __name__ == "__main__": 22 | toJSONFilter(deflists) 23 | -------------------------------------------------------------------------------- /docs/help.txt: -------------------------------------------------------------------------------- 1 | REST Primer: 2 | http://www.sphinx-doc.org/en/stable/rest.html 3 | 4 | REST and Sphinx Cheatsheet: 5 | http://openalea.gforge.inria.fr/doc/openalea/doc/_build/html/source/sphinx/rest_syntax.html#restructured-text-rest-and-sphinx-cheatsheet 6 | 7 | Sphinx commands: 8 | https://pythonhosted.org/an_example_pypi_project/sphinx.html 9 | 10 | Sphinx domains (used to get links to other python packages and to the stdlib): 11 | http://www.sphinx-doc.org/en/stable/domains.html 12 | 13 | EG: 14 | 15 | :py:mod:`sys` 16 | :py:data:`sys.stdin` 17 | :py:obj:`sys.stdin` 18 | 19 | :mod:`sys` 20 | :data:`sys.stdin` 21 | :obj:`sys.stdin` 22 | 23 | :class:`zipfile.ZipFile` 24 | -------------------------------------------------------------------------------- /examples/input/gabc-score.gabc: -------------------------------------------------------------------------------- 1 | name:Ave María; 2 | %% 3 | (c4) A(fg/hgh fhf/gvFE. d!ef!g'h fhf/gh)ve(g.) *(;) 4 | Ma(h)rí(jj//jjjvH'GFgh!jvHF'g)a,(g.) (:) 5 | 6 | grá(g_d/fv.efd de!f'g)ti(fg)a(g) ple(ghhg)na,(g.) (:) 7 | 8 | Dó(jvIHk_j ijh/ig./hi/jg.,jvIHk_j ijh/ig./hi/jg)mi(fg)nus(g.) (,) 9 | te(ggghvGFg_d//gjhi)cum :(hg..) (:) 10 | 11 | be(h)ne(jj)dí(jk/lvlk)cta(jkkj) tu(ji/jkhhg.) (;) 12 | in(g) mu(gjjvI'H)li(hkj)é(jjvI'H)ri(jvIHij)bus,(i.) (:) 13 | 14 | et(g) be(i)ne(jkj)dí(hjgh)ctus(fg!hvhg.) (;) 15 | fru(g_f/ghffe)ctus(d.) ven(de!f'g/hvF'ED,de!f'g)tris(fhfg) tu(ghhg)i.(g.) (::) 16 | 17 | T. P. Al(h!iwji~)le(jkJH'//gi. hjIH'//g!jj/hig___)lú(ghg___)ia.(g.) (::) 18 | -------------------------------------------------------------------------------- /tests/filters/do_nothing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pandoc filter using panflute 3 | """ 4 | 5 | import panflute as pf 6 | 7 | 8 | def prepare(doc): 9 | pass 10 | 11 | 12 | def action(elem, doc): 13 | if isinstance(elem, pf.Element) and doc.format == 'latex': 14 | pass 15 | # return None -> element unchanged 16 | # return [] -> delete element 17 | 18 | 19 | def finalize(doc): 20 | pass 21 | 22 | 23 | def main(doc=None): 24 | return pf.run_filters([action], 25 | prepare=prepare, 26 | finalize=finalize, 27 | doc=doc) 28 | 29 | 30 | if __name__ == '__main__': 31 | main() 32 | -------------------------------------------------------------------------------- /docs/source/_static/template.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pandoc filter using panflute 3 | """ 4 | 5 | import panflute as pf 6 | 7 | 8 | def prepare(doc): 9 | pass 10 | 11 | 12 | def action(elem, doc): 13 | if isinstance(elem, pf.Element) and doc.format == 'latex': 14 | pass 15 | # return None -> element unchanged 16 | # return [] -> delete element 17 | 18 | 19 | def finalize(doc): 20 | pass 21 | 22 | 23 | def main(doc=None): 24 | return pf.run_filter(action, 25 | prepare=prepare, 26 | finalize=finalize, 27 | doc=doc) 28 | 29 | 30 | if __name__ == '__main__': 31 | main() 32 | -------------------------------------------------------------------------------- /docs/source/_static/emph-last-row.py: -------------------------------------------------------------------------------- 1 | """ 2 | Make text in the last row of every table bold 3 | """ 4 | 5 | import panflute as pf 6 | 7 | 8 | def action(elem, doc): 9 | if isinstance(elem, pf.TableRow): 10 | # Exclude table headers (which are not in a list) 11 | if elem.index is None: 12 | return 13 | 14 | if elem.next is None: 15 | pf.debug(elem) 16 | elem.walk(make_emph) 17 | 18 | 19 | def make_emph(elem, doc): 20 | if isinstance(elem, pf.Str): 21 | return pf.Emph(elem) 22 | 23 | 24 | def main(doc=None): 25 | return pf.run_filter(action, doc=doc) 26 | 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /examples/panflute/deemph.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Pandoc filter that causes emphasized text to be displayed 5 | in ALL CAPS. 6 | """ 7 | 8 | from panflute import toJSONFilter, Emph, Str 9 | from caps import caps 10 | 11 | 12 | def deemph(elem, doc): 13 | if type(elem) == Emph: 14 | # Make Str elements in Emph uppercase 15 | elem.walk(caps) 16 | 17 | # Append them to Emph's parent (after the emph) 18 | for i, item in enumerate(elem.content, elem.index + 1): 19 | elem.parent.content.insert(i, item) 20 | 21 | # Delete the emph 22 | return [] 23 | 24 | 25 | if __name__ == "__main__": 26 | toJSONFilter(deemph) 27 | -------------------------------------------------------------------------------- /tests/md2json.txt: -------------------------------------------------------------------------------- 1 | pandoc --smart --parse-raw --to=json 1/pandoc.md > 1/benchmark.json 2 | pandoc --smart --parse-raw --to=json 2/example.md > 2/benchmark.json 3 | 4 | 5 | pandoc --smart --parse-raw --to=json 3/example.md > 3/benchmark.json 6 | 7 | cls & run_tests.py && pandoc --atx-headers --output=3/results.md 3/panflute.json 8 | 9 | 10 | cls & echo [PANDOC] Creating JSON && pandoc --smart --parse-raw --to=json 3/example.md > 3/benchmark.json && echo [RUNNING TEST] && run_tests.py && echo [PANDOC] Creating MD && pandoc --atx-headers --output=3/results.md 3/panflute.json && echo [RESULTS] && type 3\results.md 11 | 12 | 13 | pandoc --smart --parse-raw --to=json fenced/input.md > fenced/input.json -------------------------------------------------------------------------------- /tests/sample_files/example3/example.md: -------------------------------------------------------------------------------- 1 | # The First title 2 | 3 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 4 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 5 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 6 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 7 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 8 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 9 | 10 | ## A subtitle 11 | 12 | lorem 13 | 14 | ## Another subtitle 15 | 16 | lorem ipsum 17 | 18 | # Another title 19 | 20 | Lorem! 21 | 22 | # Last title 23 | 24 | Ipsum! -------------------------------------------------------------------------------- /tests/sample_files/example3/results.md: -------------------------------------------------------------------------------- 1 | Lorem!! ipsum!! dolor!! sit!! amet,!! consectetur!! adipisicing!! 2 | elit,!! sed!! do!! eiusmod!! tempor!! incididunt!! ut!! labore!! et!! 3 | dolore!! magna!! aliqua.!! Ut!! enim!! ad!! minim!! veniam,!! quis!! 4 | nostrud!! exercitation!! ullamco!! laboris!! nisi!! ut!! aliquip!! ex!! 5 | ea!! commodo!! consequat.!! Duis!! aute!! irure!! dolor!! in!! 6 | reprehenderit!! in!! voluptate!! velit!! esse!! cillum!! dolore!! eu!! 7 | fugiat!! nulla!! pariatur.!! Excepteur!! sint!! occaecat!! cupidatat!! 8 | non!! proident,!! sunt!! in!! culpa!! qui!! officia!! deserunt!! 9 | mollit!! anim!! id!! est!! laborum.!! 10 | 11 | lorem!! 12 | 13 | lorem!! ipsum!! 14 | 15 | Lorem!!! 16 | 17 | Ipsum!!! 18 | -------------------------------------------------------------------------------- /extra/panflute.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | element unchanged 18 | # return [] -> delete element 19 | 20 | 21 | def finalize(doc): 22 | ${6:pass} 23 | 24 | 25 | if __name__ == '__main__': 26 | pf.toJSONFilter(action, prepare=prepare, finalize=finalize) 27 | 28 | ]]> 29 | panflute 30 | source.python 31 | Pandoc filter with panflute 32 | -------------------------------------------------------------------------------- /docs/source/_static/toc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add table of contents at the beginning; 3 | uses optional metadata value 'toc-depth' 4 | """ 5 | 6 | from panflute import * 7 | 8 | 9 | def prepare(doc): 10 | doc.toc = BulletList() 11 | doc.depth = int(doc.get_metadata('toc-depth', default=1)) 12 | 13 | 14 | def action(elem, doc): 15 | if isinstance(elem, Header) and elem.level <= doc.depth: 16 | item = ListItem(Plain(*elem.content)) 17 | doc.toc.content.append(item) 18 | 19 | 20 | def finalize(doc): 21 | doc.content.insert(0, doc.toc) 22 | del doc.toc, doc.depth 23 | 24 | 25 | def main(doc=None): 26 | return run_filter(action, prepare=prepare, finalize=finalize, doc=doc) 27 | 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /tests/test_context_import.py: -------------------------------------------------------------------------------- 1 | import os 2 | from panflute.utils import ContextImport 3 | 4 | 5 | def test_all(): 6 | test_context_import() 7 | 8 | 9 | def test_context_import(): 10 | test_file = os.path.join(os.getcwd(), 'tests/dependency/dependency.py') 11 | print("Importing from the file: \n\t", end="") 12 | print(test_file) 13 | with ContextImport(test_file) as module: 14 | test_function_res = module.test_function() 15 | test_class = module.test_class() 16 | test_class_method_test = test_class.test_me() 17 | assert test_function_res == True, "Unexpected result from test function execution" 18 | assert test_class_method_test == "I'm a test", "Unexpected result from test function execution" 19 | 20 | 21 | if __name__ == "__main__": 22 | test_all() 23 | -------------------------------------------------------------------------------- /examples/panflute/deflists.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Pandoc filter to convert definition lists to bullet 5 | lists with the defined terms in strong emphasis (for 6 | compatibility with standard markdown). 7 | """ 8 | 9 | from panflute import toJSONFilter, DefinitionList, BulletList, ListItem, Para, Strong 10 | 11 | 12 | def deflists(elem, doc): 13 | if type(elem) == DefinitionList: 14 | bullets = [tobullet(item) for item in elem.content] 15 | return BulletList(*bullets) 16 | 17 | 18 | def tobullet(item): 19 | ans = [Para(Strong(*item.term))] 20 | for definition in item.definitions: 21 | for block in definition.content: 22 | ans.append(block) 23 | return ListItem(*ans) 24 | 25 | 26 | if __name__ == "__main__": 27 | toJSONFilter(deflists) 28 | -------------------------------------------------------------------------------- /tests/filters/underline.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pandoc filter using panflute 3 | """ 4 | 5 | import panflute as pf 6 | 7 | 8 | def prepare(doc): 9 | pass 10 | 11 | 12 | def action(elem, doc): 13 | if isinstance(elem, pf.Element) and doc.format == 'latex': 14 | pass 15 | # return None -> element unchanged 16 | # return [] -> delete element 17 | 18 | 19 | def strong2underline(elem, doc): 20 | if isinstance(elem, pf.Strong): 21 | return pf.Underline(*elem.content) 22 | 23 | 24 | def finalize(doc): 25 | pass 26 | 27 | 28 | def main(doc=None): 29 | return pf.run_filters([action, strong2underline], 30 | prepare=prepare, 31 | finalize=finalize, 32 | doc=doc) 33 | 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /examples/panflute/comments.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import panflute as pf 3 | import re 4 | 5 | """ 6 | Pandoc filter that causes everything between 7 | '' and '' 8 | to be ignored. The comment lines must appear on 9 | lines by themselves, with blank lines surrounding 10 | them. 11 | """ 12 | 13 | def prepare(doc): 14 | doc.ignore = False 15 | 16 | def comment(el, doc): 17 | is_relevant = (type(el) == pf.RawBlock) and (el.format == 'html') 18 | if is_relevant and re.search("", el.text): 19 | doc.ignore = True 20 | if doc.ignore: 21 | if is_relevant and re.search("", el.text): 22 | doc.ignore = False 23 | return [] 24 | 25 | if __name__ == "__main__": 26 | pf.toJSONFilter(comment, prepare=prepare) 27 | -------------------------------------------------------------------------------- /tests/autofilter/input.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Some Title 3 | author: Some Author 4 | panflute-echo: "Hello world!" 5 | panflute-verbose: False 6 | panflute-filters: [caps, headers] 7 | panflute-path: 'C:\git\panflute\examples\panflute' 8 | ... 9 | 10 | # This is a Level 1 title 11 | 12 | ## Level 2 13 | 14 | ### Level 3 15 | 16 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 17 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 18 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 19 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 20 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 21 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 22 | 23 | ## Level 2 24 | 25 | Nobody expects... 26 | -------------------------------------------------------------------------------- /tests/sample_files/example4/example.md: -------------------------------------------------------------------------------- 1 | # A table without header 2 | 3 | ------- ------ ---------- ------- 4 | 12 12 12 12 5 | 123 123 123 123 6 | 1 1 1 1 7 | ------- ------ ---------- ------- 8 | 9 | 10 | # A table with caption 11 | 12 | : Add-ons 13 | 14 | Category | Details | Amount 15 | --------------| ---------|-----: 16 | Category1 | Hourly | $20 17 | Category2 | Hourly | $25 18 | Category3 | Fixed | $30 19 | 20 | # Another table with caption 21 | 22 | 23 | Right Left Center Default 24 | ------- ------ ---------- ------- 25 | 12 12 12 12 26 | 123 123 123 123 27 | 1 1 1 1 28 | 29 | Table: Demonstration of simple table syntax. 30 | 31 | -------------------------------------------------------------------------------- /docs/source/_static/fenced-template.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pandoc filter using panflute, for fenced code blocks 3 | """ 4 | 5 | import panflute as pf 6 | 7 | 8 | def prepare(doc): 9 | pass 10 | 11 | 12 | def fenced_action(options, data, element, doc): 13 | if doc.format == 'latex': 14 | pass 15 | # return None -> element unchanged 16 | # return [] -> delete element 17 | 18 | 19 | def finalize(doc): 20 | pass 21 | 22 | 23 | def main(doc=None): 24 | return pf.run_filter(pf.yaml_filter, prepare=prepare, finalize=finalize, 25 | tag='sometag', function=fenced_action, doc=doc) 26 | # Alternatively: 27 | # tags = {'sometag': fenced_action, 'another_tag': another_action} 28 | # return pf.run_filter(... , tags=tags, doc=doc) 29 | 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /tests/sample_files/example2/example.md: -------------------------------------------------------------------------------- 1 | --- 2 | jel: [C01, C13, C23, C55, C81] 3 | abstract: | 4 | Lorem ipsum 5 | Ipsum lorem 6 | title: "CEO and Firm *Fixed* Effects in a Matched Panel" 7 | subtitle: Identification and Estimation 8 | #author: Sergio Correia 9 | date: March 2016 10 | institute: Duke University 11 | 12 | theme: metropolis 13 | fontsize: 11pt # 14pt 14 | foobar: true 15 | spam: false 16 | q1: 'asd' 17 | q2: "ASD" 18 | 19 | foo: 20 | sapo: b 21 | rana: d 22 | 23 | header-includes: 24 | - \metroset{progressbar=frametitle} 25 | - \usefonttheme[onlymath]{serif} 26 | # - \setsansfont[BoldFont={Fira Sans SemiBold}]{Fira Sans Book} # Bolder 27 | 28 | build: cls & pandoc slides.md --to=beamer --latex-engine=xelatex --template=templates\default.beamer --output=..\out\slides.pdf && SumatraPDF ..\out\slides.pdf 29 | --- 30 | 31 | asd -------------------------------------------------------------------------------- /examples/panflute/metavars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Pandoc filter to allow interpolation of metadata fields 5 | into a document. %{fields} will be replaced by the field's 6 | value. 7 | """ 8 | 9 | from panflute import toJSONFilter, Span, Str, MetaInlines 10 | import re 11 | 12 | pattern = re.compile('%\{(.*)\}$') 13 | 14 | 15 | def metavars(elem, doc): 16 | if type(elem) == Str: 17 | m = pattern.match(elem.text) 18 | if m: 19 | field = m.group(1) 20 | result = doc.get_metadata(field, None) 21 | 22 | if type(result) == MetaInlines: 23 | return Span(*result.content, classes=['interpolated'], 24 | attributes={'field': field}) 25 | elif isinstance(result, str): 26 | return Str(result) 27 | 28 | if __name__ == "__main__": 29 | toJSONFilter(metavars) 30 | -------------------------------------------------------------------------------- /tests/test_equality.py: -------------------------------------------------------------------------------- 1 | import panflute as pf 2 | 3 | text = '''--- 4 | title: my title 5 | author: Bob 6 | --- 7 | 8 | # SomeHeader 9 | 10 | Some text 11 | ''' 12 | 13 | def test_equality(): 14 | doc1 = pf.convert_text(text, standalone=True) 15 | doc2 = pf.convert_text(text, standalone=True) 16 | doc3 = pf.convert_text(text, standalone=True) 17 | 18 | assert doc1 == doc2 19 | assert doc1 == doc2 == doc3 20 | 21 | doc2.content[0].content[0].text = 'Changed' 22 | assert doc1 != doc2 23 | assert doc2 != doc3 24 | assert doc1 == doc3 25 | 26 | doc3.metadata['author'] = pf.MetaInlines(pf.Str('John')) 27 | assert doc1 != doc3 28 | doc3.metadata['author'] = pf.MetaInlines(pf.Str('Bob')) 29 | assert doc1 == doc3 30 | 31 | assert doc1.content != doc2.content 32 | assert doc1.content == doc3.content 33 | 34 | 35 | if __name__ == "__main__": 36 | test_equality() 37 | -------------------------------------------------------------------------------- /examples/input/tikz-sample.md: -------------------------------------------------------------------------------- 1 | Use this 2 | 3 | 4 | ```latex 5 | \begin{tikzpicture} 6 | 7 | \def \n {5} 8 | \def \radius {3cm} 9 | \def \margin {8} % margin in angles, depends on the radius 10 | 11 | \foreach \s in {1,...,\n} 12 | { 13 | \node[draw, circle] at ({360/\n * (\s - 1)}:\radius) {$\s$}; 14 | \draw[->, >=latex] ({360/\n * (\s - 1)+\margin}:\radius) 15 | arc ({360/\n * (\s - 1)+\margin}:{360/\n * (\s)-\margin}:\radius); 16 | } 17 | \end{tikzpicture} 18 | 19 | ``` 20 | 21 | to get 22 | 23 | \begin{tikzpicture} 24 | 25 | \def \n {5} 26 | \def \radius {3cm} 27 | \def \margin {8} % margin in angles, depends on the radius 28 | 29 | \foreach \s in {1,...,\n} 30 | { 31 | \node[draw, circle] at ({360/\n * (\s - 1)}:\radius) {$\s$}; 32 | \draw[->, >=latex] ({360/\n * (\s - 1)+\margin}:\radius) 33 | arc ({360/\n * (\s - 1)+\margin}:{360/\n * (\s)-\margin}:\radius); 34 | } 35 | \end{tikzpicture} 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/pandocfilters/comments.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from pandocfilters import toJSONFilter 3 | import re 4 | 5 | """ 6 | Pandoc filter that causes everything between 7 | '' and '' 8 | to be ignored. The comment lines must appear on 9 | lines by themselves, with blank lines surrounding 10 | them. 11 | """ 12 | 13 | incomment = False 14 | 15 | 16 | def comment(k, v, fmt, meta): 17 | global incomment 18 | if k == 'RawBlock': 19 | fmt, s = v 20 | if fmt == "html": 21 | if re.search("", s): 22 | incomment = True 23 | return [] 24 | elif re.search("", s): 25 | incomment = False 26 | return [] 27 | if incomment: 28 | return [] # suppress anything in a comment 29 | 30 | if __name__ == "__main__": 31 | toJSONFilter(comment) 32 | -------------------------------------------------------------------------------- /docs/source/_static/wiki.py: -------------------------------------------------------------------------------- 1 | """ 2 | Panflute filter that embeds wikipedia text 3 | 4 | Replaces markdown such as [Stack Overflow](wiki://) with the resulting text. 5 | """ 6 | 7 | import requests 8 | import panflute as pf 9 | 10 | 11 | def action(elem, doc): 12 | if isinstance(elem, pf.Link) and elem.url.startswith('wiki://'): 13 | title = pf.stringify(elem).strip() 14 | baseurl = 'https://en.wikipedia.org/w/api.php' 15 | query = {'format': 'json', 'action': 'query', 'prop': 'extracts', 16 | 'explaintext': '', 'titles': title} 17 | r = requests.get(baseurl, params=query) 18 | data = r.json() 19 | extract = list(data['query']['pages'].values())[0]['extract'] 20 | extract = extract.split('.', maxsplit=1)[0] 21 | return pf.RawInline(extract) 22 | 23 | 24 | def main(doc=None): 25 | return pf.run_filter(action, doc=doc) 26 | 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | =================== 3 | 4 | To install panflute from PyPI, open the command line and type:: 5 | 6 | pip install panflute 7 | 8 | - Works with Python 3.6+, and PyPy 9 | - On Windows, you might need to open the command line (``cmd``) as administrator (`ctrl+shift+enter`). 10 | 11 | To install the latest Github version of panflute, type:: 12 | 13 | pip install git+https://github.com/sergiocorreia/panflute.git 14 | 15 | 16 | Dev Install 17 | *************** 18 | 19 | After cloning the Github repo into your computer, you can install the package locally:: 20 | 21 | python setup.py install 22 | 23 | Alternatively, you can install it through a symlink, so changes are automatically updated:: 24 | 25 | python setup.py develop 26 | 27 | Source Code 28 | *************** 29 | 30 | To browse the source code, report issues or contribute, check the `github repository `_. 31 | -------------------------------------------------------------------------------- /examples/pandocfilters/metavars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Pandoc filter to allow interpolation of metadata fields 5 | into a document. %{fields} will be replaced by the field's 6 | value, assuming it is of the type MetaInlines or MetaString. 7 | """ 8 | 9 | from pandocfilters import toJSONFilter, attributes, Span, Str 10 | import re 11 | 12 | pattern = re.compile('%\{(.*)\}$') 13 | 14 | 15 | def metavars(key, value, format, meta): 16 | if key == 'Str': 17 | m = pattern.match(value) 18 | if m: 19 | field = m.group(1) 20 | result = meta.get(field, {}) 21 | if 'MetaInlines' in result['t']: 22 | return Span(attributes({'class': 'interpolated', 23 | 'field': field}), 24 | result['c']) 25 | elif 'MetaString' in result[t]: 26 | return Str(result['c']) 27 | 28 | if __name__ == "__main__": 29 | toJSONFilter(metavars) 30 | -------------------------------------------------------------------------------- /extra/panflute-fenced.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | element unchanged 18 | # return [] -> delete element 19 | 20 | 21 | def finalize(doc): 22 | ${6:pass} 23 | 24 | 25 | if __name__ == '__main__': 26 | pf.toJSONFilter(pf.yaml_filter, prepare=prepare, finalize=finalize, tag='${2:sometag}', function=fenced_action) 27 | # tags = {'sometag': fenced_action, 'another_tag': another_action} 28 | # pf.toJSONFilter(pf.yaml_filter, prepare=prepare, finalize=finalize, tags=tags) 29 | 30 | ]]> 31 | panflute-fenced 32 | source.python 33 | Fenced Code panflute filter 34 | -------------------------------------------------------------------------------- /panflute/borrar.txt: -------------------------------------------------------------------------------- 1 | Pandoc passes additional data to Lua filters by setting global variables. 2 | 3 | 4 | --> add them to .doc ??? 5 | 6 | 7 | add a from_json 8 | 9 | 10 | 11 | BUG?? 12 | def attach(element, parent, location): 13 | if not isinstance(element, (int, str, bool)): 14 | element.parent = parent 15 | element.location = location 16 | else: 17 | print(element, 'has no parent') 18 | return element 19 | PRINT ??? AND HAS NO PARENT? WHY NOT ERROR 20 | 21 | 22 | 23 | 24 | 25 | 26 | class ForceSlots(type): 27 | @classmethod 28 | def __prepare__(metaclass, name, bases, **kwds): 29 | return {'__slots__': ()} 30 | 31 | 32 | 33 | 34 | remove 35 | api_version stuff 36 | 37 | 38 | 39 | 40 | 41 | 42 | This is redundant b/c should already be the default in Element!! 43 | def to_json(self): 44 | return {'t': 'Space'} 45 | 46 | remove everything with word legacy 47 | 48 | 49 | 50 | Add a len() method?? 51 | 52 | 53 | https://pandoc.org/filters.html -------------------------------------------------------------------------------- /docs/source/_static/csv-tables.py: -------------------------------------------------------------------------------- 1 | """ 2 | Panflute filter to parse CSV in fenced YAML code blocks 3 | """ 4 | 5 | import io 6 | import csv 7 | import panflute as pf 8 | 9 | 10 | def fenced_action(options, data, element, doc): 11 | # We'll only run this for CodeBlock elements of class 'csv' 12 | title = options.get('title', 'Untitled Table') 13 | title = [pf.Str(title)] 14 | has_header = options.get('has-header', False) 15 | 16 | with io.StringIO(data) as f: 17 | reader = csv.reader(f) 18 | body = [] 19 | for row in reader: 20 | cells = [pf.TableCell(pf.Plain(pf.Str(x))) for x in row] 21 | body.append(pf.TableRow(*cells)) 22 | 23 | header = body.pop(0) if has_header else None 24 | table = pf.Table(*body, header=header, caption=title) 25 | return table 26 | 27 | 28 | def main(doc=None): 29 | return pf.run_filter(pf.yaml_filter, tag='csv', function=fenced_action, 30 | doc=doc) 31 | 32 | 33 | if __name__ == '__main__': 34 | main() 35 | -------------------------------------------------------------------------------- /examples/input/plantuml-sample.md: -------------------------------------------------------------------------------- 1 | Use this 2 | 3 | 4 | ``` 5 | Alice -> Bob: Authentication Request 6 | Bob --> Alice: Authentication Response 7 | 8 | Alice -> Bob: Another authentication Request 9 | Alice <-- Bob: another authentication Response 10 | ``` 11 | 12 | to get 13 | 14 | ```plantuml 15 | Alice -> Bob: Authentication Request 16 | Bob --> Alice: Authentication Response 17 | 18 | Alice -> Bob: Another authentication Request 19 | Alice <-- Bob: another authentication Response 20 | ``` 21 | 22 | with UNICODE REMOVED 23 | 24 | ```plantuml 25 | Älöc -> Bob: Authentication Request 26 | Bob --> Älöc: Authentication Response 27 | 28 | Älöc -> Bob: Another authentication Request 29 | Älöc <-- Bob: another authentication Response 30 | ``` 31 | 32 | See [whatever](#whatever) for an example with options `{.plantuml #whatever caption="this is the caption" width=65%}` 33 | 34 | ```{.plantuml #whatever caption="this is the caption" width=65%} 35 | Alice -> Bob: Authentication Request 36 | Bob --> Alice: Authentication Response 37 | ``` 38 | -------------------------------------------------------------------------------- /tests/filters/assert_env.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pandoc filter using panflute 3 | """ 4 | 5 | import panflute as pf 6 | 7 | 8 | def prepare(doc): 9 | pf.debug(f' Pandoc version: {doc.pandoc_version}') 10 | pf.debug(' Pandoc reader options:') 11 | for k, v in doc.pandoc_reader_options.items(): 12 | pf.debug(f' {k}={v}') 13 | 14 | assert doc.pandoc_version >= (2, 11, 0) 15 | standalone_key = 'readerStandalone' if doc.pandoc_version < (2, 16, 0) else "standalone" 16 | assert doc.pandoc_reader_options[standalone_key] is False 17 | 18 | 19 | def action(elem, doc): 20 | if isinstance(elem, pf.Element) and doc.format == 'latex': 21 | pass 22 | # return None -> element unchanged 23 | # return [] -> delete element 24 | 25 | 26 | def finalize(doc): 27 | pass 28 | 29 | 30 | def main(doc=None): 31 | return pf.run_filters([action], 32 | prepare=prepare, 33 | finalize=finalize, 34 | doc=doc) 35 | 36 | 37 | if __name__ == '__main__': 38 | main() 39 | -------------------------------------------------------------------------------- /tests/test_regressions.py: -------------------------------------------------------------------------------- 1 | import io 2 | import panflute as pf 3 | 4 | 5 | def test_quotes_129(): 6 | #pf https://github.com/sergiocorreia/panflute/issues/129 7 | text = [pf.Str("Some"), pf.Space, pf.Str("quoted text")] 8 | quoted_text = pf.Quoted(*text) 9 | para = pf.Para(quoted_text) 10 | output = pf.stringify(para, False) 11 | assert output == '"Some quoted text"' 12 | 13 | 14 | def test_index_223(): 15 | """Index values on duplicated elements are determined using list.index() 16 | but this returns the index first found element. 17 | This test checks whether the index on the element corresponds with the 18 | actual index in the parent collection. 19 | """ 20 | # pf https://github.com/sergiocorreia/panflute/issues/223 21 | doc = pf.Doc(pf.Para(pf.Str("a")), pf.Para(pf.Str("b")), 22 | pf.Para(pf.Str("a")), pf.Para(pf.Str("c"))) 23 | 24 | for (index, element) in enumerate(doc.content): 25 | assert element.index == index 26 | 27 | 28 | if __name__ == "__main__": 29 | test_quotes_129() 30 | test_index_223() 31 | -------------------------------------------------------------------------------- /docs/source/_templates/sidebar.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |

8 | 10 |

11 | 12 |

Panflute is a Python package to easily write Pandoc filters.

13 |

It is pythonic and comes with batteries included.

14 | 15 |

Stay Informed

16 | 17 |

Report issues or send suggestions.

18 | 19 |

21 | 22 |

More projects by Sergio Correia here.

23 | 24 | -------------------------------------------------------------------------------- /examples/input/abc-sample.md: -------------------------------------------------------------------------------- 1 | 2 | Use this 3 | 4 | 5 | ``` 6 | X:7 7 | T:Qui Tolis (Trio) 8 | C:André Raison 9 | M:3/4 10 | L:1/4 11 | Q:1/4=92 12 | %%staves {(Pos1 Pos2) Trompette} 13 | K:F 14 | % 15 | V:Pos1 16 | %%MIDI program 78 17 | "Positif"x3 |x3 |c'>ba|Pga/g/f|:g2a |ba2 |g2c- |c2P=B |c>de |fga | 18 | V:Pos2 19 | %%MIDI program 78 20 | Mf>ed|cd/c/B|PA2d |ef/e/d |:e2f |ef2 |c>BA |GA/G/F |E>FG |ABc- | 21 | V:Trompette 22 | %%MIDI program 56 23 | "Trompette"z3|z3 |z3 |z3 |:Mc>BA|PGA/G/F|PE>EF|PEF/E/D|C>CPB,|A,G,F,-| 24 | ``` 25 | 26 | to get 27 | 28 | ```abc 29 | X:7 30 | T:Qui Tolis (Trio) 31 | C:André Raison 32 | M:3/4 33 | L:1/4 34 | Q:1/4=92 35 | %%staves {(Pos1 Pos2) Trompette} 36 | K:F 37 | % 38 | V:Pos1 39 | %%MIDI program 78 40 | "Positif"x3 |x3 |c'>ba|Pga/g/f|:g2a |ba2 |g2c- |c2P=B |c>de |fga | 41 | V:Pos2 42 | %%MIDI program 78 43 | Mf>ed|cd/c/B|PA2d |ef/e/d |:e2f |ef2 |c>BA |GA/G/F |E>FG |ABc- | 44 | V:Trompette 45 | %%MIDI program 56 46 | "Trompette"z3|z3 |z3 |z3 |:Mc>BA|PGA/G/F|PE>EF|PEF/E/D|C>CPB,|A,G,F,-| 47 | ``` 48 | 49 | -------------------------------------------------------------------------------- /tests/test_elements.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from panflute.elements import builtin2meta, MetaList, MetaMap, MetaString 4 | 5 | 6 | class My_List(list): 7 | """Subclass for testing https://github.com/sergiocorreia/panflute/issues/166""" 8 | pass 9 | 10 | 11 | class My_Dict(dict): 12 | """Subclass for testing https://github.com/sergiocorreia/panflute/issues/166""" 13 | pass 14 | 15 | 16 | class Not_Builtin: 17 | pass 18 | 19 | 20 | @pytest.mark.parametrize( 21 | "value,expect_type", 22 | [ 23 | ([], MetaList), 24 | ({}, MetaMap), 25 | (My_List(), MetaList), 26 | (My_Dict(), MetaMap), 27 | (1, MetaString), 28 | ('a', MetaString), 29 | ([1], MetaList), 30 | ({'a': My_List()}, MetaMap), 31 | (Not_Builtin(), Not_Builtin) 32 | ] 33 | ) 34 | def test_builtin2meta(value, expect_type): 35 | """ 36 | test output types of builtin2meta. 37 | Comparison of output value would be preferable, 38 | but does not work since __eq__ methods are not defined for MetaValue classes. 39 | """ 40 | assert type(builtin2meta(value)) == expect_type 41 | -------------------------------------------------------------------------------- /tests/input/awesome-c/profiler.py: -------------------------------------------------------------------------------- 1 | import cProfile 2 | import timeit 3 | import panflute as pf 4 | 5 | 6 | def empty_test(element, doc): 7 | return 8 | 9 | def test_filter(element, doc): 10 | if type(element)==pf.Header: 11 | return [] 12 | if type(element)==pf.Str: 13 | element.text = element.text + '!!' 14 | return element 15 | 16 | 17 | def run(): 18 | print('\nLoading JSON...') 19 | input_fn = 'benchmark.json' 20 | output_fn = 'panflute.json' 21 | 22 | with open(input_fn, encoding='utf-8') as f: 23 | doc = pf.load(f) 24 | 25 | print('\nApplying trivial filter...') 26 | doc = doc.walk(action=empty_test, doc=doc) 27 | 28 | print('Dumping JSON...') 29 | with open(output_fn, mode='w', encoding='utf-8') as f: 30 | pf.dump(doc, f) 31 | f.write('\n') 32 | 33 | print(' - Done!') 34 | 35 | 36 | if __name__ == "__main__": 37 | #cProfile.run('run()') 38 | t = timeit.repeat('run()', setup="from __main__ import run", 39 | number=1, repeat=3) 40 | 41 | print('Times:') 42 | print(t) 43 | print('minimum:') 44 | print(min(t)) 45 | -------------------------------------------------------------------------------- /.github/workflows/force-publish.yml: -------------------------------------------------------------------------------- 1 | name: Manually publish on PyPI (experimental) 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish: 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: '3.10' 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install ".[pypi]" 24 | 25 | - name: Build source distribution 26 | run: | 27 | python3 setup.py sdist bdist_wheel 28 | twine check dist/* 29 | 30 | - name: Publish distribution to Test PyPI 31 | uses: pypa/gh-action-pypi-publish@master 32 | with: 33 | password: ${{ secrets.test_pypi_password }} 34 | repository_url: https://test.pypi.org/legacy/ 35 | continue-on-error: true 36 | 37 | - name: Publish distribution to PyPI 38 | uses: pypa/gh-action-pypi-publish@master 39 | with: 40 | password: ${{ secrets.pypi_password }} 41 | -------------------------------------------------------------------------------- /examples/panflute/theorem.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Pandoc filter to convert divs with class="theorem" to LaTeX 5 | theorem environments in LaTeX output, and to numbered theorems 6 | in HTML output. 7 | """ 8 | 9 | from panflute import Div, RawBlock, toJSONFilter 10 | 11 | def prepare(doc): 12 | doc.theoremcount = 0 13 | 14 | def theorems(e, doc): 15 | if type(e) == Div and 'theorem' in e.classes: 16 | doc.theoremcount += 1 17 | if doc.format == 'latex': 18 | label = '\\label{' + e.identifier + '}' if e.identifier else '' 19 | left = RawBlock('\\begin{theorem}' + label, format='latex') 20 | right = RawBlock('\\end{theorem}', format='latex') 21 | elif doc.format in ('html', 'html5'): 22 | label = '
Theorem {}
\n
'.format(doc.theoremcount) 23 | left = RawBlock(label, format='html') 24 | right = RawBlock('
\n', format='html') 25 | else: 26 | return 27 | 28 | e.content = [left] + list(e.content) + [right] 29 | return e 30 | 31 | 32 | if __name__ == "__main__": 33 | toJSONFilter(theorems, prepare=prepare) 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # panflute-specific 2 | README.rst 3 | # note: generated when pushing to PyPI 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # Code style reports 11 | pycodestyle-report.txt 12 | pylint-report.txt 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Sphinx build 18 | /docs/build/ 19 | 20 | # Distribution / packaging 21 | .Python 22 | env/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | tests/1/*.txt 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | #Ipython Notebook 72 | .ipynb_checkpoints 73 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish on PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | # published, unpublished, created, edited, deleted, or prereleased 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.10' 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install ".[pypi]" 27 | 28 | - name: Build source distribution 29 | run: | 30 | python3 setup.py sdist bdist_wheel 31 | twine check dist/* 32 | 33 | - name: Publish distribution to Test PyPI 34 | uses: pypa/gh-action-pypi-publish@master 35 | with: 36 | password: ${{ secrets.test_pypi_password }} 37 | repository_url: https://test.pypi.org/legacy/ 38 | continue-on-error: true 39 | 40 | - name: Publish distribution to PyPI 41 | if: github.event.action == 'published' && !github.event.release.prerelease 42 | uses: pypa/gh-action-pypi-publish@master 43 | with: 44 | password: ${{ secrets.pypi_password }} 45 | -------------------------------------------------------------------------------- /examples/panflute/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 panflute import toJSONFilter, Str, Para, Image, CodeBlock 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(elem, doc): 22 | if type(elem) == CodeBlock and 'graphviz' in elem.classes: 23 | code = elem.text 24 | caption = "caption" 25 | G = pygraphviz.AGraph(string=code) 26 | G.layout() 27 | filename = sha1(code) 28 | filetype = {'html': 'png', 'latex': 'pdf'}.get(doc.format, 'png') 29 | alt = Str(caption) 30 | src = imagedir + '/' + filename + '.' + filetype 31 | if not os.path.isfile(src): 32 | try: 33 | os.mkdir(imagedir) 34 | sys.stderr.write('Created directory ' + imagedir + '\n') 35 | except OSError: 36 | pass 37 | G.draw(src) 38 | sys.stderr.write('Created image ' + src + '\n') 39 | return Para(Image(alt, url=source, title='')) 40 | 41 | 42 | if __name__ == "__main__": 43 | toJSONFilter(graphviz) 44 | -------------------------------------------------------------------------------- /examples/pandocfilters/theorem.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Pandoc filter to convert divs with class="theorem" to LaTeX 5 | theorem environments in LaTeX output, and to numbered theorems 6 | in HTML output. 7 | """ 8 | 9 | from pandocfilters import toJSONFilter, RawBlock, Div 10 | 11 | theoremcount = 0 12 | 13 | 14 | def latex(x): 15 | return RawBlock('latex', x) 16 | 17 | 18 | def html(x): 19 | return RawBlock('html', x) 20 | 21 | 22 | def theorems(key, value, format, meta): 23 | if key == 'Div': 24 | [[ident, classes, kvs], contents] = value 25 | if "theorem" in classes: 26 | if format == "latex": 27 | if ident == "": 28 | label = "" 29 | else: 30 | label = '\\label{' + ident + '}' 31 | return([latex('\\begin{theorem}' + label)] + contents + 32 | [latex('\\end{theorem}')]) 33 | elif format == "html" or format == "html5": 34 | global theoremcount 35 | theoremcount = theoremcount + 1 36 | newcontents = [html('
Theorem ' + str(theoremcount) + '
'), 37 | html('
')] + contents + [html('
\n')] 38 | return Div([ident, classes, kvs], newcontents) 39 | 40 | if __name__ == "__main__": 41 | toJSONFilter(theorems) 42 | -------------------------------------------------------------------------------- /tests/test_env.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Test that we are able to access environment vars set by Pandoc 3 | 4 | See: https://pandoc.org/filters.html#environment-variables 5 | ''' 6 | 7 | import panflute as pf 8 | from pathlib import Path 9 | 10 | 11 | def test_env(): 12 | # A Doc() created by panflute has no environment vars 13 | print(f'\n - Testing Doc() created by panflute:') 14 | doc = pf.Doc() 15 | assert doc.pandoc_version is None 16 | assert isinstance(doc.pandoc_reader_options, dict) and not doc.pandoc_reader_options 17 | print(f' - No environment vars; as expected') 18 | 19 | # A Doc() created by running convert_text also doesn't 20 | print(f'\n - Testing Doc() created by panflute.convert_text():') 21 | fn = Path("./tests/sample_files/fenced/example.md") 22 | with fn.open(encoding='utf-8') as f: 23 | markdown_text = f.read() 24 | json_pandoc = pf.convert_text(markdown_text, input_format='markdown', output_format='json', standalone=True) 25 | doc = pf.convert_text(json_pandoc, input_format='json', output_format='panflute', standalone=True) 26 | assert doc.pandoc_version is None 27 | assert isinstance(doc.pandoc_reader_options, dict) and not doc.pandoc_reader_options 28 | print(f' - No environment vars; as expected') 29 | 30 | print(f'\n - Testing Doc() as created by a filter:') 31 | pf.run_pandoc(text='Hello!', args=['--filter=./tests/filters/assert_env.py']) 32 | print(f' - Found environment vars; as expected') 33 | 34 | 35 | 36 | 37 | if __name__ == "__main__": 38 | test_env() 39 | -------------------------------------------------------------------------------- /panflute/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Panflute: pandoc filters made simple 3 | ==================================== 4 | 5 | Panflute is a Python package that makes `Pandoc `_ 6 | filters fun to write. (`Installation `_) 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 |

Header 1

2 |

Some emphasized, bold and striken out text.

3 |

Header 2

4 |

$include(../ch2/invalid)

5 |

$include this $include is invalid

6 |

Header lvl 3

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 |

Header 3

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
VariableMean
Price10
Weight12
31 |

lorem lorem..

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
VariableMean
Price10
Price10
Price10
Weight12
58 |

Another header 1

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 | [![Development Status](https://img.shields.io/pypi/status/panflute.svg)](https://pypi.python.org/pypi/panflute/) 4 | [![Build Status](https://github.com/sergiocorreia/panflute/workflows/CI%20Tests/badge.svg)](https://github.com/sergiocorreia/panflute/actions?query=workflow%3A%22CI+Tests%22) 5 | ![License](https://img.shields.io/pypi/l/panflute.svg) 6 | [![DOI](https://zenodo.org/badge/55024750.svg)](https://zenodo.org/badge/latestdoi/55024750) 7 | 8 | [![GitHub Releases](https://img.shields.io/github/tag/sergiocorreia/panflute.svg?label=github+release)](https://github.com/sergiocorreia/panflute/releases) 9 | [![PyPI version](https://img.shields.io/pypi/v/panflute.svg)](https://pypi.python.org/pypi/panflute/) 10 | [![Conda Version](https://img.shields.io/conda/vn/conda-forge/panflute.svg)](https://anaconda.org/conda-forge/panflute) 11 | [![Python version](https://img.shields.io/pypi/pyversions/panflute.svg)](https://pypi.python.org/pypi/panflute/) 12 | [![Supported implementations](https://img.shields.io/pypi/implementation/panflute.svg)](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: [![Python version](https://img.shields.io/pypi/pyversions/panflute.svg)](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 | --------------------------------------------------------------------------------