diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..25176cb0 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,10 @@ +[report] + +exclude_lines = + if __name__ == .__main__.: + def main + except ZeroDivisionError + return \[\] + return None + __version__ + py2 diff --git a/.gitignore b/.gitignore index 63f581b9..5ba5101e 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,25 @@ # macOS files .DS_Store -test/*.pdf \ No newline at end of file +tests/reference_idempotent.native +tests/*.native +tests/*.pdf + +# python auxiliary +__pycache__/ +.cache +*.pyc + +# pypi +dist/ +pantable.egg-info/ + +# coverage +.coverage +htmlcov/ + +# pycharm +.idea/ + +# rst2html +README.html diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..2a9b9b5d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,109 @@ +# os: linux and sudo: false is assumed, which means it is using container-based Ubuntu 12.04 +language: python +cache: pip +# build matrix: different python and pandoc versions +python: + - "2.7" + - "3.3" + - "3.4" + - "3.5" + - "3.5-dev" # 3.5 development branch + - "3.6-dev" # 3.6 development branch + - "nightly" # currently points to 3.7-dev + # pypy (version info from [Changelogs — PyPy documentation](http://doc.pypy.org/en/latest/index-of-whatsnew.html)) + - "pypy" # PyPy2.7 5.3.1 (CPython 2.7 compatible) + - "pypy3" # PyPy3 2.4.0 (CPython 3.2 compatible) +env: + # - pandocVersion=1.13.2 + # - pandocVersion=1.14 + # - pandocVersion=1.14.0.1 + # - pandocVersion=1.14.1 + # - pandocVersion=1.15 + # - pandocVersion=1.15.0.5 + # - pandocVersion=1.15.0.6 + # - pandocVersion=1.15.1 + # - pandocVersion=1.15.2 + # - pandocVersion=1.16 + # - pandocVersion=1.16.0.1 + # - pandocVersion=1.16.0.2 + # - pandocVersion=1.17 + # - pandocVersion=1.17.0.1 + # - pandocVersion=1.17.0.2 + # - pandocVersion=1.17.1 + - pandocVersion=1.17.2 + # - pandocVersion=1.18 + - pandocVersion=latest +matrix: + allow_failures: + - python: "3.5-dev" + - python: "3.6-dev" + - python: "nightly" + - python: "pypy3" + # - env: pandocVersion=1.13.2 + # - env: pandocVersion=1.14 + # - env: pandocVersion=1.14.0.1 + # - env: pandocVersion=1.14.1 + # - env: pandocVersion=1.15 + # - env: pandocVersion=1.15.0.5 + # - env: pandocVersion=1.15.0.6 + # - env: pandocVersion=1.15.1 + # - env: pandocVersion=1.15.2 + fast_finish: true +# download pandoc +before_install: + - | + if [[ $pandocVersion == "latest" ]]; then + url="https://github.com/jgm/pandoc/releases/latest" + else + url="https://github.com/jgm/pandoc/releases/tag/$pandocVersion" + fi + path=$(curl -L $url | grep -o '/jgm/pandoc/releases/download/.*-amd64\.deb') + downloadUrl="https://github.com$path" + file=${path##*/} +# install dependencies +install: + # pandoc + - wget $downloadUrl && + sudo dpkg -i $file + # latest pip dropped support for py3.2, which is the version of python in pypy3 + - if [[ "$TRAVIS_PYTHON_VERSION" != "pypy3" ]]; then pip install -U pip; fi + - pip install -e .[test] +before_script: + # pasteurize for py2 only, except setup.py & pantable/version.py + - | + if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]] || [[ "$TRAVIS_PYTHON_VERSION" == "pypy" ]]; then + mv setup.py setup.py.temp + mv pantable/version.py pantable/version.py.temp + pasteurize -wnj 4 . + mv setup.py.temp setup.py + mv pantable/version.py.temp pantable/version.py + fi +# command to run tests +script: + - make -j4 test +after_success: + - if [[ "$pandocVersion" == "latest" ]] && [[ "$TRAVIS_PYTHON_VERSION" == "3.5" ]]; then coveralls; else printf "skip coveralls"; fi +before_deploy: + # build unpasteurized python3 wheel + - python3 setup.py bdist_wheel + # pasteurize except setup.py & pantable/version.py + - | + mv setup.py setup.py.temp + mv pantable/version.py pantable/version.py.temp + pasteurize -wnj 4 . + mv setup.py.temp setup.py + mv pantable/version.py.temp pantable/version.py + # prepare wheel for py2 + - pip2 install wheel + - python2 setup.py bdist_wheel +deploy: + provider: pypi + user: ickc + password: $pypi_password + # do not add bdist_wheel here, since it is done above + distributions: "sdist" + skip_cleanup: true + on: + tags: true + python: "3.5" + condition: $pandocVersion = latest diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100755 index 00000000..9561fb10 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.rst diff --git a/README.md b/README.md new file mode 100644 index 00000000..57f0a29e --- /dev/null +++ b/README.md @@ -0,0 +1,293 @@ + + +- [`pantable`](#pantable) + - [Example](#example) + - [Install and Use](#install-and-use) + - [Syntax](#syntax) +- [`pantable2csv`](#pantable2csv) +- [Related Filters](#related-filters) + +[![Build Status](https://travis-ci.org/ickc/pantable.svg?branch=master)](https://travis-ci.org/ickc/pantable) [![GitHub Releases](https://img.shields.io/github/tag/ickc/pantable.svg?label=github+release)](https://github.com/ickc/pantable/releases) [![PyPI version](https://img.shields.io/pypi/v/pantable.svg)](https://pypi.python.org/pypi/pantable/) [![Development Status](https://img.shields.io/pypi/status/pantable.svg)](https://pypi.python.org/pypi/pantable/) [![Python version](https://img.shields.io/pypi/pyversions/pantable.svg)](https://pypi.python.org/pypi/pantable/) ![License](https://img.shields.io/pypi/l/pantable.svg) [![Coveralls](https://img.shields.io/coveralls/ickc/pantable.svg)](https://coveralls.io/github/ickc/pantable) + +The pantable package comes with 2 pandoc filters, `pantable.py` and `pantable2csv.py`. `pantable` is the main filter, introducing a syntax to include CSV table in markdown source. `pantable2csv` complements `pantable`, is the inverse of `pantable`, which convert native pandoc tables into the CSV table format defined by `pantable`. + +Some example uses are: + +1. You already have tables in CSV format. + +2. You feel that directly editing markdown table is troublesome. You want a spreadsheet interface to edit, but want to convert it to native pandoc table for higher readability. And this process might go back and forth. + +3. You want lower-level control on the table and column widths. + +4. You want to use all table features supported by the pandoc’s internal AST table format, which is not possible in markdown for pandoc <= 1.18.[1] + +[1] In pandoc 1.19, grid-tables is improved to support all features available to the AST too. + +`pantable` +========== + +This allows CSV tables, optionally containing markdown syntax (disabled by default), to be put in markdown as a fenced code blocks. + +Example +------- + +Also see the README in [GitHub Pages](https://ickc.github.io/pantable/). There’s a [LaTeX output](https://ickc.github.io/pantable/README.pdf) too. + + ```table + --- + caption: '*Awesome* **Markdown** Table' + alignment: RC + table-width: 2/3 + markdown: True + --- + First row,defaulted to be header row,can be disabled + 1,cell can contain **markdown**,"It can be aribrary block element: + + - following standard markdown syntax + - like this" + 2,"Any markdown syntax, e.g.",$$E = mc^2$$ + ``` + +becomes + + + +++++ + + + + + + + + + + + + + + + + + + + +
Awesome Markdown Table

First row

defaulted to be header row

can be disabled

1

cell can contain markdown

It can be aribrary block element:

+
    +
  • following standard markdown syntax
  • +
  • like this
  • +

2

Any markdown syntax, e.g.


E = mc2

+ +(The equation might not work if you view this on PyPI.) + +Install and Use +--------------- + +Install: + +``` bash +pip install -U pantable +``` + +Use: + +``` bash +pandoc -F pantable -o README.html README.md +``` + +Syntax +------ + +Fenced code blocks is used, with a class `table`. See [Example](#example). + +Optionally, YAML metadata block can be used within the fenced code block, following standard pandoc YAML metadata block syntax. 7 metadata keys are recognized: + +- `caption`: the caption of the table. If omitted, no caption will be inserted. Default: disabled. + +- `alignment`: a string of characters among `L,R,C,D`, case-insensitive, corresponds to Left-aligned, Right-aligned, Center-aligned, Default-aligned respectively. e.g. `LCRD` for a table with 4 columns. Default: `DDD...` + +- `width`: a list of relative width corresponding to the width of each columns. e.g. + + ``` yaml + - width + - 0.1 + - 0.2 + - 0.3 + - 0.4 + ``` + + Default: auto calculated from the length of each line in table cells. + +- `table-width`: the relative width of the table (e.g. relative to `\linewidth`). default: 1.0 + +- `header`: If it has a header row or not. True/False/yes/NO are accepted, case-insensitive. default: True + +- `markdown`: If CSV table cell contains markdown syntax or not. Same as above. Default: False + +- `include`: the path to an CSV file, can be relative/absolute. If non-empty, override the CSV in the CodeBlock. default: None + +When the metadata keys is invalid, the default will be used instead. Note that width and table-width accept fractions as well. + +`pantable2csv` +============== + +This one is the inverse of `pantable`, a panflute filter to convert any native pandoc tables into the CSV table format used by pantable. + +Effectively, `pantable` forms a “CSV Reader”, and `pantable2csv` forms a “CSV Writer”. It allows you to convert back and forth between these 2 formats. + +For example, in the markdown source: + + +--------+---------------------+--------------------------+ + | First | defaulted to be | can be disabled | + | row | header row | | + +========+=====================+==========================+ + | 1 | cell can contain | It can be aribrary block | + | | **markdown** | element: | + | | | | + | | | - following standard | + | | | markdown syntax | + | | | - like this | + +--------+---------------------+--------------------------+ + | 2 | Any markdown | $$E = mc^2$$ | + | | syntax, e.g. | | + +--------+---------------------+--------------------------+ + + : *Awesome* **Markdown** Table + +running `pandoc -F pantable2csv -o output.md input.md`, it becomes + + ``` {.table} + --- + alignment: DDD + caption: '*Awesome* **Markdown** Table' + header: true + markdown: true + table-width: 0.8055555555555556 + width: [0.125, 0.3055555555555556, 0.375] + --- + First row,defaulted to be header row,can be disabled + 1,cell can contain **markdown**,"It can be aribrary block element: + + - following standard markdown syntax + - like this + " + 2,"Any markdown syntax, e.g.",$$E = mc^2$$ + ``` + +Related Filters +=============== + +The followings are pandoc filters written in Haskell that provide similar functionality. This filter is born after testing with theirs. + +- [baig/pandoc-csv2table: A Pandoc filter that renders CSV as Pandoc Markdown Tables.](https://github.com/baig/pandoc-csv2table) +- [mb21/pandoc-placetable: Pandoc filter to include CSV data (from file or URL)](https://github.com/mb21/pandoc-placetable) +- [sergiocorreia/panflute/csv-tables.py](https://github.com/sergiocorreia/panflute/blob/1ddcaba019b26f41f8c4f6f66a8c6540a9c5f31a/docs/source/csv-tables.py) + + +++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
pandoc-csv2tablepandoc-placetablepanflute examplepantable
captioncaptioncaptiontitlecaption
alignsaligns = LRCDaligns = LRCDaligns = LRCD
widthwidths = "0.5 0.2 0.3"width: [0.5, 0.2, 0.3]
table-widthtable-width: 1.0
headerheader = yes | noheader = yes | nohas_header: True | Falseheader: True | False | yes | NO
markdowninlinemarkdownmarkdown: True | False | yes | NO
sourcesourcefilesourceinclude
otherstype = simple | multiline | grid | pipe
delimiter
quotechar
id (wrapped by div)
Noteswidth are auto-calculated when width is not specified
diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..d2b2639d --- /dev/null +++ b/README.rst @@ -0,0 +1,275 @@ +.. This README is auto-generated from `docs/README.md`. Do not edit this file directly. + +===================================================== +CSV Tables in Markdown — Pandoc Filter for CSV Tables +===================================================== + +:Date: December 4, 2016 + +.. role:: math(raw) + :format: html latex +.. + +.. contents:: + :depth: 3 +.. + +|Build Status| |GitHub Releases| |PyPI version| |Development Status| +|Python version| |License| |Coveralls| + +The pantable package comes with 2 pandoc filters, ``pantable.py`` and +``pantable2csv.py``. ``pantable`` is the main filter, introducing a +syntax to include CSV table in markdown source. ``pantable2csv`` +complements ``pantable``, is the inverse of ``pantable``, which convert +native pandoc tables into the CSV table format defined by ``pantable``. + +Some example uses are: + +#. You already have tables in CSV format. + +#. You feel that directly editing markdown table is troublesome. You + want a spreadsheet interface to edit, but want to convert it to + native pandoc table for higher readability. And this process might go + back and forth. + +#. You want lower-level control on the table and column widths. + +#. You want to use all table features supported by the pandoc’s internal + AST table format, which is not possible in markdown for pandoc <= + 1.18. [1]_ + +``pantable`` +============ + +This allows CSV tables, optionally containing markdown syntax (disabled +by default), to be put in markdown as a fenced code blocks. + +Example +------- + +Also see the README in `GitHub +Pages `__. There’s a `LaTeX +output `__ too. + +:: + + ```table + --- + caption: '*Awesome* **Markdown** Table' + alignment: RC + table-width: 2/3 + markdown: True + --- + First row,defaulted to be header row,can be disabled + 1,cell can contain **markdown**,"It can be aribrary block element: + + - following standard markdown syntax + - like this" + 2,"Any markdown syntax, e.g.",$$E = mc^2$$ + ``` + +becomes + ++--------+--------------------+------------------------+ +| First | defaulted to be | can be disabled | +| row | header row | | ++========+====================+========================+ +| 1 | cell can contain | It can be aribrary | +| | **markdown** | block element: | +| | | | +| | | - following standard | +| | | markdown syntax | +| | | - like this | ++--------+--------------------+------------------------+ +| 2 | Any markdown | .. math:: E = mc^2 | +| | syntax, e.g. | | ++--------+--------------------+------------------------+ + +Table: *Awesome* **Markdown** Table + +(The equation might not work if you view this on PyPI.) + +Install and Use +--------------- + +Install: + +.. code:: bash + + pip install -U pantable + +Use: + +.. code:: bash + + pandoc -F pantable -o README.html README.md + +Syntax +------ + +Fenced code blocks is used, with a class ``table``. See [Example]. + +Optionally, YAML metadata block can be used within the fenced code +block, following standard pandoc YAML metadata block syntax. 7 metadata +keys are recognized: + +- ``caption``: the caption of the table. If omitted, no caption will be + inserted. Default: disabled. + +- ``alignment``: a string of characters among ``L,R,C,D``, + case-insensitive, corresponds to Left-aligned, Right-aligned, + Center-aligned, Default-aligned respectively. e.g. ``LCRD`` for a + table with 4 columns. Default: ``DDD...`` + +- ``width``: a list of relative width corresponding to the width of + each columns. e.g. + + .. code:: yaml + + - width + - 0.1 + - 0.2 + - 0.3 + - 0.4 + + Default: auto calculated from the length of each line in table cells. + +- ``table-width``: the relative width of the table (e.g. relative to + ``\linewidth``). default: 1.0 + +- ``header``: If it has a header row or not. True/False/yes/NO are + accepted, case-insensitive. default: True + +- ``markdown``: If CSV table cell contains markdown syntax or not. Same + as above. Default: False + +- ``include``: the path to an CSV file, can be relative/absolute. If + non-empty, override the CSV in the CodeBlock. default: None + +When the metadata keys is invalid, the default will be used instead. +Note that width and table-width accept fractions as well. + +``pantable2csv`` +================ + +This one is the inverse of ``pantable``, a panflute filter to convert +any native pandoc tables into the CSV table format used by pantable. + +Effectively, ``pantable`` forms a “CSV Reader”, and ``pantable2csv`` +forms a “CSV Writer”. It allows you to convert back and forth between +these 2 formats. + +For example, in the markdown source: + +:: + + +--------+---------------------+--------------------------+ + | First | defaulted to be | can be disabled | + | row | header row | | + +========+=====================+==========================+ + | 1 | cell can contain | It can be aribrary block | + | | **markdown** | element: | + | | | | + | | | - following standard | + | | | markdown syntax | + | | | - like this | + +--------+---------------------+--------------------------+ + | 2 | Any markdown | $$E = mc^2$$ | + | | syntax, e.g. | | + +--------+---------------------+--------------------------+ + + : *Awesome* **Markdown** Table + +running ``pandoc -F pantable2csv -o output.md input.md``, it becomes + +:: + + ``` {.table} + --- + alignment: DDD + caption: '*Awesome* **Markdown** Table' + header: true + markdown: true + table-width: 0.8055555555555556 + width: [0.125, 0.3055555555555556, 0.375] + --- + First row,defaulted to be header row,can be disabled + 1,cell can contain **markdown**,"It can be aribrary block element: + + - following standard markdown syntax + - like this + " + 2,"Any markdown syntax, e.g.",$$E = mc^2$$ + ``` + +Related Filters +=============== + +The followings are pandoc filters written in Haskell that provide +similar functionality. This filter is born after testing with theirs. + +- `baig/pandoc-csv2table: A Pandoc filter that renders CSV as Pandoc + Markdown Tables. `__ +- `mb21/pandoc-placetable: Pandoc filter to include CSV data (from file + or URL) `__ +- `sergiocorreia/panflute/csv-tables.py `__ + ++--------+--------------------+------------+-------------+--------------------------+ +| | pandoc-csv2table | pandoc-pla | panflute ex | pantable | +| | | cetable | ample | | ++========+====================+============+=============+==========================+ +| captio | caption | caption | title | caption | +| n | | | | | ++--------+--------------------+------------+-------------+--------------------------+ +| aligns | aligns = LRCD | aligns = L | | aligns = LRCD | +| | | RCD | | | ++--------+--------------------+------------+-------------+--------------------------+ +| width | | widths = " | | width: [0.5, 0.2, 0.3] | +| | | 0.5 0.2 0. | | | +| | | 3" | | | ++--------+--------------------+------------+-------------+--------------------------+ +| table- | | | | table-width: 1.0 | +| width | | | | | ++--------+--------------------+------------+-------------+--------------------------+ +| header | header = yes \| no | header = y | has\_header | header: True \| False \| | +| | | es \| no | : True \| F | yes \| NO | +| | | | alse | | ++--------+--------------------+------------+-------------+--------------------------+ +| markdo | | inlinemark | | markdown: True \| False | +| wn | | down | | \| yes \| NO | ++--------+--------------------+------------+-------------+--------------------------+ +| source | source | file | source | include | ++--------+--------------------+------------+-------------+--------------------------+ +| others | type = simple \| m | | | | +| | ultiline \| grid \ | | | | +| | | pipe | | | | ++--------+--------------------+------------+-------------+--------------------------+ +| | | delimiter | | | ++--------+--------------------+------------+-------------+--------------------------+ +| | | quotechar | | | ++--------+--------------------+------------+-------------+--------------------------+ +| | | id (wrappe | | | +| | | d by div) | | | ++--------+--------------------+------------+-------------+--------------------------+ +| Notes | | | | width are auto-calculate | +| | | | | d when width is not spec | +| | | | | ified | ++--------+--------------------+------------+-------------+--------------------------+ + +.. [1] + In pandoc 1.19, grid-tables is improved to support all features + available to the AST too. + +.. |Build Status| image:: https://travis-ci.org/ickc/pantable.svg?branch=master + :target: https://travis-ci.org/ickc/pantable +.. |GitHub Releases| image:: https://img.shields.io/github/tag/ickc/pantable.svg?label=github+release + :target: https://github.com/ickc/pantable/releases +.. |PyPI version| image:: https://img.shields.io/pypi/v/pantable.svg + :target: https://pypi.python.org/pypi/pantable/ +.. |Development Status| image:: https://img.shields.io/pypi/status/pantable.svg + :target: https://pypi.python.org/pypi/pantable/ +.. |Python version| image:: https://img.shields.io/pypi/pyversions/pantable.svg + :target: https://pypi.python.org/pypi/pantable/ +.. |License| image:: https://img.shields.io/pypi/l/pantable.svg +.. |Coveralls| image:: https://img.shields.io/coveralls/ickc/pantable.svg + :target: https://coveralls.io/github/ickc/pantable diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..c84642d9 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,188 @@ +--- +fontsize: 11pt +documentclass: memoir +classoption: article +geometry: inner=1in, outer=1in, top=1in, bottom=1.25in +title: CSV Tables in Markdown --- Pandoc Filter for CSV Tables +... + +The pantable package comes with 2 pandoc filters, `pantable.py` and `pantable2csv.py`. `pantable` is the main filter, introducing a syntax to include CSV table in markdown source. `pantable2csv` complements `pantable`, is the inverse of `pantable`, which convert native pandoc tables into the CSV table format defined by `pantable`. + +Some example uses are: + +1. You already have tables in CSV format. + +2. You feel that directly editing markdown table is troublesome. You want a spreadsheet interface to edit, but want to convert it to native pandoc table for higher readability. And this process might go back and forth. + +3. You want lower-level control on the table and column widths. + +4. You want to use all table features supported by the pandoc's internal AST table format, which is not possible in markdown for pandoc \<= 1.18.^[In pandoc 1.19, grid-tables is improved to support all features available to the AST too.] + +# `pantable` + +This allows CSV tables, optionally containing markdown syntax (disabled by default), to be put in markdown as a fenced code blocks. + +## Example + +Also see the README in [GitHub Pages](https://ickc.github.io/pantable/). There's a [LaTeX output](https://ickc.github.io/pantable/README.pdf) too. + +~~~ +```table +--- +caption: '*Awesome* **Markdown** Table' +alignment: RC +table-width: 2/3 +markdown: True +--- +First row,defaulted to be header row,can be disabled +1,cell can contain **markdown**,"It can be aribrary block element: + +- following standard markdown syntax +- like this" +2,"Any markdown syntax, e.g.",$$E = mc^2$$ +``` +~~~ + +becomes + +```table +--- +caption: '*Awesome* **Markdown** Table' +alignment: RC +table-width: 2/3 +markdown: True +--- +First row,defaulted to be header row,can be disabled +1,cell can contain **markdown**,"It can be aribrary block element: + +- following standard markdown syntax +- like this" +2,"Any markdown syntax, e.g.",$$E = mc^2$$ +``` + +(The equation might not work if you view this on PyPI.) + +## Install and Use + +Install: + +```bash +pip install -U pantable +``` + +Use: + +```bash +pandoc -F pantable -o README.html README.md +``` + +## Syntax + +Fenced code blocks is used, with a class `table`. See [Example]. + +Optionally, YAML metadata block can be used within the fenced code block, following standard pandoc YAML metadata block syntax. 7 metadata keys are recognized: + +- `caption`: the caption of the table. If omitted, no caption will be inserted. + Default: disabled. + +- `alignment`: a string of characters among `L,R,C,D`, case-insensitive, + corresponds to Left-aligned, Right-aligned, + Center-aligned, Default-aligned respectively. + e.g. `LCRD` for a table with 4 columns. + Default: `DDD...` + +- `width`: a list of relative width corresponding to the width of each columns. + e.g. + + ```yaml + - width + - 0.1 + - 0.2 + - 0.3 + - 0.4 + ``` + + Default: auto calculated from the length of each line in table cells. + +- `table-width`: the relative width of the table (e.g. relative to `\linewidth`). + default: 1.0 + +- `header`: If it has a header row or not. + True/False/yes/NO are accepted, case-insensitive. + default: True + +- `markdown`: If CSV table cell contains markdown syntax or not. + Same as above. + Default: False + +- `include`: the path to an CSV file, can be relative/absolute. + If non-empty, override the CSV in the CodeBlock. + default: None + +When the metadata keys is invalid, the default will be used instead. +Note that width and table-width accept fractions as well. + +# `pantable2csv` + +This one is the inverse of `pantable`, a panflute filter to convert any native pandoc tables into the CSV table format used by pantable. + +Effectively, `pantable` forms a "CSV Reader", and `pantable2csv` forms a "CSV Writer". It allows you to convert back and forth between these 2 formats. + +For example, in the markdown source: + +~~~ ++--------+---------------------+--------------------------+ +| First | defaulted to be | can be disabled | +| row | header row | | ++========+=====================+==========================+ +| 1 | cell can contain | It can be aribrary block | +| | **markdown** | element: | +| | | | +| | | - following standard | +| | | markdown syntax | +| | | - like this | ++--------+---------------------+--------------------------+ +| 2 | Any markdown | $$E = mc^2$$ | +| | syntax, e.g. | | ++--------+---------------------+--------------------------+ + +: *Awesome* **Markdown** Table +~~~ + +running `pandoc -F pantable2csv -o output.md input.md`{.bash}, it becomes + +~~~ +``` {.table} +--- +alignment: DDD +caption: '*Awesome* **Markdown** Table' +header: true +markdown: true +table-width: 0.8055555555555556 +width: [0.125, 0.3055555555555556, 0.375] +--- +First row,defaulted to be header row,can be disabled +1,cell can contain **markdown**,"It can be aribrary block element: + +- following standard markdown syntax +- like this +" +2,"Any markdown syntax, e.g.",$$E = mc^2$$ +``` +~~~ + +# Related Filters + +The followings are pandoc filters written in Haskell that provide similar functionality. This filter is born after testing with theirs. + +- [baig/pandoc-csv2table: A Pandoc filter that renders CSV as Pandoc Markdown Tables.](https://github.com/baig/pandoc-csv2table) +- [mb21/pandoc-placetable: Pandoc filter to include CSV data (from file or URL)](https://github.com/mb21/pandoc-placetable) +- [sergiocorreia/panflute/csv-tables.py](https://github.com/sergiocorreia/panflute/blob/1ddcaba019b26f41f8c4f6f66a8c6540a9c5f31a/docs/source/csv-tables.py) + +```table +--- +Caption: Comparison +include: docs/comparison.csv +... +``` + diff --git a/docs/README.pdf b/docs/README.pdf new file mode 100644 index 00000000..c26a3365 Binary files /dev/null and b/docs/README.pdf differ diff --git a/docs/badges.markdown b/docs/badges.markdown new file mode 100644 index 00000000..086f15ab --- /dev/null +++ b/docs/badges.markdown @@ -0,0 +1,9 @@ +[![Build Status](https://travis-ci.org/ickc/pantable.svg?branch=master)](https://travis-ci.org/ickc/pantable) +[![GitHub Releases](https://img.shields.io/github/tag/ickc/pantable.svg?label=github+release)](https://github.com/ickc/pantable/releases) +[![PyPI version](https://img.shields.io/pypi/v/pantable.svg)](https://pypi.python.org/pypi/pantable/) +[![Development Status](https://img.shields.io/pypi/status/pantable.svg)](https://pypi.python.org/pypi/pantable/) +[![Python version](https://img.shields.io/pypi/pyversions/pantable.svg)](https://pypi.python.org/pypi/pantable/) + +![License](https://img.shields.io/pypi/l/pantable.svg) +[![Coveralls](https://img.shields.io/coveralls/ickc/pantable.svg)](https://coveralls.io/github/ickc/pantable) + diff --git a/docs/comparison.csv b/docs/comparison.csv new file mode 100644 index 00000000..cfc920a9 --- /dev/null +++ b/docs/comparison.csv @@ -0,0 +1,13 @@ +,pandoc-csv2table,pandoc-placetable,panflute example,pantable +caption,caption,caption,title,caption +aligns,aligns = LRCD,aligns = LRCD,,aligns = LRCD +width,,"widths = ""0.5 0.2 0.3""",,"width: [0.5, 0.2, 0.3]" +table-width,,,,table-width: 1.0 +header,header = yes | no,header = yes | no,has_header: True | False,header: True | False | yes | NO +markdown,,inlinemarkdown,,markdown: True | False | yes | NO +source,source,file,source,include +others,type = simple | multiline | grid | pipe,,, +,,delimiter,, +,,quotechar,, +,,id (wrapped by div),, +Notes,,,,width are auto-calculated when width is not specified \ No newline at end of file diff --git a/docs/example.csv b/docs/example.csv new file mode 100644 index 00000000..93637e4d --- /dev/null +++ b/docs/example.csv @@ -0,0 +1,3 @@ +First row,defaulted to be header row,can be disabled +1,cell can contain **markdown**,"It can be aribrary block element: - following standard markdown syntax - like this" +2,"Any markdown syntax, e.g.",$$E = mc^2$$ \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..0dd69916 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,308 @@ + + + + + + + CSV Tables in Markdown — Pandoc Filter for CSV Tables + + + + + + + +
+

CSV Tables in Markdown — Pandoc Filter for CSV Tables

+

December 4, 2016

+
+ +

Build Status GitHub Releases PyPI version Development Status Python version License Coveralls

+

The pantable package comes with 2 pandoc filters, pantable.py and pantable2csv.py. pantable is the main filter, introducing a syntax to include CSV table in markdown source. pantable2csv complements pantable, is the inverse of pantable, which convert native pandoc tables into the CSV table format defined by pantable.

+

Some example uses are:

+
    +
  1. You already have tables in CSV format.

  2. +
  3. You feel that directly editing markdown table is troublesome. You want a spreadsheet interface to edit, but want to convert it to native pandoc table for higher readability. And this process might go back and forth.

  4. +
  5. You want lower-level control on the table and column widths.

  6. +
  7. You want to use all table features supported by the pandoc’s internal AST table format, which is not possible in markdown for pandoc <= 1.18.1

  8. +
+

1 pantable

+

This allows CSV tables, optionally containing markdown syntax (disabled by default), to be put in markdown as a fenced code blocks.

+

1.1 Example

+

Also see the README in GitHub Pages. There’s a LaTeX output too.

+
```table
+---
+caption: '*Awesome* **Markdown** Table'
+alignment: RC
+table-width: 2/3
+markdown: True
+---
+First row,defaulted to be header row,can be disabled
+1,cell can contain **markdown**,"It can be aribrary block element:
+
+- following standard markdown syntax
+- like this"
+2,"Any markdown syntax, e.g.",$$E = mc^2$$
+```
+

becomes

+ + +++++ + + + + + + + + + + + + + + + + + + + +
Awesome Markdown Table

First row

defaulted to be header row

can be disabled

1

cell can contain markdown

It can be aribrary block element:

+
    +
  • following standard markdown syntax
  • +
  • like this
  • +

2

Any markdown syntax, e.g.


E = mc2

+

(The equation might not work if you view this on PyPI.)

+

1.2 Install and Use

+

Install:

+
pip install -U pantable
+

Use:

+
pandoc -F pantable -o README.html README.md
+

1.3 Syntax

+

Fenced code blocks is used, with a class table. See Example.

+

Optionally, YAML metadata block can be used within the fenced code block, following standard pandoc YAML metadata block syntax. 7 metadata keys are recognized:

+
    +
  • caption: the caption of the table. If omitted, no caption will be inserted. Default: disabled.

  • +
  • alignment: a string of characters among L,R,C,D, case-insensitive, corresponds to Left-aligned, Right-aligned, Center-aligned, Default-aligned respectively. e.g. LCRD for a table with 4 columns. Default: DDD...

  • +
  • width: a list of relative width corresponding to the width of each columns. e.g.

    +
    - width
    +    - 0.1
    +    - 0.2
    +    - 0.3
    +    - 0.4
    +

    Default: auto calculated from the length of each line in table cells.

  • +
  • table-width: the relative width of the table (e.g. relative to \linewidth). default: 1.0

  • +
  • header: If it has a header row or not. True/False/yes/NO are accepted, case-insensitive. default: True

  • +
  • markdown: If CSV table cell contains markdown syntax or not. Same as above. Default: False

  • +
  • include: the path to an CSV file, can be relative/absolute. If non-empty, override the CSV in the CodeBlock. default: None

  • +
+

When the metadata keys is invalid, the default will be used instead. Note that width and table-width accept fractions as well.

+

2 pantable2csv

+

This one is the inverse of pantable, a panflute filter to convert any native pandoc tables into the CSV table format used by pantable.

+

Effectively, pantable forms a “CSV Reader”, and pantable2csv forms a “CSV Writer”. It allows you to convert back and forth between these 2 formats.

+

For example, in the markdown source:

+
+--------+---------------------+--------------------------+
+| First  | defaulted to be     | can be disabled          |
+| row    | header row          |                          |
++========+=====================+==========================+
+| 1      | cell can contain    | It can be aribrary block |
+|        | **markdown**        | element:                 |
+|        |                     |                          |
+|        |                     | -   following standard   |
+|        |                     |     markdown syntax      |
+|        |                     | -   like this            |
++--------+---------------------+--------------------------+
+| 2      | Any markdown        | $$E = mc^2$$             |
+|        | syntax, e.g.        |                          |
++--------+---------------------+--------------------------+
+
+: *Awesome* **Markdown** Table
+

running pandoc -F pantable2csv -o output.md input.md, it becomes

+
``` {.table}
+---
+alignment: DDD
+caption: '*Awesome* **Markdown** Table'
+header: true
+markdown: true
+table-width: 0.8055555555555556
+width: [0.125, 0.3055555555555556, 0.375]
+---
+First row,defaulted to be header row,can be disabled
+1,cell can contain **markdown**,"It can be aribrary block element:
+
+-   following standard markdown syntax
+-   like this
+"
+2,"Any markdown syntax, e.g.",$$E = mc^2$$
+```
+

3 Related Filters

+

The followings are pandoc filters written in Haskell that provide similar functionality. This filter is born after testing with theirs.

+ + +++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
pandoc-csv2tablepandoc-placetablepanflute examplepantable
captioncaptioncaptiontitlecaption
alignsaligns = LRCDaligns = LRCDaligns = LRCD
widthwidths = "0.5 0.2 0.3"width: [0.5, 0.2, 0.3]
table-widthtable-width: 1.0
headerheader = yes | noheader = yes | nohas_header: True | Falseheader: True | False | yes | NO
markdowninlinemarkdownmarkdown: True | False | yes | NO
sourcesourcefilesourceinclude
otherstype = simple | multiline | grid | pipe
delimiter
quotechar
id (wrapped by div)
Noteswidth are auto-calculated when width is not specified
+
+
+
    +
  1. In pandoc 1.19, grid-tables is improved to support all features available to the AST too.

  2. +
+
+ + diff --git a/makefile b/makefile index d8584b86..3bd168c8 100644 --- a/makefile +++ b/makefile @@ -1,32 +1,143 @@ SHELL := /usr/bin/env bash -test := $(wildcard test/*.md) -native := $(patsubst %.md,%.native,$(test)) -pdf := $(patsubst %.md,%.pdf,$(test)) +# configure engine +python := python +pip := pip +## LaTeX engine +### LaTeX workflow: pdf; xelatex; lualatex +latexmkEngine := pdf +### pandoc workflow: pdflatex; xelatex; lualatex +pandocEngine := pdflatex +## HTML +HTMLVersion := html5 +## ePub +ePubVersion := epub -filter := ./pandoc-tables.py +pantable := pantable +pantable2csv := pantable2csv + +CSSURL:=https://ickc.github.io/markdown-latex-css + +# command line arguments +pandocArgCommon := -f markdown+autolink_bare_uris-fancy_lists --toc --normalize -S -V linkcolorblue -V citecolor=blue -V urlcolor=blue -V toccolor=blue --latex-engine=$(pandocEngine) -M date="`date "+%B %e, %Y"`" +# Workbooks +## MD +pandocArgMD := -f markdown+abbreviations+autolink_bare_uris+markdown_attribute+mmd_header_identifiers+mmd_link_attributes+mmd_title_block+tex_math_double_backslash-latex_macros-auto_identifiers -t markdown+raw_tex-native_spans-simple_tables-multiline_tables-grid_tables-latex_macros --normalize -s --wrap=none --column=999 --atx-headers --reference-location=block --file-scope +## TeX/PDF +### LaTeX workflow +latexmkArg := -$(latexmkEngine) +pandocArgFragment := $(pandocArgCommon) --filter=$(pantable) +### pandoc workflow +pandocArgStandalone := $(pandocArgFragment) --toc-depth=1 -s -N +## HTML/ePub +pandocArgHTML := $(pandocArgFragment) -t $(HTMLVersion) --toc-depth=2 -s -N -c $(CSSURL)/css/common.css -c $(CSSURL)/fonts/fonts.css +pandocArgePub := $(pandocArgHTML) -t $(ePubVersion) --epub-chapter-level=2 +# GitHub README +pandocArgReadmeGitHub := $(pandocArgFragment) --toc-depth=2 -s -t markdown_github --reference-location=block +pandocArgReadmePypi := $(pandocArgFragment) -s -t rst --reference-location=block -f markdown+autolink_bare_uris-fancy_lists-implicit_header_references + +test := $(wildcard tests/*.md) +testNative := $(patsubst %.md,%.native,$(test)) +testPdf := $(patsubst %.md,%.pdf,$(test)) +testAll := $(testNative) $(testPdf) + +docs := $(wildcard docs/*.md) +# docsHtml := $(patsubst %.md,%.html,$(docs)) +docsPdf := $(patsubst %.md,%.pdf,$(docs)) +docsAll := $(docsPdf) docs/index.html README.md README.rst README.html # $(docsHtml) # Main Targets ######################################################################################################################################################################################## -all: $(native) $(pdf) +all: $(testAll) $(docsAll) +docs: $(docsAll) +readme: docs +test: pytest pep8 + coverage html +testFull: pytest pep8 pylint + coverage html + +clean: + rm -f .coverage $(testAll) README.html + rm -rf htmlcov pantable.egg-info + find . -type f -name "*.py[co]" -delete -or -type d -name "__pycache__" -delete +Clean: + rm -f .coverage $(testAll) $(docsAll) + rm -rf htmlcov pantable.egg-info + find . -type f -name "*.py[co]" -delete -or -type d -name "__pycache__" -delete + +# Making dependancies ################################################################################################################################################################################# -test/%.native: test/%.md $(filter) - pandoc -t native -F $(filter) -o $@ $< +%.native: %.md $(pantable) + pandoc -t native -F $(pantable) -o $@ $< +%.pdf: %.md $(pantable) + pandoc $(pandocArgStandalone) -o $@ $< +%.html: %.md $(pantable) + pandoc $(pandocArgHTML) $< -o $@ -test/%.pdf: test/%.md $(filter) - pandoc -F $(filter) -o $@ $< +# readme +## index.html +docs/index.html: docs/badges.markdown docs/README.md + pandoc $(pandocArgHTML) $^ -o $@ +## GitHub README +README.md: docs/badges.markdown docs/README.md + printf "%s\n\n" "" > $@ + pandoc $(pandocArgReadmeGitHub) $^ >> $@ +## PyPI README +README.rst: docs/badges.markdown docs/README.md + printf "%s\n\n" ".. This README is auto-generated from \`docs/README.md\`. Do not edit this file directly." > $@ + pandoc $(pandocArgReadmePypi) $^ >> $@ +README.html: README.rst + rst2html.py $< > $@ -# update submodule -update: - git submodule update --recursive --remote +# maintenance ######################################################################################################################################################################################### -# Automation on */*.md, in the order from draft to finish ############################################################################################################################################# +# Deploy to PyPI +## by Travis, properly git tagged +pypi: + git tag -a v$$($(python) setup.py --version) -m 'Deploy to PyPI' && git push origin v$$($(python) setup.py --version) +## Manually +pypiManual: + $(python) setup.py register -r pypitest && $(python) setup.py sdist upload -r pypitest && $(python) setup.py register -r pypi && $(python) setup.py sdist upload -r pypi -# autopep8 +init: + $(pip) install -r requirements.txt + $(pip) install -r tests/requirements.txt + +dev: + $(pip) install -e .[test] + +pytest: $(testNative) tests/test_idempotent.native + $(python) -m pytest -vv --cov=pantable tests +pytestLite: + $(python) -m pytest -vv --cov=pantable tests +tests/reference_idempotent.native: tests/test_pantable.md + pandoc --normalize -t native -F $(pantable) -F $(pantable2csv) -F $(pantable) -F $(pantable2csv) -o $@ $< +tests/test_idempotent.native: tests/reference_idempotent.native + pandoc --normalize -f native -t native -F $(pantable) -F $(pantable2csv) -o $@ $< + +# check python styles pep8: - find . -maxdepth 2 -iname "*.py" | xargs -i -n1 -P8 autopep8 --in-place --aggressive --aggressive {} + pep8 . --ignore=E402,E501,E731 +pep8Strict: + pep8 . +pyflakes: + pyflakes . +flake8: + flake8 . +pylint: + pylint pantable + +# cleanup python +autopep8: + autopep8 . --recursive --in-place --pep8-passes 2000 --verbose +autopep8Aggressive: + autopep8 . --recursive --in-place --pep8-passes 2000 --verbose --aggressive --aggressive + +# pasteurize +past: + pasteurize -wnj 4 . -# cleanup source code +# cleanup markdown cleanup: style normalize ## Normalize white spaces: ### 1. Add 2 trailing newlines @@ -41,4 +152,4 @@ normalize: ### 1. pandoc from markdown to markdown ### 2. transform unicode non-breaking space back to `\ ` style: - find . -maxdepth 2 -mindepth 2 -iname "*.md" | xargs -i -n1 -P8 bash -c 'pandoc -o $$0 $$0 && sed -i -e '"'"'s/ /\\ /g'"'"' $$0' {} + find . -maxdepth 2 -mindepth 2 -iname "*.md" | xargs -i -n1 -P8 bash -c 'pandoc $(pandocArgMD) -o $$0 $$0 && sed -i -e '"'"'s/ /\\ /g'"'"' $$0' {} \ No newline at end of file diff --git a/pandoc-tables.py b/pandoc-tables.py deleted file mode 100755 index e6d88087..00000000 --- a/pandoc-tables.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env python3 - -""" -Panflute filter to parse table in fenced YAML code blocks. -Currently only CSV table is supported. - -7 metadata keys are recognized: - -- caption: the caption of the table. If omitted, no caption will be inserted. -- alignment: a string of characters among L,R,C,D, case-insensitive, - corresponds to Left-aligned, Right-aligned, Center-aligned, Default-aligned respectively. - e.g. LCRD for a table with 4 columns - default: DDD... -- width: a list of relative width corresponding to the width of each columns. - default: auto calculate from the length of line in a (potentially multiline) cell. -- table-width: the relative width of the table (comparing to, say, \linewidth). - default: 1.0 -- header: If it has a header row. default: true -- markdown: If CSV table cell contains markdown syntax. default: True -- include: the path to an CSV file. If non-empty, override the CSV in the CodeBlock. - default: None - -When the metadata keys is invalid, the default will be used instead. - -e.g. - -```markdown -~~~table ---- -caption: "*Great* Title" -alignment: LRC -width: - - 0.1 - - 0.2 - - 0.3 - - 0.4 -header: False -markdown: True -... -**_Fruit_**,~~Price~~,_Number_,`Advantages` -*Bananas~1~*,$1.34,12~units~,"Benefits of eating bananas -(**Note the appropriately -rendered block markdown**): - -- _built-in wrapper_ -- ~~**bright color**~~ - -" -*Oranges~2~*,$2.10,5^10^~units~,"Benefits of eating oranges: - -- **cures** scurvy -- `tasty`" -~~~ -``` -""" - -import io -import os -import csv -import panflute - - -def to_bool(x): - """ - Do nothing if x is boolean, - return `False` if it is "false" or "no" (case-insensitive), - otherwise return `True`. - """ - if not isinstance(x, bool): - if str(x).lower() in ("false", "no"): - x = False - else: - x = True - return x - - -def get_table_options(options): - """ - It parses the options output from `panflute.yaml_filter` and - return it as variables `(caption, alignment, width, table_width, header, markdown)`. - """ - caption = options.get('caption') - alignment = options.get('alignment') - width = options.get('width') - table_width = options.get('table-width', 1.0) - header = options.get('header', True) - markdown = options.get('markdown', True) - include = options.get('include', None) - return (caption, alignment, width, table_width, header, markdown, include) - - -def check_table_options(width, table_width, header, markdown, include): - """ - It sets the varaibles to default if they are invalid: - - - `width` set to `None` when invalid, each element in `width` set to `0` when negative - - `table_width`: set to `1.0` if invalid or not positive - - set `header` to `True` if invalid - - set `markdown` to `True` if invalid - """ - try: - width = [(float(x) if float(x) >= 0 else 0) for x in width] - except (TypeError, ValueError): - width = None - try: - table_width = float(table_width) if float(table_width) > 0 else 1.0 - except (TypeError, ValueError): - table_width = 1.0 - header = to_bool(header) - markdown = to_bool(markdown) - if include is not None: - if not os.path.isfile(include): - include = None - return (width, table_width, header, markdown, include) - - -def parse_table_options( - caption, - alignment, - width, - table_width, - raw_table_list): - """ - `caption` is assumed to contain markdown, as in standard pandoc YAML metadata - `alignment` string is parsed into pandoc format (AlignDefault, etc.) - `width` is auto-calculated if not given in YAML - """ - # parse caption - if caption is not None: - caption = panflute.convert_text(str(caption))[0].content - # preparation: get no of columns of the table - number_of_columns = len(raw_table_list[0]) - # parse alignment - if alignment is not None: - alignment = str(alignment) - parsed_alignment = [] - for i in range(number_of_columns): - try: - if alignment[i].lower() == "l": - parsed_alignment.append("AlignLeft") - elif alignment[i].lower() == "c": - parsed_alignment.append("AlignCenter") - elif alignment[i].lower() == "r": - parsed_alignment.append("AlignRight") - else: - parsed_alignment.append("AlignDefault") - except IndexError: - for i in range(number_of_columns - len(parsed_alignment)): - parsed_alignment.append("AlignDefault") - alignment = parsed_alignment - # calculate width - if width is None: - width_abs = [max([max([len(line) for line in row[i].split("\n")]) - for row in raw_table_list]) for i in range(number_of_columns)] - width_tot = sum(width_abs) - try: - width = [ - width_abs[i] / - width_tot * - table_width for i in range(number_of_columns)] - except ZeroDivisionError: - width = None - return (caption, alignment, width) - - -def read_csv(data, include): - """ - read csv and return the table in list - """ - if include is not None: - with open(include) as f: - raw_table_list = list(csv.reader(f)) - else: - with io.StringIO(data) as f: - raw_table_list = list(csv.reader(f)) - return raw_table_list - - -def parse_table_list(raw_table_list, markdown): - """ - read table in list and return panflute table format - """ - body = [] - for row in raw_table_list: - if markdown: - cells = [ - panflute.TableCell( - *panflute.convert_text(x)) for x in row] - else: - cells = [ - panflute.TableCell( - panflute.Plain( - panflute.Str(x))) for x in row] - body.append(panflute.TableRow(*cells)) - return body - - -def convert2table(options, data, element, doc): - # get table options from YAML metadata - caption, alignment, width, table_width, header, markdown, include = get_table_options( - options) - # check table options - width, table_width, header, markdown, include = check_table_options( - width, table_width, header, markdown, include) - # parse csv to list - raw_table_list = read_csv(data, include) - # parse list to panflute table - body = parse_table_list(raw_table_list, markdown) - # parse table options - caption, alignment, width = parse_table_options( - caption, alignment, width, table_width, raw_table_list) - # finalize table according to metadata - header_row = body.pop(0) if header else None - table = panflute.Table( - *body, - caption=caption, - alignment=alignment, - width=width, - header=header_row) - return table - -# We'll only run this for CodeBlock elements of class 'table' - - -def main(doc=None): - return panflute.run_filter( - panflute.yaml_filter, - tag='table', - function=convert2table, - strict_yaml=True) - -if __name__ == '__main__': - main() diff --git a/pantable/__init__.py b/pantable/__init__.py new file mode 100755 index 00000000..58f3ace6 --- /dev/null +++ b/pantable/__init__.py @@ -0,0 +1 @@ +from .version import __version__ diff --git a/pantable/pantable.py b/pantable/pantable.py new file mode 100755 index 00000000..f7228c5c --- /dev/null +++ b/pantable/pantable.py @@ -0,0 +1,445 @@ +r""" +Panflute filter to parse table in fenced YAML code blocks. +Currently only CSV table is supported. + +7 metadata keys are recognized: + +- caption: the caption of the table. If omitted, no caption will be inserted. +- alignment: a string of characters among L,R,C,D, case-insensitive, + corresponds to Left-aligned, Right-aligned, + Center-aligned, Default-aligned respectively. + e.g. LCRD for a table with 4 columns + default: DDD... +- width: a list of relative width corresponding to the width of each columns. + default: auto calculate from the length of each line in table cells. +- table-width: the relative width of the table (e.g. relative to \linewidth). + default: 1.0 +- header: If it has a header row. default: true +- markdown: If CSV table cell contains markdown syntax. default: False +- include: the path to an CSV file. + If non-empty, override the CSV in the CodeBlock. + default: None + +When the metadata keys is invalid, the default will be used instead. +Note that width and table-width accept fractions as well. + +e.g. + +```table +--- +caption: '*Awesome* **Markdown** Table' +alignment: RC +table-width: 2/3 +markdown: True +--- +First row,defaulted to be header row,can be disabled +1,cell can contain **markdown**,"It can be aribrary block element: + +- following standard markdown syntax +- like this" +2,"Any markdown syntax, e.g.",$$E = mc^2$$ +``` +""" + +import re +import csv +import fractions +import io +import panflute + +import sys +py2 = sys.version_info[0] == 2 + +# begin helper functions + + +def get_width(options, number_of_columns): + """ + get width: set to `None` when + + 1. not given + 2. not a list + 3. length not equal to the number of columns + 4. negative entries + """ + try: + # if width not exists, exits immediately through except + width = options['width'] + assert len(width) == number_of_columns + custom_float = lambda x: float(fractions.Fraction(x)) + width = [custom_float(x) for x in options['width']] + assert all(i >= 0 for i in width) + except KeyError: + width = None + except (AssertionError, ValueError, TypeError): + width = None + panflute.debug("pantable: invalid width") + return width + + +def get_table_width(options): + """ + `table-width` set to `1.0` if invalid + """ + try: + table_width = float(fractions.Fraction( + (options.get('table-width', 1.0)))) + assert table_width > 0 + except (ValueError, AssertionError, TypeError): + table_width = 1.0 + panflute.debug("pantable: invalid table-width") + return table_width + + +def table_filter_cell(cell, table_filter): + """Match the cell data to the given table_filter + + table_filter is a dictionary with the supported keys for filtering. Empty + dict always match the cell. See `apply_table_filter` for more info on the + dict structure. + + Args: + cell: Str of cell content to be matched against. + table_filter: Dict of table_filter rules. + + """ + if cell is None: + # None indicates that the cell index is out of bounds, we need to + # pretend that we keep it, else the row will be removed because of the + # out of bounds index. + return True + elif not table_filter: + # Dict is empty, aka no filter function, thus keep the cell + return True + elif 'filter' in table_filter: + str_universal = basestring if py2 else (str, bytes) + if isinstance(table_filter['filter'], str_universal): + return cell == table_filter['filter'] + elif isinstance(table_filter['filter'], list): + # Assuming all the list items are of type 'basestring' + return any([cell == match for match in table_filter['filter']]) + else: + raise Exception("Unhandled filter type: {}" + .format(table_filter['filter'])) + elif 'regex' in table_filter: + return re.match(table_filter['regex'], cell) is not None + + return True + +# end helper functions + + +def auto_width(table_width, number_of_columns, table_list): + """ + `width` is auto-calculated if not given in YAML + It also returns None when table is empty. + """ + # calculate width + # The +3 match the way pandoc handle width, see jgm/pandoc commit 0dfceda + width_abs = [3 + max( + [max( + [len(line) for line in row[column_index].split("\n")] + ) for row in table_list] + ) for column_index in range(number_of_columns)] + try: + width_tot = sum(width_abs) + # when all are 3 means all are empty, see comment above + assert width_tot != 3 * number_of_columns + width = [ + each_width / width_tot * table_width + for each_width in width_abs + ] + except AssertionError: + width = None + panflute.debug("pantable: table is empty") + return width + + +def parse_alignment(alignment_string, number_of_columns): + """ + `alignment` string is parsed into pandoc format (AlignDefault, etc.). + Cases are checked: + + - if not given, return None (let panflute handle it) + - if wrong type + - if too long + - if invalid characters are given + - if too short + """ + # alignment string can be None or empty; return None: set to default by + # panflute + if not alignment_string: + return None + + # prepare alignment_string + try: + # test valid type + str_universal = basestring if py2 else str + if not isinstance(alignment_string, str_universal): + raise TypeError + number_of_alignments = len(alignment_string) + # truncate and debug if too long + assert number_of_alignments <= number_of_columns + except TypeError: + panflute.debug("pantable: alignment string is invalid") + # return None: set to default by panflute + return None + except AssertionError: + alignment_string = alignment_string[:number_of_columns] + panflute.debug( + "pantable: alignment string is too long, truncated instead.") + + # parsing alignment + align_dict = {'l': "AlignLeft", + 'c': "AlignCenter", + 'r': "AlignRight", + 'd': "AlignDefault"} + try: + alignment = [align_dict[i.lower()] for i in alignment_string] + except KeyError: + panflute.debug( + "pantable: alignment: invalid character found, default is used instead.") + return None + + # fill up with default if too short + if number_of_columns > number_of_alignments: + alignment += ["AlignDefault" for __ in range( + number_of_columns - number_of_alignments)] + + return alignment + + +def apply_table_filter(options, rows): + """Apply the filter to the rows and/or columns if specified in the options. + + If the filter is not specified or is an empty list, then the table is + not modified. + + Each element in the filter list must be an integer or a dictionary + with at least the key 'col'. + + Specifying an integer in the filter list makes sure that the column + index is kept (first column index is 0 -- python list indexing), all other + columns are removed. + + Specifying a dictionary with at least the 'col' key, gives the optional + possibility of specifying the following keys in the dictionary (note: the + keys are mutually exclusive and specifying more than one has undefined + behaviour). + + - filter: filters out (removes) the row, if the content inside this + column doesn't match (exact string matching of the value of this key + and the content of the cell). The value may be a list of strings to + be matched. + + - regex: filters out (removes) the row, if the content inside this + column doesn't match. The value of this key is placed directly into + `re.match(pattern, string)` as the `pattern` and the cell value as + the `string`. Note: Currently we assume that a small amount of + regex's is used, such that we don't have to deal with compiling the + regex's, but rely on the built in caching to handle it for us. + + + Example: This example won't filter out any column, but it demonstrates the + three different ways that you may specify a table-filter. Just try and + make changes to either one of them, and see how either columns or rows will + be filtered from the resulting table. + + ```{.table} + --- + caption: "*Bar* table" + markdown: yes + table-filter: + - 0 + - col: 1 + regex: ".*B|[\\d]" + - col: 2 + filter: ['C', '3'] + --- + A,B,C + 1,2,3 + ``` + + Args: + options: Dict of the YAML defined in the beginning of the CodeBlock + rows: A generator over the rows in the table. + + """ + table_filter = options.get('table-filter', []) + if table_filter == []: + # Return the rows unchanged. + return rows + + # Normalise the table_filter list into a dictionary, so we can easily lookup + # column indexes -- Mainly converting integer filters to dictionaries. The + # value of each index is a dictionary, which is empty if no filter was + # specified. + table_filter_dict = {} + for cell_filter in table_filter: + if isinstance(cell_filter, int): + table_filter_dict[cell_filter] = {} + elif isinstance(cell_filter, dict): + # Verify that we have a 'col' key + col = cell_filter.get('col', None) + assert col is not None, "Dictionary type table filters must contain a 'col' key: {}" \ + .format(cell_filter) + # remove the 'col' key and convert it to an int. + del cell_filter['col'] + col = int(col) + # Add the remaining dict as our filter for this column index + table_filter_dict[col] = cell_filter + else: + raise Exception("table-filter element is of non supported type: {}" + .format(cell_filter)) + + # Lastly we need to iterate over the rows and only return the rows and + # columns that should be kept. + table_filter_keys = [k for k, v in table_filter_dict.items() + if 'exclude' not in v or bool(v['exclude']) == False] + # We need to handle if the first row is a header. + if 'header' in options and options['header']: + header_row = rows[0] + rows = rows[1:] + yield [cell for idx, cell in enumerate(header_row) if idx in table_filter_keys] + + for row in rows: + # This dict should be fairly small,m so `.items()` is fine to use in PY2 + # where it returns a list instead of a generator. + # + # If not all table_filters match this row, then filter it out by + # continuing to the next. + try: + if not all([table_filter_cell(row[idx], cell_filter) for \ + idx, cell_filter in table_filter_dict.items()]): + continue + except IndexError: + raise IndexError("You specified a column index (zero indexed) that " + "was bigger than the number of columns ({}) in the row: '{}'" + .format(len(row), row)) + + # Remove the non specified columns, and return the resulting row. + yield [cell for idx, cell in enumerate(row) if idx in table_filter_keys] + + +def read_data(options, include, data): + """ + read csv and return the table in list. + Return None when the include path is invalid. + """ + if include is None: + if py2: + data = data.encode('utf-8') + io_universal = io.BytesIO if py2 else io.StringIO + with io_universal(data) as file: + raw_table_list = list(csv.reader(file)) + else: + try: + with open(str(include)) as file: + raw_table_list = list(csv.reader(file)) + except IOError: # FileNotFoundError is not in Python2 + raw_table_list = None + panflute.debug("pantable: file not found from the path", include) + + return list(apply_table_filter(options, raw_table_list)) + + +def regularize_table_list(raw_table_list): + """ + When the length of rows are uneven, make it as long as the longest row. + """ + length_of_rows = [len(row) for row in raw_table_list] + number_of_columns = max(length_of_rows) + try: + assert all(i == number_of_columns for i in length_of_rows) + table_list = raw_table_list + except AssertionError: + table_list = [ + row + ['' for __ in range(number_of_columns - len(row))] for row in raw_table_list] + panflute.debug( + "pantable: table rows are of irregular length. Empty cells appended.") + return (table_list, number_of_columns) + + +def parse_table_list(markdown, table_list): + """ + read table in list and return panflute table format + """ + # make functions local + to_table_row = panflute.TableRow + if markdown: + to_table_cell = lambda x: panflute.TableCell(*panflute.convert_text(x)) + else: + to_table_cell = lambda x: panflute.TableCell( + panflute.Plain(panflute.Str(x))) + return [to_table_row(*[to_table_cell(x) for x in row]) for row in table_list] + + +def convert2table(options, data, **__): + """ + provided to panflute.yaml_filter to parse its content as pandoc table. + """ + # prepare table in list from data/include + raw_table_list = read_data(options, options.get('include', None), data) + # delete element if table is empty (by returning []) + # element unchanged if include is invalid (by returning None) + try: + assert raw_table_list and raw_table_list is not None + except AssertionError: + panflute.debug("pantable: table is empty or include is invalid") + # [] means delete the current element; None means kept as is + return raw_table_list + # regularize table: all rows should have same length + table_list, number_of_columns = regularize_table_list(raw_table_list) + + # Initialize the `options` output from `panflute.yaml_filter` + # parse width + width = get_width(options, number_of_columns) + # auto-width when width is not specified + if width is None: + width = auto_width(get_table_width( + options), number_of_columns, table_list) + # delete element if table is empty (by returning []) + # width remains None only when table is empty + try: + assert width is not None + except AssertionError: + panflute.debug("pantable: table is empty") + return [] + # parse alignment + alignment = parse_alignment(options.get( + 'alignment', None), number_of_columns) + header = options.get('header', True) + markdown = options.get('markdown', False) + + # get caption: parsed as markdown into panflute AST if non-empty. + caption = panflute.convert_text(str(options['caption']))[ + 0].content if 'caption' in options else None + # parse list to panflute table + table_body = parse_table_list(markdown, table_list) + # extract header row + header_row = table_body.pop(0) if ( + len(table_body) > 1 and header + ) else None + return panflute.Table( + *table_body, + caption=caption, + alignment=alignment, + width=width, + header=header_row + ) + + +def main(_=None): + """ + Fenced code block with class table will be parsed using + panflute.yaml_filter with the fuction convert2table above. + """ + return panflute.run_filter( + panflute.yaml_filter, + tag='table', + function=convert2table, + strict_yaml=True + ) + +if __name__ == '__main__': + main() diff --git a/pantable/pantable2csv.py b/pantable/pantable2csv.py new file mode 100755 index 00000000..b034d24e --- /dev/null +++ b/pantable/pantable2csv.py @@ -0,0 +1,132 @@ +r""" +Panflute filter to convert any native pandoc tables into the CSV table format used by pantable: + +- in code-block with class table +- metadata in YAML +- table in CSV + +e.g. + +~~~markdown ++--------+---------------------+--------------------------+ +| First | defaulted to be | can be disabled | +| row | header row | | ++========+=====================+==========================+ +| 1 | cell can contain | It can be aribrary block | +| | **markdown** | element: | +| | | | +| | | - following standard | +| | | markdown syntax | +| | | - like this | ++--------+---------------------+--------------------------+ +| 2 | Any markdown | $$E = mc^2$$ | +| | syntax, e.g. | | ++--------+---------------------+--------------------------+ + +: *Awesome* **Markdown** Table +~~~ + +becomes + +~~~markdown +``` {.table} +--- +alignment: DDD +caption: '*Awesome* **Markdown** Table' +header: true +markdown: true +table-width: 0.8055555555555556 +width: [0.125, 0.3055555555555556, 0.375] +--- +First row,defaulted to be header row,can be disabled +1,cell can contain **markdown**,"It can be aribrary block element: + +- following standard markdown syntax +- like this +" +2,"Any markdown syntax, e.g.",$$E = mc^2$$ +``` +~~~ +""" + +import csv +import io +import panflute +import yaml + +import sys +py2 = sys.version_info[0] == 2 + + +def ast2markdown(ast): + """ + A shorthand to convert panflute AST to Markdown + """ + return panflute.convert_text( + ast, + input_format='panflute', + output_format='markdown' + ) + + +def table2csv(elem, *__): + """ + find Table element and return a csv table in code-block with class "table" + """ + if isinstance(elem, panflute.Table): + # get options as a dictionary + options = {} + # options: caption: panflute ast to markdown + if elem.caption: + options['caption'] = ast2markdown(panflute.Para(*elem.caption)) + # options: alignment + align_dict = {"AlignLeft": 'L', + "AlignCenter": 'C', + "AlignRight": 'R', + "AlignDefault": 'D'} + parsed_alignment = [align_dict[i] for i in elem.alignment] + options['alignment'] = "".join(parsed_alignment) + # options: width + options['width'] = elem.width + # options: table-width from width + options['table-width'] = sum(options['width']) + # options: header: False if empty header row, else True + options['header'] = bool(panflute.stringify(elem.header)) + # options: markdown + options['markdown'] = True + + # option in YAML + yaml_metadata = yaml.safe_dump(options) + + # table in panflute AST + table_body = elem.content + if options['header']: + table_body.insert(0, elem.header) + # table in list + table_list = [[ast2markdown(cell.content) + for cell in row.content] + for row in table_body] + # table in CSV + io_universal = io.BytesIO if py2 else io.StringIO + with io_universal() as file: + writer = csv.writer(file) + writer.writerows(table_list) + csv_table = file.getvalue() + code_block = "{delimiter}{yaml}{delimiter}{csv}".format( + yaml=yaml_metadata, csv=csv_table, delimiter='---\n') + return panflute.CodeBlock(code_block, classes=["table"]) + return None + + +def main(_=None): + """ + Any native pandoc tables will be converted into the CSV table format used by pantable: + + - in code-block with class table + - metadata in YAML + - table in CSV + """ + panflute.run_filter(table2csv) + +if __name__ == '__main__': + main() diff --git a/pantable/version.py b/pantable/version.py new file mode 100644 index 00000000..06cc88be --- /dev/null +++ b/pantable/version.py @@ -0,0 +1 @@ +__version__ = '0.10.5' diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 00000000..688559ed --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +panflute diff --git a/setup.cfg b/setup.cfg new file mode 100755 index 00000000..5aef279b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.rst diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..ce9583fb --- /dev/null +++ b/setup.py @@ -0,0 +1,126 @@ +"""CSV Tables in Markdown: Pandoc Filter for CSV Tables + +See: +https://github.com/ickc/pantable +""" + +# Always prefer setuptools over distutils +from setuptools import setup, find_packages +# To use a consistent encoding +from codecs import open +from os import path + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with open(path.join(here, 'README.rst'), encoding='utf-8') as f: + long_description = f.read() + +# Import version number +version = {} +with open("pantable/version.py") as f: + exec(f.read(), version) +version = version['__version__'] + +setup( + name='pantable', + + # Versions should comply with PEP440. For a discussion on single-sourcing + # the version across setup.py and the project code, see + # https://packaging.python.org/en/latest/single_source_version.html + version=version, + + description='CSV Tables in Markdown: Pandoc Filter for CSV Tables', + long_description=long_description, + + # The project's main homepage. + url='https://github.com/ickc/pantable', + + # Author details + author='Kolen Cheung', + author_email='christian.kolen@gmail.com', + + # Choose your license + license='GPLv3', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + 'Development Status :: 4 - Beta', + + # Indicate who your project is intended for + 'Environment :: Console', + 'Intended Audience :: End Users/Desktop', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Build Tools', + 'Topic :: Text Processing :: Filters', + + # Pick your license as you wish (should match "license" above) + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + + # Specify the Python versions you support here. In particular, ensure + # that you indicate whether you support Python 2, Python 3 or both. + # https://pypi.python.org/pypi?%3Aaction=list_classifiers + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy' + ], + + # What does your project relate to? + keywords='pandoc pandocfilters panflute markdown latex', + + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + packages=find_packages(exclude=['contrib', 'docs', 'tests']), + + # Alternatively, if you want to distribute just a my_module.py, uncomment + # this: + # py_modules=["my_module"], + + # List run-time dependencies here. These will be installed by pip when + # your project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/requirements.html + install_requires=['panflute>=1.8.2'], + + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, + # for example: + # $ pip install -e .[dev,test] + extras_require={ + 'dev': ['check-manifest'], + 'test': ['shutilwhich', 'pep8', 'pylint', 'pytest', 'pytest-cov', 'coverage', 'coveralls', 'future'], + }, + + # If there are data files included in your packages that need to be + # installed, specify them here. If using Python 2.6 or less, then these + # have to be included in MANIFEST.in as well. + # package_data={ + # 'sample': ['package_data.dat'], + # }, + + # Although 'package_data' is the preferred approach, in some case you may + # need to place data files outside of your packages. See: + # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa + # In this case, 'data_file' will be installed into '/my_data' + # data_files=[('my_data', ['data/data_file'])], + + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. + entry_points={ + 'console_scripts': [ + 'pantable = pantable.pantable:main', + 'pantable2csv = pantable.pantable2csv:main' + ], + }, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/test/csv-tables.csv b/tests/csv_tables.csv similarity index 100% rename from test/csv-tables.csv rename to tests/csv_tables.csv diff --git a/tests/pantable/__init__.py b/tests/pantable/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/tests/pantable/context.py b/tests/pantable/context.py new file mode 100755 index 00000000..f15fea8b --- /dev/null +++ b/tests/pantable/context.py @@ -0,0 +1,5 @@ +import os +import sys +sys.path.insert(0, os.path.abspath('..' + os.sep + '..')) + +from pantable.pantable import * diff --git a/tests/pantable/test_auto_width.py b/tests/pantable/test_auto_width.py new file mode 100755 index 00000000..e5755f23 --- /dev/null +++ b/tests/pantable/test_auto_width.py @@ -0,0 +1,13 @@ +""" +""" +from .context import auto_width + + +def test_auto_width(): + raw_table_list = [ + ['asdfdfdfguhfdhghfdgkla', '334\n2', '**la**', '4'], + ['5', '6', '7', '8'] + ] + assert auto_width(1.2, 4, raw_table_list) == [25 / 44 * 1.2, + 6 / 44 * 1.2, 9 / 44 * 1.2, 4 / 44 * 1.2] + return diff --git a/tests/pantable/test_convert2table.py b/tests/pantable/test_convert2table.py new file mode 100755 index 00000000..186cfdf7 --- /dev/null +++ b/tests/pantable/test_convert2table.py @@ -0,0 +1,36 @@ +""" +`header` and `markdown` is checked by `test_to_bool` instead +""" +from .context import convert2table +from panflute import * + + +def test_convert2table(): + # normal table + data = r''' +1,2 +3,4 +''' + table_converted = convert2table({'width': [0, 0]}, data) + table_referenced = Table(TableRow(TableCell(Plain(Str('1'))), TableCell(Plain(Str('2')))), TableRow(TableCell( + Plain(Str('3'))), TableCell(Plain(Str('4')))), alignment=['AlignDefault', 'AlignDefault'], width=[0.0, 0.0]) + assert repr(table_converted) == repr(table_referenced) + # empty header_row + table_converted = convert2table({'header': False}, data) + table_referenced = Table(TableRow(TableCell(Plain(Str(''))), TableCell(Plain(Str('')))), TableRow(TableCell(Plain(Str('1'))), TableCell(Plain( + Str('2')))), TableRow(TableCell(Plain(Str('3'))), TableCell(Plain(Str('4')))), alignment=['AlignDefault', 'AlignDefault'], width=[0.5, 0.5]) + assert repr(table_converted) == repr(table_referenced) + # empty table + data = ',' + table = convert2table({}, data) + assert table == [] + # 1 row table + data = '1,2' + table_converted = convert2table({}, data) + table_referenced = Table(TableRow(TableCell(Plain(Str('1'))), TableCell( + Plain(Str('2')))), alignment=['AlignDefault', 'AlignDefault'], width=[0.5, 0.5]) + assert repr(table_converted) == repr(table_referenced) + # empty data + table = convert2table({}, '') + assert table == [] + return diff --git a/tests/pantable/test_get_table_width.py b/tests/pantable/test_get_table_width.py new file mode 100755 index 00000000..19ed77ad --- /dev/null +++ b/tests/pantable/test_get_table_width.py @@ -0,0 +1,17 @@ +""" +""" +from .context import get_table_width + + +def test_get_table_width(): + # check table-width + # init + options = {} + assert get_table_width(options) == 1.0 + # negative table-width + options['table-width'] = -1 + assert get_table_width(options) == 1.0 + # invalid table-width + options['table-width'] = "happy" + assert get_table_width(options) == 1.0 + return diff --git a/tests/pantable/test_get_width.py b/tests/pantable/test_get_width.py new file mode 100755 index 00000000..5ef75e5b --- /dev/null +++ b/tests/pantable/test_get_width.py @@ -0,0 +1,29 @@ +""" +""" +from .context import get_width + + +def test_get_width(): + # check width + # init + options = {} + assert get_width(options, 2) is None + # negative width + options['width'] = [0.1, -0.2] + assert get_width(options, 2) is None + # invalid width + options['width'] = "happy" + assert get_width(options, 1) is None + # invalid width 2 + options['width'] = ["happy", "birthday"] + assert get_width(options, 2) is None + # fractional + options['width'] = ["1/2", "1/10"] + assert get_width(options, 2) == [0.5, 0.1] + # width too short + options['width'] = [0.1, 0.2, 0.3] + assert get_width(options, 4) is None + # width too long + options['width'] = [0.1, 0.2, 0.3, 0.4, 0.5] + assert get_width(options, 4) is None + return diff --git a/tests/pantable/test_parse_alignment.py b/tests/pantable/test_parse_alignment.py new file mode 100755 index 00000000..d45e6600 --- /dev/null +++ b/tests/pantable/test_parse_alignment.py @@ -0,0 +1,27 @@ +""" +""" +from .context import parse_alignment + + +def test_parse_alignment(): + # init + options = {} + # check alignment + assert parse_alignment('LRC', 4) == [ + 'AlignLeft', + 'AlignRight', + 'AlignCenter', + 'AlignDefault' + ] + # check alignment too long + assert parse_alignment('LRCDLRCDLRCDLRCDLRCDLRCD', 4) == [ + 'AlignLeft', + 'AlignRight', + 'AlignCenter', + 'AlignDefault' + ] + # check invalid + assert parse_alignment('abcd', 4) is None + # check wrong type + assert parse_alignment(1, 1) is None + return diff --git a/tests/pantable/test_parse_table_list.py b/tests/pantable/test_parse_table_list.py new file mode 100755 index 00000000..d16f04d9 --- /dev/null +++ b/tests/pantable/test_parse_table_list.py @@ -0,0 +1,35 @@ +""" + +""" +from .context import parse_table_list +from panflute import * + + +def test_parse_table_list(): + markdown = False + raw_table_list = [['1', '2'], ['3', '4']] + table_list_converted = parse_table_list(markdown, raw_table_list) + table_list_referenced = [TableRow(TableCell(Plain(Str('1'))), TableCell(Plain( + Str('2')))), TableRow(TableCell(Plain(Str('3'))), TableCell(Plain(Str('4'))))] + assert repr(table_list_converted) == repr(table_list_referenced) + markdown = True + raw_table_list = [['**markdown**', '~~like this~~'], + ['$E=mc^2$', '`great`']] + table_list_converted = parse_table_list(markdown, raw_table_list) + table_list_referenced = [TableRow(TableCell(Para(Strong(Str('markdown')))), TableCell(Para(Strikeout(Str('like'), Space, Str( + 'this'))))), TableRow(TableCell(Para(Math('E=mc^2', format='InlineMath'))), TableCell(Para(Code('great'))))] + assert repr(table_list_converted) == repr(table_list_referenced) + # test irregular table + markdown = True + raw_table_list = [['1', '', '', '', '', ''], + ['2', '3', '4', '5', '6', '7']] + table_list_converted = parse_table_list(markdown, raw_table_list) + table_list_referenced = [TableRow(TableCell(Para(Str('1'))), TableCell(), TableCell(), TableCell(), TableCell(), TableCell()), TableRow(TableCell( + Para(Str('2'))), TableCell(Para(Str('3'))), TableCell(Para(Str('4'))), TableCell(Para(Str('5'))), TableCell(Para(Str('6'))), TableCell(Para(Str('7'))))] + assert repr(table_list_converted) == repr(table_list_referenced) + markdown = False + table_list_converted = parse_table_list(markdown, raw_table_list) + table_list_referenced = [TableRow(TableCell(Plain(Str('1'))), TableCell(Plain(Str(''))), TableCell(Plain(Str(''))), TableCell(Plain(Str(''))), TableCell(Plain(Str(''))), TableCell(Plain( + Str('')))), TableRow(TableCell(Plain(Str('2'))), TableCell(Plain(Str('3'))), TableCell(Plain(Str('4'))), TableCell(Plain(Str('5'))), TableCell(Plain(Str('6'))), TableCell(Plain(Str('7'))))] + assert repr(table_list_converted) == repr(table_list_referenced) + return diff --git a/tests/pantable/test_read_data.py b/tests/pantable/test_read_data.py new file mode 100755 index 00000000..35f241d2 --- /dev/null +++ b/tests/pantable/test_read_data.py @@ -0,0 +1,46 @@ +""" +""" +from .context import read_data + + +def test_read_data(): + # check include + # invalid include: file doesn't exist + assert read_data('abc.xyz', '') is None + # invalid include: wrong type + assert read_data(True, '') is None + # valid include + assert read_data('tests/csv_tables.csv', '') is not None + # check type + data = r"""1,2 +3,4 +""" + assert read_data(None, data) == [ + ['1', '2'], + ['3', '4'] + ] + # check complex cells + data = r"""asdfdfdfguhfdhghfdgkla,"334 +2",**la**,4 +5,6,7,8""" + assert read_data(None, data) == [ + ['asdfdfdfguhfdhghfdgkla', '334\n2', '**la**', '4'], + ['5', '6', '7', '8'] + ] + # check include + assert read_data('tests/csv_tables.csv', + data) == [['**_Fruit_**', + '~~Price~~', + '_Number_', + '`Advantages`'], + ['*Bananas~1~*', + '$1.34', + '12~units~', + 'Benefits of eating bananas \n(**Note the appropriately\nrendered block markdown**): \n\n- _built-in wrapper_ \n- ~~**bright color**~~\n\n'], + ['*Oranges~2~*', + '$2.10', + '5^10^~units~', + 'Benefits of eating oranges:\n\n- **cures** scurvy\n- `tasty`']] + # check empty table + assert read_data(None, '') == [] + return diff --git a/tests/pantable/test_regularize_table_list.py b/tests/pantable/test_regularize_table_list.py new file mode 100755 index 00000000..48271098 --- /dev/null +++ b/tests/pantable/test_regularize_table_list.py @@ -0,0 +1,15 @@ +""" + +""" +from .context import regularize_table_list + + +def test_regularize_table_list(): + raw_table_list = [['1'], ['2', '3', '4', '5', '6', '7']] + table_list, number_of_columns = regularize_table_list(raw_table_list) + assert table_list == [ + ['1', '', '', '', '', ''], + ['2', '3', '4', '5', '6', '7'] + ] + assert number_of_columns == 6 + return diff --git a/tests/pantable2csv/__init__.py b/tests/pantable2csv/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/tests/pantable2csv/context.py b/tests/pantable2csv/context.py new file mode 100755 index 00000000..2343e045 --- /dev/null +++ b/tests/pantable2csv/context.py @@ -0,0 +1,5 @@ +import os +import sys +sys.path.insert(0, os.path.abspath('..' + os.sep + '..')) + +from pantable.pantable2csv import * diff --git a/tests/pantable2csv/test_table2csv.py b/tests/pantable2csv/test_table2csv.py new file mode 100755 index 00000000..7d38a38a --- /dev/null +++ b/tests/pantable2csv/test_table2csv.py @@ -0,0 +1,28 @@ +""" +test panflute ast to markdown conversion +""" +from .context import table2csv +from panflute import * + + +def test_table2csv(): + # get_table_body + markdown = """| 1 | 2 | 3 | 4 | +|:--|--:|:-:|---| +| 1 | 2 | 3 | 4 | + +: *abcd* +""" + Panflute = convert_text(markdown) + code_block_converted = table2csv(*Panflute) + code_block_referenced = CodeBlock('''--- +alignment: LRCD +caption: '*abcd*' +header: true +markdown: true +table-width: 0 +width: [0, 0, 0, 0] +--- +1,2,3,4\r\n1,2,3,4\r\n''', classes=['table']) + assert repr(code_block_converted) == repr(code_block_referenced) + return diff --git a/test/csv-tables.native b/tests/reference_pantable.native similarity index 57% rename from test/csv-tables.native rename to tests/reference_pantable.native index 6c09cb5e..e78d79ba 100644 --- a/test/csv-tables.native +++ b/tests/reference_pantable.native @@ -1,67 +1,85 @@ [Header 1 ("comparison",[],[]) [Str "Comparison"] -,Table [] [AlignDefault,AlignDefault,AlignDefault,AlignDefault,AlignDefault] [8.666666666666667e-2,0.3838095238095238,0.24761904761904763,0.22285714285714286,0.35904761904761906] - [[] - ,[Para [Str "pandoc-csv2table"]] - ,[Para [Str "pandoc-placetable"]] - ,[Para [Str "panflute",Space,Str "example"]] - ,[Para [Str "my",Space,Str "proposal"]]] - [[[Para [Str "type"]] - ,[Para [Str "type=simple|multiline|grid|pipe"]] - ,[] - ,[] - ,[]] - ,[[Para [Str "header"]] - ,[Para [Str "header=yes|no"]] - ,[Para [Str "header=yes|no"]] - ,[Para [Str "header:",Space,Str "True|False"]] - ,[Para [Str "header:",Space,Str "True|False"]]] - ,[[Para [Str "caption"]] - ,[Para [Str "caption"]] - ,[Para [Str "caption"]] - ,[Para [Str "title"]] - ,[Para [Str "caption"]]] - ,[[Para [Str "source"]] - ,[Para [Str "source"]] - ,[Para [Str "file"]] - ,[Para [Str "source"]] - ,[Para [Str "include"]]] - ,[[Para [Str "aligns"]] - ,[Para [Str "aligns=LRCD"]] - ,[Para [Str "aligns=LRCD"]] - ,[] - ,[Para [Str "alignment:",Space,Str "LRCD"]]] - ,[[Para [Str "width"]] - ,[] - ,[Para [Str "widths=\"0.5",Space,Str "0.2",Space,Str "0.3\""]] - ,[] - ,[Para [Str "column-width:",Space,Str "[0.5,",Space,Str "0.2,",Space,Str "0.3]"]]] - ,[[] - ,[] - ,[Para [Str "inlinemarkdown"]] - ,[] - ,[Para [Str "markdown:",Space,Str "True|False"]]] - ,[[] - ,[] - ,[Para [Str "delimiter"]] - ,[] - ,[]] - ,[[] - ,[] - ,[Para [Str "quotechar"]] - ,[] - ,[]] - ,[[] - ,[] - ,[Para [Str "id",Space,Str "(wrapped",Space,Str "by",Space,Str "div)"]] - ,[] - ,[]]] +,Table [] [AlignDefault,AlignDefault,AlignDefault,AlignDefault,AlignDefault] [0.10833333333333334,0.36833333333333335,0.2491666666666667,0.22749999999999998,0.3466666666666667] + [[Plain [Str ""]] + ,[Plain [Str "pandoc-csv2table"]] + ,[Plain [Str "pandoc-placetable"]] + ,[Plain [Str "panflute example"]] + ,[Plain [Str "my proposal"]]] + [[[Plain [Str "type"]] + ,[Plain [Str "type=simple|multiline|grid|pipe"]] + ,[Plain [Str ""]] + ,[Plain [Str ""]] + ,[Plain [Str ""]]] + ,[[Plain [Str "header"]] + ,[Plain [Str "header=yes|no"]] + ,[Plain [Str "header=yes|no"]] + ,[Plain [Str "header: True|False"]] + ,[Plain [Str "header: True|False"]]] + ,[[Plain [Str "caption"]] + ,[Plain [Str "caption"]] + ,[Plain [Str "caption"]] + ,[Plain [Str "title"]] + ,[Plain [Str "caption"]]] + ,[[Plain [Str "source"]] + ,[Plain [Str "source"]] + ,[Plain [Str "file"]] + ,[Plain [Str "source"]] + ,[Plain [Str "include"]]] + ,[[Plain [Str "aligns"]] + ,[Plain [Str "aligns=LRCD"]] + ,[Plain [Str "aligns=LRCD"]] + ,[Plain [Str ""]] + ,[Plain [Str "alignment: LRCD"]]] + ,[[Plain [Str "width"]] + ,[Plain [Str ""]] + ,[Plain [Str "widths=\"0.5 0.2 0.3\""]] + ,[Plain [Str ""]] + ,[Plain [Str "column-width: [0.5, 0.2, 0.3]"]]] + ,[[Plain [Str ""]] + ,[Plain [Str ""]] + ,[Plain [Str "inlinemarkdown"]] + ,[Plain [Str ""]] + ,[Plain [Str "markdown: True|False"]]] + ,[[Plain [Str ""]] + ,[Plain [Str ""]] + ,[Plain [Str "delimiter"]] + ,[Plain [Str ""]] + ,[Plain [Str ""]]] + ,[[Plain [Str ""]] + ,[Plain [Str ""]] + ,[Plain [Str "quotechar"]] + ,[Plain [Str ""]] + ,[Plain [Str ""]]] + ,[[Plain [Str ""]] + ,[Plain [Str ""]] + ,[Plain [Str "id (wrapped by div)"]] + ,[Plain [Str ""]] + ,[Plain [Str ""]]]] ,Header 1 ("simple-test",[],[]) [Str "Simple",Space,Str "Test"] -,Table [] [AlignDefault,AlignDefault,AlignDefault,AlignDefault] [0.2,0.15,0.2,0.45] - [[Para [Strong [Emph [Str "Fruit"]]]] - ,[Para [Strikeout [Str "Price"]]] - ,[Para [Emph [Str "Number"]]] - ,[Para [Code ("",[],[]) "Advantages"]]] - [[[Para [Emph [Str "Bananas",Subscript [Str "1"]]]] +,Table [] [AlignDefault,AlignDefault,AlignDefault,AlignDefault] [0.20833333333333334,0.16666666666666666,0.20833333333333334,0.4166666666666667] + [[Plain [Str "**_Fruit_**"]] + ,[Plain [Str "~~Price~~"]] + ,[Plain [Str "_Number_"]] + ,[Plain [Str "`Advantages`"]]] + [[[Plain [Str "*Bananas~1~*"]] + ,[Plain [Str "$1.34"]] + ,[Plain [Str "12~units~"]] + ,[Plain [Str "Benefits of eating bananas\n(**Note the appropriately\nrendered block markdown**):\n\n- _built-in wrapper_\n- ~~**bright color**~~\n\n"]]] + ,[[Plain [Str "*Oranges~2~*"]] + ,[Plain [Str "$2.10"]] + ,[Plain [Str "5^10^~units~"]] + ,[Plain [Str "Benefits of eating oranges:\n\n- **cures** scurvy\n- `tasty`"]]]] +,Header 1 ("full-test",[],[]) [Str "Full",Space,Str "Test"] +,Table [Emph [Str "Great"],Space,Str "Title"] [AlignLeft,AlignRight,AlignCenter,AlignDefault] [0.1,0.2,0.3,0.4] + [[] + ,[] + ,[] + ,[]] + [[[Para [Strong [Emph [Str "Fruit"]]]] + ,[Para [Strikeout [Str "Price"]]] + ,[Para [Emph [Str "Number"]]] + ,[Para [Code ("",[],[]) "Advantages"]]] + ,[[Para [Emph [Str "Bananas",Subscript [Str "1"]]]] ,[Para [Str "$1.34"]] ,[Para [Str "12",Subscript [Str "units"]]] ,[Para [Str "Benefits",Space,Str "of",Space,Str "eating",Space,Str "bananas",SoftBreak,Str "(",Strong [Str "Note",Space,Str "the",Space,Str "appropriately",SoftBreak,Str "rendered",Space,Str "block",Space,Str "markdown"],Str "):"] @@ -75,14 +93,13 @@ ,BulletList [[Plain [Strong [Str "cures"],Space,Str "scurvy"]] ,[Plain [Code ("",[],[]) "tasty"]]]]]] -,Header 1 ("full-test",[],[]) [Str "Full",Space,Str "Test"] -,Table [Emph [Str "Great"],Space,Str "Title"] [AlignLeft,AlignRight,AlignCenter,AlignDefault] [0.1,0.2,0.3,0.4] - [] - [[[Para [Strong [Emph [Str "Fruit"]]]] - ,[Para [Strikeout [Str "Price"]]] - ,[Para [Emph [Str "Number"]]] - ,[Para [Code ("",[],[]) "Advantages"]]] - ,[[Para [Emph [Str "Bananas",Subscript [Str "1"]]]] +,Header 1 ("testing-wrong-type",[],[]) [Str "Testing",Space,Str "Wrong",Space,Str "Type"] +,Table [Str "0.1"] [AlignDefault,AlignDefault,AlignDefault,AlignDefault] [0.20833333333333334,0.16666666666666666,0.20833333333333334,0.4166666666666667] + [[Para [Strong [Emph [Str "Fruit"]]]] + ,[Para [Strikeout [Str "Price"]]] + ,[Para [Emph [Str "Number"]]] + ,[Para [Code ("",[],[]) "Advantages"]]] + [[[Para [Emph [Str "Bananas",Subscript [Str "1"]]]] ,[Para [Str "$1.34"]] ,[Para [Str "12",Subscript [Str "units"]]] ,[Para [Str "Benefits",Space,Str "of",Space,Str "eating",Space,Str "bananas",SoftBreak,Str "(",Strong [Str "Note",Space,Str "the",Space,Str "appropriately",SoftBreak,Str "rendered",Space,Str "block",Space,Str "markdown"],Str "):"] @@ -96,28 +113,8 @@ ,BulletList [[Plain [Strong [Str "cures"],Space,Str "scurvy"]] ,[Plain [Code ("",[],[]) "tasty"]]]]]] -,Header 1 ("testing-wrong-type",[],[]) [Str "Testing",Space,Str "Wrong",Space,Str "Type"] -,Table [Str "0.1"] [AlignDefault,AlignDefault,AlignDefault,AlignDefault] [0.0,0.0,0.0,0.0] - [[Plain [Str "**_Fruit_**"]] - ,[Plain [Str "~~Price~~"]] - ,[Plain [Str "_Number_"]] - ,[Plain [Str "`Advantages`"]]] - [[[Plain [Str "*Bananas~1~*"]] - ,[Plain [Str "$1.34"]] - ,[Plain [Str "12~units~"]] - ,[Plain [Str "Benefits of eating bananas\n(**Note the appropriately\nrendered block markdown**):\n\n- _built-in wrapper_\n- ~~**bright color**~~\n"]]] - ,[[Plain [Str "*Oranges~2~*"]] - ,[Plain [Str "$2.10"]] - ,[Plain [Str "5^10^~units~"]] - ,[Plain [Str "Benefits of eating oranges:\n\n- **cures** scurvy\n- `tasty`"]]]] -,Header 1 ("testing-0-table-width",[],[]) [Str "Testing",Space,Str "0",Space,Str "Table",Space,Str "Width"] -,Table [] [AlignDefault,AlignDefault] [0.0,0.0] - [[] - ,[]] - [[[] - ,[]]] ,Header 1 ("include-external-csv",[],[]) [Str "Include",Space,Str "External",Space,Str "CSV"] -,Table [Emph [Str "Great"],Space,Str "Title"] [AlignDefault,AlignLeft,AlignDefault,AlignDefault] [0.1875,0.140625,0.1875,0.484375] +,Table [Emph [Str "Great"],Space,Str "Title"] [AlignDefault,AlignDefault,AlignDefault,AlignDefault] [0.19736842105263158,0.15789473684210525,0.19736842105263158,0.4473684210526316] [[Para [Strong [Emph [Str "Fruit"]]]] ,[Para [Strikeout [Str "Price"]]] ,[Para [Emph [Str "Number"]]] @@ -135,4 +132,30 @@ ,[Para [Str "Benefits",Space,Str "of",Space,Str "eating",Space,Str "oranges:"] ,BulletList [[Plain [Strong [Str "cures"],Space,Str "scurvy"]] - ,[Plain [Code ("",[],[]) "tasty"]]]]]]] + ,[Plain [Code ("",[],[]) "tasty"]]]]]] +,Header 1 ("include-external-csv-invalid-path",[],[]) [Str "Include",Space,Str "External",Space,Str "CSV",Space,Str "(invalid",Space,Str "path)"] +,CodeBlock ("",["table"],[]) "---\ncaption: \"*Great* Title\"\nheader: True\nmarkdown: True\nalignment: AlignLeft, AlignRight, AlignCenter, AlignDefault\ninclude: xyz/csv_tables.csv\n..." +,Header 1 ("empty-csv",[],[]) [Str "Empty",Space,Str "CSV"] +,Header 1 ("testing-0-table-width",[],[]) [Str "Testing",Space,Str "0",Space,Str "Table",Space,Str "Width"] +,Header 1 ("irregular-csv",[],[]) [Str "Irregular",Space,Str "CSV"] +,Table [] [AlignDefault,AlignDefault,AlignDefault,AlignDefault,AlignDefault,AlignDefault] [0.16666666666666666,0.16666666666666666,0.16666666666666666,0.16666666666666666,0.16666666666666666,0.16666666666666666] + [[Plain [Str "2"]] + ,[Plain [Str "3"]] + ,[Plain [Str "4"]] + ,[Plain [Str "5"]] + ,[Plain [Str "6"]] + ,[Plain [Str "7"]]] + [[[Plain [Str "1"]] + ,[Plain [Str ""]] + ,[Plain [Str ""]] + ,[Plain [Str ""]] + ,[Plain [Str ""]] + ,[Plain [Str ""]]]] +,Header 1 ("invalid-yaml",[],[]) [Str "Invalid",Space,Str "YAML"] +,CodeBlock ("",["table"],[]) "---\ncaption: *unquoted*\n...\n1,2\n3,4" +,Header 1 ("one-row-table",[],[]) [Str "One",Space,Str "Row",Space,Str "Table"] +,Table [] [AlignDefault,AlignDefault] [0.5,0.5] + [[] + ,[]] + [[[Plain [Str "1"]] + ,[Plain [Str "2"]]]]] diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100755 index 00000000..baff2bf3 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,8 @@ +shutilwhich +pep8 +pylint +pytest +pytest-cov +coverage +coveralls +future diff --git a/tests/tables.md b/tests/tables.md new file mode 100644 index 00000000..3fb43ec7 --- /dev/null +++ b/tests/tables.md @@ -0,0 +1,5 @@ +| 1 | 2 | 3 | 4 | +|:--|--:|:-:|---| +| 1 | 2 | 3 | 4 | + +: *abcd* diff --git a/tests/test_native.py b/tests/test_native.py new file mode 100755 index 00000000..e6014794 --- /dev/null +++ b/tests/test_native.py @@ -0,0 +1,25 @@ +""" +test pantable: + +- `reference_pantable.native`: the correct output +- `test_pantable.native`: from `make` + +test idempotency between pantable and pantable2csv: + +- `reference_idempotent.native`: the 1st AST write +- `test_idempotent.native`: after AST-CSV-AST cycle +""" + +import filecmp + + +def test_native(): + assert filecmp.cmp( + 'tests/reference_pantable.native', + 'tests/test_pantable.native' + ) + assert filecmp.cmp( + 'tests/reference_idempotent.native', + 'tests/test_idempotent.native' + ) + return diff --git a/test/csv-tables.md b/tests/test_pantable.md similarity index 75% rename from test/csv-tables.md rename to tests/test_pantable.md index af655b0a..288596a5 100644 --- a/test/csv-tables.md +++ b/tests/test_pantable.md @@ -1,5 +1,4 @@ -Comparison -========== +# Comparison ``` {.table} --- @@ -18,8 +17,7 @@ width,,"widths=""0.5 0.2 0.3""",,"column-width: [0.5, 0.2, 0.3]" ,,id (wrapped by div),, ``` -Simple Test -=========== +# Simple Test ``` {.table} **_Fruit_**,~~Price~~,_Number_,`Advantages` @@ -37,8 +35,7 @@ rendered block markdown**): - `tasty`" ``` -Full Test -========= +# Full Test ``` {.table} --- @@ -67,8 +64,7 @@ rendered block markdown**): - `tasty`" ``` -Testing Wrong Type -================== +# Testing Wrong Type ``` {.table} **_Fruit_**,~~Price~~,_Number_,`Advantages` @@ -82,7 +78,7 @@ rendered block markdown**): --- caption: 0.1 header: IDK -markdown: false +markdown: true ... " @@ -101,23 +97,67 @@ alignment: 0.1 --- ``` -Testing 0 Table Width -===================== + + +# Include External CSV ``` {.table} -, -, +--- +caption: "*Great* Title" +header: True +markdown: True +alignment: AlignLeft, AlignRight, AlignCenter, AlignDefault +include: tests/csv_tables.csv +... ``` -Include External CSV -==================== +# Include External CSV (invalid path) ``` {.table} --- caption: "*Great* Title" header: True +markdown: True alignment: AlignLeft, AlignRight, AlignCenter, AlignDefault -include: test/csv-tables.csv +include: xyz/csv_tables.csv ... ``` +# Empty CSV + +``` {.table} +--- +... +``` + +# Testing 0 Table Width + +``` {.table} +, +, +``` + +# Irregular CSV + +``` {.table} +--- +markdown: False +... +2,3,4,5,6,7 +1 +``` + +# Invalid YAML +``` {.table} +--- +caption: *unquoted* +... +1,2 +3,4 +``` + +# One Row Table + +``` {.table} +1,2 +```