From c66cdab5091c8b83eb2970b495539310b7bdca4c Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Thu, 25 Jul 2019 02:12:36 +0100 Subject: [PATCH 1/7] Restructure documentation index and add Developer Guide Split up into separate indexes, so the ToC looks cleaner. also: - add documentation on pandoc and sphinx notebook metadata options - added cell metadata schema --- docs/source/dev_guide.rst | 76 +++++++ docs/source/index.rst | 33 ++- docs/source/latex_ipypublish_all.rst | 2 + docs/source/markdown_cells.Rmd | 16 +- docs/source/metadata_cell_schema.rst | 7 + docs/source/metadata_tags.rst | 132 ++++++++++-- docs/source/outline_schema.rst | 2 + docs/source/segment_example.rst | 2 + docs/source/sphinx_ext_notebook.rst | 4 + docs/source/sphinx_extensions.rst | 10 +- ipypublish/schema/cell_metadata.schema.json | 222 ++++++++++++++++++++ 11 files changed, 462 insertions(+), 44 deletions(-) create mode 100644 docs/source/dev_guide.rst create mode 100644 docs/source/metadata_cell_schema.rst create mode 100644 ipypublish/schema/cell_metadata.schema.json diff --git a/docs/source/dev_guide.rst b/docs/source/dev_guide.rst new file mode 100644 index 0000000..f007ce2 --- /dev/null +++ b/docs/source/dev_guide.rst @@ -0,0 +1,76 @@ +Developer Guide ++++++++++++++++ + +Installation +~~~~~~~~~~~~ + +To install the development version:: + + >> git clone https://github.com/chrisjsewell/ipypublish . + >> cd ipypublish + +and either use the pre-written Conda development environment (recommended):: + + >> conda env create -n ipub_testenv -f conda_dev_env.yaml python=3.6 + >> conda activate ipub_testenv + >> pip install --no-deps -e . + +or install all *via* pip:: + + >> pip install -e .[tests] + +Testing +~~~~~~~ + +|Build Status| |Coverage Status| + +The following will discover and run all unit test: + +.. code:: shell + + >> cd ipypublish + >> pytest -v + +Coding Style Requirements +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The code style is tested using `flake8 `__, +with the configuration set in ``.flake8``, and code should be formatted +with `yapf `__ (configuration set in +``.style.yapf``). + +Installing with ``ipypublish[tests]`` makes the +`pre-commit `__ package available, which will +ensure these tests are passed by reformatting the code and testing for +lint errors before submitting a commit. It can be setup by: + +.. code:: shell + + >> cd ipypublish + >> pre-commit install + +Optionally you can run ``yapf`` and ``flake8`` separately: + +.. code:: shell + + >> yapf -i path/to/file # format file in-place + >> flake8 + +Editors like VS Code also have automatic code reformat utilities, which +can check and adhere to this standard. + +Documentation +~~~~~~~~~~~~~ + +The documentation can be created locally by: + +.. code:: shell + + >> cd ipypublish/docs + >> make clean + >> make # or make debug + +.. |Build Status| image:: https://travis-ci.org/chrisjsewell/ipypublish.svg?branch=master + :target: https://travis-ci.org/chrisjsewell/ipypublish +.. |Coverage Status| image:: https://coveralls.io/repos/github/chrisjsewell/ipypublish/badge.svg?branch=master + :target: https://coveralls.io/github/chrisjsewell/ipypublish?branch=master diff --git a/docs/source/index.rst b/docs/source/index.rst index b3f44e2..ed09c53 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -54,9 +54,10 @@ Badges |Build Status| |Coverage Status| |PyPI| |Conda| .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :numbered: - :caption: Contents: + :caption: Using IPyPublish + :hidden: getting_started code_cells @@ -64,23 +65,37 @@ Badges nb_conversion metadata_tags custom_export_config + +.. toctree:: + :maxdepth: 1 + :caption: Sphinx Extensions + :hidden: + sphinx_extensions + sphinx_ext_notebook + sphinx_ext_bibgloss + +.. toctree:: + :maxdepth: 1 + :caption: Additional Information + :hidden: + + releases examples applications + dev_guide additional_tools acknowledgements package_api - releases .. toctree:: - :maxdepth: 2 - :caption: Included Files: + :maxdepth: 1 + :caption: Validation Schemas + :hidden: - latex_ipypublish_all - export_schema - outline_schema - segment_example metadata_doc_schema + metadata_cell_schema + export_schema .. todo:: how to use with vs-code diff --git a/docs/source/latex_ipypublish_all.rst b/docs/source/latex_ipypublish_all.rst index f9812e9..3c31ca5 100644 --- a/docs/source/latex_ipypublish_all.rst +++ b/docs/source/latex_ipypublish_all.rst @@ -1,3 +1,5 @@ +:orphan: + .. _latex_ipypublish_all: Example Export Configuration diff --git a/docs/source/markdown_cells.Rmd b/docs/source/markdown_cells.Rmd index 3e7ccb0..d41dac2 100644 --- a/docs/source/markdown_cells.Rmd +++ b/docs/source/markdown_cells.Rmd @@ -10,7 +10,7 @@ jupyter: author: Chris Sewell email: chrisj\_sewell@hotmail.com logo: _static/logo_example.png - title: Example of Converted Jupyter Notebook + title: Example of Converted Jupyter Notebook subtitle: Formatting Markdown Cells tagline: Converted using IPyPublish ('latex\_ipypublish\_all.exec'). supervisors: @@ -20,7 +20,7 @@ jupyter: - Institution1 - Institution2 pandoc: - convert_raw: true + convert_raw: true hide_raw: false at_notation: true use_numref: true @@ -61,7 +61,7 @@ This filter extends the common markdown syntax to: - Correctly translate pieces of documentation written in other formats (such as using LaTeX commands like `\cite` or RST roles like `:cite:`) - Handle labelling and referencing of figures, tables and equations, and - add additional formatting options. + add additional formatting options. `ipubpandoc` is detached from the rest of the notebook conversion process, and so can be used as a standalone process on any markdown content: @@ -74,6 +74,8 @@ $ pandoc -f markdown -t html --filter ipubpandoc path/to/file.md [The PDF representation of this notebook](_static/markdown_cells.pdf) + :ref:`pandoc_doc_metadata`, for the notebook document level metadata options. + .. _jupytext_rmarkdown: ## Converting Notebooks to RMarkdown @@ -309,7 +311,7 @@ Glossary Terms: %@gtkey2, &@akey2, &@symbol2 .. note:: - Citations in sphinx are provided by the excellent + Citations in sphinx are provided by the excellent [sphinxcontrib-bibtex extension](https://sphinxcontrib-bibtex.readthedocs.io), and glossary referencing is provided by the equally good :ref:`sphinx_ext_gls` @@ -384,10 +386,10 @@ data-latex="cref" data-rst="numref" data-a="1"> @label -xyz +xyz @label2 - + @label3 @@ -396,4 +398,4 @@ xyz ``` Formatter filters, then look for these parent spans, -to provide identifiers, classes and attributes. \ No newline at end of file +to provide identifiers, classes and attributes. diff --git a/docs/source/metadata_cell_schema.rst b/docs/source/metadata_cell_schema.rst new file mode 100644 index 0000000..c7efe33 --- /dev/null +++ b/docs/source/metadata_cell_schema.rst @@ -0,0 +1,7 @@ +.. _meta_celllevel_schema: + +Cell/Output Level Metadata Schema +--------------------------------- + +.. literalinclude:: ../../ipypublish/schema/cell_metadata.schema.json + :language: JSON diff --git a/docs/source/metadata_tags.rst b/docs/source/metadata_tags.rst index 9e4fba3..bf75004 100644 --- a/docs/source/metadata_tags.rst +++ b/docs/source/metadata_tags.rst @@ -196,9 +196,101 @@ See `Positioning_images_and_tables `__ for placement options. +.. _pandoc_doc_metadata: + +Pandoc Markdown Conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To control how the ipypandoc filters convert markdown cells, +the following options are available: + +.. code:: json + + { + "ipub": { + "sphinx": { + "apply_filters": true, + "convert_raw": true, + "hide_raw": false, + "at_notation": true, + "use_numref": true, + "reftag": "cite" + } + } + } + +.. seealso:: + + :ref:`markdown_cells` + +.. _sphinx_doc_metadata: + +Sphinx Output Control +~~~~~~~~~~~~~~~~~~~~~ + +.. seealso:: + + :ref:`sphinx_ext_notebook`, for documentation on the Sphinx extension. + +To suppress the output of a bibliography or glossary: + +.. code:: json + + { + "ipub": { + "sphinx": { + "no_bib": true, + "no_glossary": true + } + } + } + +To change the title text of the bibliography and glossary: + +.. code:: json + + { + "ipub": { + "sphinx": { + "bib_title": "My Title", + "glossary_title": "My Title" + } + } + } + + +To control the addition of toggle buttons for code/output cells: + +.. code:: json + + { + "ipub": { + "sphinx": { + "toggle_input": true, + "toggle_output": true, + "toggle_input_all": true, + "toggle_output_all": true + } + } + } + +To denote the notebook as an orphan (i.e. not required in an index): + +.. code:: json + + { + "ipub": { + "sphinx": { + "orphan": true + } + } + } + Cell/Output Level ----------------- +The full schema can be viewed at :ref:`meta_celllevel_schema`. + Ignore ~~~~~~ @@ -232,7 +324,7 @@ To **output a code block**: { "ipub": { "code": { - "format" : {}, + "format" : {}, "asfloat": true, "caption": "", "label": "code:example_sym", @@ -262,15 +354,15 @@ command): { "ipub": { "text": { - "format": { + "format": { "basicstyle": "\\small" - }, - "asfloat": true, - "caption": "", - "label": "code:example_sym", - "widefigure": false, - "placement": "H", - "use_ansi": false + }, + "asfloat": true, + "caption": "", + "label": "code:example_sym", + "widefigure": false, + "placement": "H", + "use_ansi": false } } } @@ -302,7 +394,7 @@ metadata: "caption": "Figure caption.", "label": "fig:flabel", "placement": "H", - "height":0.4, + "height":0.4, "widefigure": false } } @@ -336,14 +428,14 @@ metadata: .. code:: json { - "ipub": { + "ipub": { "table": { "caption": "Table caption.", "label": "tbl:tlabel", "placement": "H", - "alternate": "gray!20" + "alternate": "gray!20" } - } + } } - ``caption`` and ``label`` are optional @@ -376,7 +468,7 @@ metadata: "ipub": { "equation": { "environment": "equation", - "label": "eqn:elabel" + "label": "eqn:elabel" } } } @@ -421,7 +513,7 @@ metadata tag: { "ipub": { - "caption": "fig:example_mpl" + "caption": "fig:example_mpl" } } @@ -439,11 +531,11 @@ label matching any stored caption name, for example: .. code:: json { - "ipub": { - "figure": { - "caption": "", - "label": "fig:example_mpl" - } + "ipub": { + "figure": { + "caption": "", + "label": "fig:example_mpl" + } } } diff --git a/docs/source/outline_schema.rst b/docs/source/outline_schema.rst index 7bc859c..e24ceef 100644 --- a/docs/source/outline_schema.rst +++ b/docs/source/outline_schema.rst @@ -1,3 +1,5 @@ +:orphan: + .. _outline_schema: Template Outline Example diff --git a/docs/source/segment_example.rst b/docs/source/segment_example.rst index 4e909ee..fe8f0cd 100644 --- a/docs/source/segment_example.rst +++ b/docs/source/segment_example.rst @@ -1,3 +1,5 @@ +:orphan: + .. _segment_config: Example of a Template Segment diff --git a/docs/source/sphinx_ext_notebook.rst b/docs/source/sphinx_ext_notebook.rst index e5062e5..4d1c2f0 100644 --- a/docs/source/sphinx_ext_notebook.rst +++ b/docs/source/sphinx_ext_notebook.rst @@ -81,6 +81,10 @@ or for sphinx<1.8: (including the ipypublish extensions), and call `sphinx-build `_. +.. seealso:: + + :ref:`sphinx_doc_metadata`, for the notebook document level metadata options. + Configuration ------------- diff --git a/docs/source/sphinx_extensions.rst b/docs/source/sphinx_extensions.rst index 202d40b..15429ea 100644 --- a/docs/source/sphinx_extensions.rst +++ b/docs/source/sphinx_extensions.rst @@ -1,7 +1,7 @@ .. _sphinx_extensions: -Sphinx Extensions -================= +Introduction +============ IPyPublish packages a number of `sphinx `_ extensions which are used to convert notebooks to (primarily) HTML. @@ -16,9 +16,3 @@ extensions which are used to convert notebooks to (primarily) HTML. This will convert the notebook to .rst, create a basic conf.py file (including the ipypublish extensions), and call `sphinx-build `_. - -.. toctree:: - :maxdepth: 2 - - sphinx_ext_notebook - sphinx_ext_bibgloss \ No newline at end of file diff --git a/ipypublish/schema/cell_metadata.schema.json b/ipypublish/schema/cell_metadata.schema.json new file mode 100644 index 0000000..2a5cff5 --- /dev/null +++ b/ipypublish/schema/cell_metadata.schema.json @@ -0,0 +1,222 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "description": "validation of the Jupyter Notebook cell/output level metadata for ipypublish", + "type": "object", + "properties": { + "ipub": { + "type": "object", + "additionalProperties": false, + "properties": { + "ignore": { + "description": "do not output this cell", + "type": "boolean", + "default": false + }, + "slideonly": { + "description": "only output this cell in slides", + "type": "boolean", + "default": false + }, + "slide": { + "type": [ + "string", + "boolean" + ], + "enum": [ + "new", + "notes" + ] + }, + "code": { + "description": "output a code block", + "type": [ + "boolean", + "object" + ], + "properties": { + "asfloat": { + "description": "whether the code is wrapped in a codecell (float) environment or is inline", + "type": "boolean", + "default": false + }, + "caption": { + "description": "the caption of the code cell", + "type": "string" + }, + "label": { + "description": "the label of the code cell, e.g. code:example_sym", + "type": "string" + }, + "widefigure": { + "description": "whether to expand the figure to the page width (placement arguments will then be ignored)", + "type": "boolean", + "default": false + }, + "placement": { + "description": "the placement of the code cell, e.g. 'H'", + "type": "string" + }, + "format": { + "description": "can contain any keywords related to the latex Listings package (such as syntax highlighting colors)", + "type": "object" + } + } + }, + "text": { + "description": "output a text block", + "type": [ + "boolean", + "object" + ], + "properties": { + "asfloat": { + "description": "whether the text is wrapped in a textcell (float) environment or is inline", + "type": "boolean", + "default": false + }, + "caption": { + "description": "the caption of the text cell", + "type": "string" + }, + "label": { + "description": "the label of the text cell, e.g. text:example_sym", + "type": "string" + }, + "widefigure": { + "description": "whether to expand the text to the page width (placement arguments will then be ignored)", + "type": "boolean", + "default": false + }, + "placement": { + "description": "the placement of the text cell, e.g. 'H'", + "type": "string" + }, + "format": { + "description": "can contain any keywords related to the latex Listings package (such as syntax highlighting colors)", + "type": "object" + }, + "use_ansi": { + "description": "if true then, instead of stripping ansi colors in latex output, they will be converted to latex, wrapped in % characters and the listings option escapechar=% set", + "type": "boolean", + "default": false + } + } + }, + "figure": { + "description": "output a figure", + "type": [ + "boolean", + "object" + ], + "properties": { + "caption": { + "description": "the caption of the figure", + "type": "string" + }, + "label": { + "description": "the label of the figure, e.g. fig:example_sym", + "type": "string" + }, + "widefigure": { + "description": "whether to expand the figure to the page width (placement arguments will then be ignored)", + "type": "boolean", + "default": false + }, + "placement": { + "description": "the placement of the figure, e.g. 'H'", + "type": "string" + }, + "width": { + "description": "the width of the figure, as a fraction of the page", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "height": { + "description": "the height of the figure, as a fraction of the page", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + } + } + }, + "table": { + "description": "output a table", + "type": [ + "boolean", + "object" + ], + "properties": { + "caption": { + "description": "the caption of the table", + "type": "string" + }, + "label": { + "description": "the label of the table, e.g. fig:example_sym", + "type": "string" + }, + "placement": { + "description": "the placement of the table, e.g. 'H'", + "type": "string" + }, + "alternate": { + "description": "using alternating colors for the table rows (see https://tex.stackexchange.com/a/5365/107738)", + "type": "string" + } + } + }, + "equations": { + "description": "output an equation", + "type": [ + "boolean", + "object" + ], + "properties": { + "label": { + "description": "the label of the figure, e.g. eqn:example_sym", + "type": "string" + }, + "environment": { + "description": "an environment from amsmath: https://www.sharelatex.com/learn/Aligning_equations_with_amsmath or breqn: https://ctan.org/pkg/breqn", + "type": [ + "string", + "null" + ] + } + } + }, + "embed_html": { + "description": "embed a html file (html only), use either filepath or url", + "type": "object", + "properties": { + "filepath": { + "type": "string" + }, + "url": { + "type": "string" + }, + "other_files": { + "description": " files required by the html file (e.g. javascript libraries). These files will be copied to the the same folder as the html", + "type": "array", + "items": { + "type": "string" + } + }, + "width": { + "description": "the width of the embedded object, as a fraction of the viewspace", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "height": { + "description": "the height of the embedded object, as a fraction of the viewspace", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + } + } + } + } + } + } +} From 00671aff87f5783101ded137dc012c8b4c63a01a Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 12 Aug 2019 17:42:52 +0100 Subject: [PATCH 2/7] Fix typo in documentation --- docs/source/sphinx_ext_notebook.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/sphinx_ext_notebook.rst b/docs/source/sphinx_ext_notebook.rst index 4d1c2f0..3294229 100644 --- a/docs/source/sphinx_ext_notebook.rst +++ b/docs/source/sphinx_ext_notebook.rst @@ -9,7 +9,7 @@ ipypublish.sphinx.notebook for converting notebooks with :py:class:`ipypublish.convert.main.IpyPubMain`. This website is built using it, -and a good example its use would be to look at the +so a good example of its use would be to look at the `ipypublish/docs/source/conf.py `_. This extension loads: From ca363e3f8ece8d4dd7594dde58f6b69f0e110b77 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 12 Aug 2019 17:48:06 +0100 Subject: [PATCH 3/7] fix documentation formatting --- docs/source/sphinx_ext_notebook.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/sphinx_ext_notebook.rst b/docs/source/sphinx_ext_notebook.rst index 3294229..687679f 100644 --- a/docs/source/sphinx_ext_notebook.rst +++ b/docs/source/sphinx_ext_notebook.rst @@ -148,6 +148,7 @@ Basic input :execution-count: 2 :caption: A caption for the code cell :name: ref_label + :no-output: print("hallo") From f1e08dd670cf18ee03afb8b5d6ae65fc277a6941 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 12 Aug 2019 18:10:47 +0100 Subject: [PATCH 4/7] fix compare_rst_files --- ipypublish/tests/conftest.py | 139 +++++++++++++++-------------------- 1 file changed, 61 insertions(+), 78 deletions(-) diff --git a/ipypublish/tests/conftest.py b/ipypublish/tests/conftest.py index 0f02198..de86e4f 100644 --- a/ipypublish/tests/conftest.py +++ b/ipypublish/tests/conftest.py @@ -58,15 +58,16 @@ def test_example(ipynb_app): @pytest.fixture(autouse=True) def dont_open_webbrowser(monkeypatch): + def nullfunc(*arg, **kwrgs): pass + monkeypatch.setattr('webbrowser.open', nullfunc) @pytest.fixture def external_export_plugin(): - return pathlib.Path(os.path.join(TEST_FILES_DIR, - 'example_new_plugin.json')) + return pathlib.Path(os.path.join(TEST_FILES_DIR, 'example_new_plugin.json')) @pytest.fixture @@ -86,9 +87,9 @@ def ipynb_params(request): # ##### process pytest.mark.ipynb if hasattr(request.node, 'iter_markers'): # pytest-3.6.0 or newer - markers = request.node.iter_markers("ipynb") + markers = request.node.iter_markers('ipynb') else: - markers = request.node.get_marker("ipynb") + markers = request.node.get_marker('ipynb') pargs = {} kwargs = {} @@ -101,8 +102,7 @@ def ipynb_params(request): args = [pargs[i] for i in sorted(pargs.keys())] - return namedtuple( - 'ipynb_params', 'args,kwargs')(args, kwargs) # type: ignore + return namedtuple('ipynb_params', 'args,kwargs')(args, kwargs) # type: ignore @pytest.fixture(scope='function') @@ -110,9 +110,7 @@ def ipynb_app(temp_folder, ipynb_params): args, kwargs = ipynb_params if len(args) <= 0: - raise ValueError( - 'a subfolder must be supplied as the first argument to ' - '@pytest.mark.ipynb') + raise ValueError('a subfolder must be supplied as the first argument to ' '@pytest.mark.ipynb') subfolder = args[0] # 'ipynb_with_glossary' input_file = kwargs.get('main_file', 'main.ipynb') @@ -122,10 +120,8 @@ def ipynb_app(temp_folder, ipynb_params): expected_folder = kwargs.get('expected', 'expected') use_temp = kwargs.get('out_to_temp', True) - source_folder_path = os.path.join( - test_files_dir, subfolder, source_folder) - expected_folder_path = os.path.join( - test_files_dir, subfolder, expected_folder) + source_folder_path = os.path.join(test_files_dir, subfolder, source_folder) + expected_folder_path = os.path.join(test_files_dir, subfolder, expected_folder) temp_source_path = os.path.join(temp_folder, source_folder) shutil.copytree(source_folder_path, temp_source_path) @@ -133,8 +129,7 @@ def ipynb_app(temp_folder, ipynb_params): if use_temp: converted_path = os.path.join(temp_folder, convert_folder) else: - converted_path = os.path.join( - test_files_dir, subfolder, convert_folder) + converted_path = os.path.join(test_files_dir, subfolder, convert_folder) yield IpyTestApp( temp_source_path, @@ -146,8 +141,7 @@ def ipynb_app(temp_folder, ipynb_params): class IpyTestApp(object): - def __init__(self, src_path, input_file, converted_path, - expected_folder_path): + def __init__(self, src_path, input_file, converted_path, expected_folder_path): self._src_folder_path = src_path self._converted_folder_path = converted_path self._expected_folder_path = expected_folder_path @@ -179,33 +173,28 @@ def pandoc_version(self): def run(self, ipub_config=None): if ipub_config is None: ipub_config = {} - ipub_config["outpath"] = str(self.converted_path) - app = IpyPubMain( - config={"IpyPubMain": ipub_config}) - self._output_data = app(self.input_file if self.input_file is not None - else self.source_path) + ipub_config['outpath'] = str(self.converted_path) + app = IpyPubMain(config={'IpyPubMain': ipub_config}) + self._output_data = app(self.input_file if self.input_file is not None else self.source_path) @property def output_data(self): if self._output_data is None: - raise ValueError( - "the app must be run first to retrieve output data") + raise ValueError('the app must be run first to retrieve output data') return copy.copy(self._output_data) @property def export_extension(self): if self._output_data is None: - raise ValueError( - "the app must be run first to retrieve export file extension") - exporter = self._output_data["exporter"] + raise ValueError('the app must be run first to retrieve export file extension') + exporter = self._output_data['exporter'] return exporter.file_extension @property def export_mimetype(self): if self._output_data is None: - raise ValueError( - "the app must be run first to retrieve export mimetype") - exporter = self._output_data["exporter"] + raise ValueError('the app must be run first to retrieve export mimetype') + exporter = self._output_data['exporter'] return exporter.output_mimetype def assert_converted_exists(self, file_name=None, extension=None): @@ -219,7 +208,7 @@ def assert_converted_exists(self, file_name=None, extension=None): converted_path = self.converted_path.joinpath(file_name + extension) if not self.converted_path.joinpath(file_name + extension).exists(): - raise AssertionError("could not find: {}".format(converted_path)) + raise AssertionError('could not find: {}'.format(converted_path)) def assert_converted_contains(self, regexes, encoding='utf8'): @@ -239,11 +228,9 @@ def assert_converted_contains(self, regexes, encoding='utf8'): for regex in regexes: if not re.search(regex, content): - raise AssertionError( - "content does not contain regex: {}".format(regex)) + raise AssertionError('content does not contain regex: {}'.format(regex)) - def assert_converted_equals_expected(self, expected_file_name, - encoding='utf8'): + def assert_converted_equals_expected(self, expected_file_name, encoding='utf8'): if self.input_file is None: file_name = self.source_path.name @@ -252,24 +239,19 @@ def assert_converted_equals_expected(self, expected_file_name, extension = self.export_extension converted_path = self.converted_path.joinpath(file_name + extension) - expected_path = self.expected_path.joinpath( - expected_file_name + extension) + expected_path = self.expected_path.joinpath(expected_file_name + extension) mime_type = self.export_mimetype if mime_type == 'text/latex': - compare_tex_files( - converted_path, expected_path, encoding=encoding) + compare_tex_files(converted_path, expected_path, encoding=encoding) elif mime_type == 'text/html': - compare_html_files( - converted_path, expected_path, encoding=encoding) + compare_html_files(converted_path, expected_path, encoding=encoding) elif mime_type == 'text/restructuredtext': - compare_rst_files( - converted_path, expected_path, encoding=encoding) + compare_rst_files(converted_path, expected_path, encoding=encoding) else: # TODO add comparison for nb (applicatio/json) # and python (application/x-python) - message = ("no comparison function exists for " - "mimetype: {}".format(mime_type)) + message = ('no comparison function exists for ' 'mimetype: {}'.format(mime_type)) # raise ValueError(message) logger.warn(message) @@ -285,7 +267,10 @@ def compare_rst_files(testpath, outpath, encoding='utf8'): content = fobj.read() # python 3.5 used .jpg instead of .jpeg - content = content.replace(".jpg", ".jpeg") + content = content.replace('.jpg', '.jpeg') + + # a recent dependency change is inserting new lines at the top of the file + content = content.lstrip() output.append(content) @@ -293,9 +278,9 @@ def compare_rst_files(testpath, outpath, encoding='utf8'): # only report differences if out_content != test_content: - raise AssertionError("\n"+"\n".join(context_diff( - test_content.splitlines(), out_content.splitlines(), - fromfile=str(testpath), tofile=str(outpath)))) + raise AssertionError('\n' + '\n'.join( + context_diff( + test_content.splitlines(), out_content.splitlines(), fromfile=str(testpath), tofile=str(outpath)))) def compare_html_files(testpath, outpath, encoding='utf8'): @@ -310,18 +295,18 @@ def compare_html_files(testpath, outpath, encoding='utf8'): # extract only the body # could use html.parser or beautifulsoup to do this better - body_rgx = re.compile("\\(.*)\\", re.DOTALL) + body_rgx = re.compile('\\(.*)\\', re.DOTALL) body_search = body_rgx.search(content) if not body_search: - raise IOError("could not find body content of {}".format(path)) + raise IOError('could not find body content of {}'.format(path)) content = body_search.group(1) # remove script environments which can change (e.g. reveal) - script_rgx = re.compile("\\(.*)\\", re.DOTALL) - content = script_rgx.sub("", content) + script_rgx = re.compile('\\(.*)\\', re.DOTALL) + content = script_rgx.sub('', content) # remove trailing whitespace - content = "\n".join([l.rstrip() for l in content.splitlines()]) + content = '\n'.join([l.rstrip() for l in content.splitlines()]) output.append(content) @@ -329,9 +314,9 @@ def compare_html_files(testpath, outpath, encoding='utf8'): # only report differences if out_content != test_content: - raise AssertionError("\n"+"\n".join(context_diff( - test_content.splitlines(), out_content.splitlines(), - fromfile=str(testpath), tofile=str(outpath)))) + raise AssertionError('\n' + '\n'.join( + context_diff( + test_content.splitlines(), out_content.splitlines(), fromfile=str(testpath), tofile=str(outpath)))) def compare_tex_files(testpath, outpath, encoding='utf8'): @@ -344,38 +329,36 @@ def compare_tex_files(testpath, outpath, encoding='utf8'): # only certain versions of pandoc wrap sections with \hypertarget # NOTE a better way to do this might be to use TexSoup - ht_rgx = re.compile("\\\\hypertarget\\{[^\\}]*\\}\\{[^\\\\]*" - "(\\\\[sub]*section\\{[^\\}]*\\}" - "\\\\label\\{[^\\}]*\\})" - "\\}", - re.DOTALL) - content = ht_rgx.sub("\\g<1>", content) + ht_rgx = re.compile( + '\\\\hypertarget\\{[^\\}]*\\}\\{[^\\\\]*' + '(\\\\[sub]*section\\{[^\\}]*\\}' + '\\\\label\\{[^\\}]*\\})' + '\\}', re.DOTALL) + content = ht_rgx.sub('\\g<1>', content) # newer versions of pandoc convert ![](file) to \begin{figure}[htbp] # TODO override pandoc figure placement of ![](file) in markdown2latex - content = content.replace("\\begin{figure}[htbp]", "\\begin{figure}") + content = content.replace('\\begin{figure}[htbp]', '\\begin{figure}') # at start of itemize - content = content.replace("\\itemsep1pt\\parskip0pt\\parsep0pt\n", "") + content = content.replace('\\itemsep1pt\\parskip0pt\\parsep0pt\n', '') # at start of enumerate - content = content.replace("\\tightlist\n", "") + content = content.replace('\\tightlist\n', '') # python 3.5 used .jpg instead of .jpeg - content = content.replace(".jpg", ".jpeg") + content = content.replace('.jpg', '.jpeg') # python < 3.6 sorts these differently - pyg_rgx = re.compile( - ("\\\\expandafter\\\\def\\\\csname " - "PY\\@tok\\@[0-9a-zA-Z]*\\\\endcsname[^\n]*"), - re.MULTILINE) - content = pyg_rgx.sub(r"\", content) + pyg_rgx = re.compile(('\\\\expandafter\\\\def\\\\csname ' + 'PY\\@tok\\@[0-9a-zA-Z]*\\\\endcsname[^\n]*'), re.MULTILINE) + content = pyg_rgx.sub(r'\', content) # also remove all space from start of lines - space_rgx = re.compile(r"^[\s]*", re.MULTILINE) - content = space_rgx.sub("", content) + space_rgx = re.compile(r'^[\s]*', re.MULTILINE) + content = space_rgx.sub('', content) # remove trailing whitespace - content = "\n".join([l.rstrip() for l in content.splitlines()]) + content = '\n'.join([l.rstrip() for l in content.splitlines()]) output.append(content) @@ -383,6 +366,6 @@ def compare_tex_files(testpath, outpath, encoding='utf8'): # only report differences if out_content != test_content: - raise AssertionError("\n"+"\n".join(context_diff( - test_content.splitlines(), out_content.splitlines(), - fromfile=str(testpath), tofile=str(outpath)))) + raise AssertionError('\n' + '\n'.join( + context_diff( + test_content.splitlines(), out_content.splitlines(), fromfile=str(testpath), tofile=str(outpath)))) From b9ceee6a67e6d856df37ffb415491e2aaa667f63 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 12 Aug 2019 19:17:00 +0100 Subject: [PATCH 5/7] Added pytest-notebook link. --- docs/source/index.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index ed09c53..95ebe90 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -13,10 +13,12 @@ and presentations, from Jupyter Notebooks. .. attention:: - IPyPublish has evolved! - + IPyPublish is evolving! Please see :ref:`releases` for all the exciting new feature. + Also see `pytest-notebook `_, + for a new tool to test and regenerate notebooks. + IPyPublish: Features ==================== From 909f3c30ae848435a9b31e7db218825f91752edd Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 4 Sep 2019 15:11:47 +0100 Subject: [PATCH 6/7] Improve ipubpandoc filter: conversion of equations (#105) - Ensure equations that are already wrapped in a math environment are not wrapped twice. - For RST output, ensure multiline equations are correctly indented --- .../filters_pandoc/format_label_elements.py | 201 ++++++++---------- .../filters_pandoc/tests/test_jinja_filter.py | 77 +++---- 2 files changed, 126 insertions(+), 152 deletions(-) diff --git a/ipypublish/filters_pandoc/format_label_elements.py b/ipypublish/filters_pandoc/format_label_elements.py index ab15431..de30285 100644 --- a/ipypublish/filters_pandoc/format_label_elements.py +++ b/ipypublish/filters_pandoc/format_label_elements.py @@ -21,13 +21,21 @@ # TODO format headers with section labels # (see ipysphinx.transforms.CreateNotebookSectionAnchors) import json +import re + from panflute import Element, Doc, Span, Div, Math, Image, Table # noqa: F401 import panflute as pf from ipypublish.filters_pandoc.utils import convert_units, convert_attributes -from ipypublish.filters_pandoc.prepare_labels import ( - LABELLED_IMAGE_CLASS, LABELLED_MATH_CLASS, LABELLED_TABLE_CLASS -) +from ipypublish.filters_pandoc.prepare_labels import (LABELLED_IMAGE_CLASS, LABELLED_MATH_CLASS, LABELLED_TABLE_CLASS) + +try: + from textwrap import indent +except ImportError: # added in python 3.3 + + def indent(text, prefix): + return ''.join(prefix + line for line in text.splitlines(True)) + LATEX_FIG_LABELLED = """\\begin{{figure}}[{options}] \\hypertarget{{{label}}}{{% @@ -45,6 +53,8 @@ \\caption{{{caption}}} \\end{{figure}}""" # noqa: E501 +MATH_ENVS = ('equation', 'align', 'alignat', 'eqnarray', 'multline', 'gather', 'flalign', 'dmath') + def format_math(math, doc): # type: (Math, Doc) -> Element @@ -55,47 +65,50 @@ def format_math(math, doc): if not isinstance(math, pf.Math): return None - if math.format != "DisplayMath": + if math.format != 'DisplayMath': return None - span = None - number = "" - env = "equation" - label_tag = "" - if (isinstance(math.parent, pf.Span) - and LABELLED_MATH_CLASS in math.parent.classes): - span = math.parent + # test if the math text is already wrapped in an environment + regex = re.compile(r'\\begin\{{((?:{0})\*?)\}}(.*)\\end\{{((?:{0})\*?)\}}'.format('|'.join(MATH_ENVS)), re.DOTALL) + wrap_match = regex.match(math.text) - number = '*' if "unnumbered" in span.classes else '' - env = span.attributes.get("env", "equation") - if doc.format in ("tex", "latex"): - label_tag = "\\label{{{0}}}".format(span.identifier) + env = None + label = None + if (isinstance(math.parent, pf.Span) and LABELLED_MATH_CLASS in math.parent.classes): + span = math.parent + numbered = '*' if 'unnumbered' in span.classes else '' + env = span.attributes.get('env', 'equation') + numbered + label = span.identifier + + if doc.format in ('tex', 'latex'): + if wrap_match: + # TODO edge case where a label has been specified, but the math is already wrapped + tex = math.text else: - label_tag = "" - - # construct latex environment - tex = '\\begin{{{0}{1}}}{2}{3}\\end{{{0}{1}}}'.format( - env, number, math.text, label_tag) + tex = '\\begin{{{0}}}{1}\\label{{{2}}}\\end{{{0}}}'.format(env or 'equation', math.text, label or '') + return pf.RawInline(tex, format='tex') - if doc.format in ("tex", "latex"): - return pf.RawInline(tex, format="tex") - - elif doc.format in ("rst"): - if not span: - rst = '\n\n.. math::\n :nowrap:\n\n {0}\n\n'.format(tex) + elif doc.format in ('rst'): + if env: + tex = indent('\\begin{{{0}}}{1}\\end{{{0}}}'.format(env, math.text), ' ') else: - rst = ( - '\n\n.. math::\n :nowrap:\n :label: {0}' - '\n\n {1}\n\n'.format(span.identifier, tex)) - return pf.RawInline(rst, format="rst") + tex = indent(math.text.strip(), ' ') + rst = '\n\n.. math::\n' + if wrap_match or env: + rst += ' :nowrap:\n' + if label: + rst += ' :label: {}\n'.format(label) + rst += '\n{}\n\n'.format(tex) + return pf.RawInline(rst, format='rst') elif doc.format in ('html', 'html5'): # new_span = pf.Span(anchor_start, math, anchor_end) # TODO add formatting # TODO name by count - if span: - math.text = tex - return _wrap_in_anchor(math, span.identifier) + if label: + if not wrap_match: + math.text = '\\begin{{{0}}}{1}\\end{{{0}}}'.format(env or 'equation', math.text) + return _wrap_in_anchor(math, label) else: return None @@ -110,8 +123,7 @@ def format_image(image, doc): return None span = None - if (isinstance(image.parent, pf.Span) - and LABELLED_IMAGE_CLASS in image.parent.classes): + if (isinstance(image.parent, pf.Span) and LABELLED_IMAGE_CLASS in image.parent.classes): span = image.parent if span is not None: @@ -123,41 +135,35 @@ def format_image(image, doc): attributes = image.attributes # classes = image.classes - if doc.format in ("tex", "latex"): + if doc.format in ('tex', 'latex'): new_doc = Doc(pf.Para(*image.content)) new_doc.api_version = doc.api_version if image.content: - caption = pf.run_pandoc(json.dumps(new_doc.to_json()), - args=["-f", "json", "-t", "latex"]).strip() + caption = pf.run_pandoc(json.dumps(new_doc.to_json()), args=['-f', 'json', '-t', 'latex']).strip() else: - caption = "" + caption = '' - options = attributes.get("placement", "") + options = attributes.get('placement', '') size = '' # max width set as 0.9\linewidth - if "width" in attributes: - width = convert_units(attributes['width'], "fraction") + if 'width' in attributes: + width = convert_units(attributes['width'], 'fraction') size = 'width={0}\\linewidth'.format(width) - elif "height" in attributes: - height = convert_units(attributes['height'], "fraction") + elif 'height' in attributes: + height = convert_units(attributes['height'], 'fraction') size = 'height={0}\\paperheight'.format(height) if identifier: - latex = LATEX_FIG_LABELLED.format( - label=identifier, - options=options, - path=image.url, - caption=caption, - size=size) + latex = LATEX_FIG_LABELLED.format(label=identifier, + options=options, + path=image.url, + caption=caption, + size=size) else: - latex = LATEX_FIG_UNLABELLED.format( - options=options, - path=image.url, - caption=caption, - size=size) + latex = LATEX_FIG_UNLABELLED.format(options=options, path=image.url, caption=caption, size=size) - return pf.RawInline(latex, format="tex") + return pf.RawInline(latex, format='tex') - elif doc.format in ("rst",): + elif doc.format in ('rst',): if not image.content.list: # If the container is empty, then pandoc will assign an iterative # reference identifier to it (image0, image1). @@ -172,7 +178,7 @@ def format_image(image, doc): return image # TODO formatting and span identifier (convert width/height to %) - elif doc.format in ("html", "html5"): + elif doc.format in ('html', 'html5'): if identifier: return _wrap_in_anchor(image, identifier) else: @@ -192,8 +198,7 @@ def format_table(table, doc): return None div = None # type: pf.Div - if (isinstance(table.parent, pf.Div) - and LABELLED_TABLE_CLASS in table.parent.classes): + if (isinstance(table.parent, pf.Div) and LABELLED_TABLE_CLASS in table.parent.classes): div = table.parent if div is None: @@ -201,68 +206,53 @@ def format_table(table, doc): attributes = convert_attributes(div.attributes) - if "align" in div.attributes: - align_text = attributes["align"] - align = [ - {'l': 'AlignLeft', - 'r': 'AlignRight', - 'c': 'AlignCenter'}.get(a, None) for a in align_text] + if 'align' in div.attributes: + align_text = attributes['align'] + align = [{'l': 'AlignLeft', 'r': 'AlignRight', 'c': 'AlignCenter'}.get(a, None) for a in align_text] if None in align: - raise ValueError( - "table '{0}' alignment must contain only l,r,c:" - " {1}".format(div.identifier, align_text)) + raise ValueError("table '{0}' alignment must contain only l,r,c:" ' {1}'.format(div.identifier, align_text)) table.alignment = align - attributes["align"] = align + attributes['align'] = align - if "widths" in div.attributes: - widths = attributes["widths"] + if 'widths' in div.attributes: + widths = attributes['widths'] try: widths = [float(w) for w in widths] except Exception: - raise ValueError( - "table '{0}' widths must be a list of numbers:" - " {1}".format(div.identifier, widths)) + raise ValueError("table '{0}' widths must be a list of numbers:" ' {1}'.format(div.identifier, widths)) table.width = widths - attributes["widths"] = widths + attributes['widths'] = widths - if doc.format in ("tex", "latex"): + if doc.format in ('tex', 'latex'): # TODO placement - table.caption.append(pf.RawInline( - '\\label{{{0}}}'.format(div.identifier), format="tex")) + table.caption.append(pf.RawInline('\\label{{{0}}}'.format(div.identifier), format='tex')) return table - if doc.format in ("rst",): + if doc.format in ('rst',): # pandoc 2.6 doesn't output table options if attributes: tbl_doc = pf.Doc(table) tbl_doc.api_version = doc.api_version - tbl_str = pf.convert_text(tbl_doc, - input_format="panflute", - output_format="rst") + tbl_str = pf.convert_text(tbl_doc, input_format='panflute', output_format='rst') tbl_lines = tbl_str.splitlines() - if tbl_lines[1].strip() == "": - tbl_lines.insert(1, " :align: center") - if "widths" in attributes: + if tbl_lines[1].strip() == '': + tbl_lines.insert(1, ' :align: center') + if 'widths' in attributes: # in rst widths must be integers - widths = " ".join([str(int(w*10)) for w in table.width]) - tbl_lines.insert(1, " :widths: {}".format(widths)) + widths = ' '.join([str(int(w * 10)) for w in table.width]) + tbl_lines.insert(1, ' :widths: {}'.format(widths)) # TODO rst column alignment, see # https://cloud-sptheme.readthedocs.io/en/latest/lib/cloud_sptheme.ext.table_styling.html return [ - pf.Para(pf.RawInline( - '.. _`{0}`:'.format(div.identifier), format="rst")), - pf.RawBlock("\n".join(tbl_lines)+"\n\n", format=doc.format) + pf.Para(pf.RawInline('.. _`{0}`:'.format(div.identifier), format='rst')), + pf.RawBlock('\n'.join(tbl_lines) + '\n\n', format=doc.format) ] - return [ - pf.Para(pf.RawInline( - '.. _`{0}`:'.format(div.identifier), format="rst")), - table - ] + return [pf.Para(pf.RawInline('.. _`{0}`:'.format(div.identifier), format='rst')), table] - if doc.format in ("html", "html5"): + if doc.format in ('html', 'html5'): return _wrap_in_anchor(table, div.identifier, inline=False) # TODO formatting, name by count @@ -270,14 +260,10 @@ def format_table(table, doc): def strip_labelled_spans(element, doc): # type: (Span, Doc) -> Element - if isinstance(element, pf.Span) and set(element.classes).intersection([ - LABELLED_IMAGE_CLASS, LABELLED_MATH_CLASS - ]): + if isinstance(element, pf.Span) and set(element.classes).intersection([LABELLED_IMAGE_CLASS, LABELLED_MATH_CLASS]): return list(element.content) - if isinstance(element, pf.Div) and set(element.classes).intersection([ - LABELLED_TABLE_CLASS - ]): + if isinstance(element, pf.Div) and set(element.classes).intersection([LABELLED_TABLE_CLASS]): return list(element.content) @@ -291,10 +277,8 @@ def _wrap_in_anchor(element, label, inline=True): raw = pf.RawInline else: raw = pf.RawBlock - anchor_start = raw( - ''.format( - label), format="html") - anchor_end = raw("", format="html") + anchor_start = raw(''.format(label), format='html') + anchor_end = raw('', format='html') return [anchor_start, element, anchor_end] @@ -313,8 +297,7 @@ def main(doc=None, strip_spans=True): to_run = [format_math, format_image, format_table] if strip_spans: to_run.append(strip_labelled_spans) - return pf.run_filters(to_run, - prepare, finalize, doc=doc) + return pf.run_filters(to_run, prepare, finalize, doc=doc) if __name__ == '__main__': diff --git a/ipypublish/filters_pandoc/tests/test_jinja_filter.py b/ipypublish/filters_pandoc/tests/test_jinja_filter.py index 4766f6b..5dde222 100644 --- a/ipypublish/filters_pandoc/tests/test_jinja_filter.py +++ b/ipypublish/filters_pandoc/tests/test_jinja_filter.py @@ -4,76 +4,67 @@ def test_basic(): - out_str = jinja_filter( - "a", "rst", {}, {} - ) - assert out_str == "a" + out_str = jinja_filter('a', 'rst', {}, {}) + assert out_str == 'a' def test_reference(): - out_str = jinja_filter( - "@label", "rst", {}, {} - ) - assert out_str == ":cite:`label`" + out_str = jinja_filter('@label', 'rst', {}, {}) + assert out_str == ':cite:`label`' def test_reference_prefix(): - out_str = jinja_filter( - "+@label", "rst", {}, {} - ) - assert out_str == ":numref:`label`" + out_str = jinja_filter('+@label', 'rst', {}, {}) + assert out_str == ':numref:`label`' def test_option_in_nb_meta(): - out_str = jinja_filter( - "+@label", "rst", create_ipub_meta({"use_numref": False}), {} - ) - assert out_str == ":ref:`label`" + out_str = jinja_filter('+@label', 'rst', create_ipub_meta({'use_numref': False}), {}) + assert out_str == ':ref:`label`' def test_option_in_cell_meta(): - out_str = jinja_filter( - "+@label", "rst", create_ipub_meta({"use_numref": False}), - create_ipub_meta({"use_numref": True}) - ) - assert out_str == ":numref:`label`" + out_str = jinja_filter('+@label', 'rst', create_ipub_meta({'use_numref': False}), + create_ipub_meta({'use_numref': True})) + assert out_str == ':numref:`label`' def test_option_in_top_matter(): # TODO create ipub yaml from IPUB_META_ROUTE - in_str = "\n".join([ - "---", - "ipub:", - " pandoc:", - " use_numref: true", - "", - "...", - "", - "+@label" - ]) + in_str = '\n'.join(['---', 'ipub:', ' pandoc:', ' use_numref: true', '', '...', '', '+@label']) - out_str = jinja_filter( - in_str, "rst", create_ipub_meta({"use_numref": False}), {} - ) - assert out_str == ":numref:`label`" + out_str = jinja_filter(in_str, 'rst', create_ipub_meta({'use_numref': False}), {}) + assert out_str == ':numref:`label`' def test_at_notation_false(): - out_str = jinja_filter( - "+@label", "rst", create_ipub_meta({"at_notation": False}), {} - ) - assert out_str == "+ :cite:`label`" + out_str = jinja_filter('+@label', 'rst', create_ipub_meta({'at_notation': False}), {}) + assert out_str == '+ :cite:`label`' def test_remove_filter(): - out_str = jinja_filter( - "+@label", "rst", create_ipub_meta({"apply_filters": False}), {} - ) - assert out_str == "+@label" + out_str = jinja_filter('+@label', 'rst', create_ipub_meta({'apply_filters': False}), {}) + assert out_str == '+@label' + + +def test_complex_equation(): + + in_source = [ + '$$\\begin{equation*}\n', 'f(x) = \\left\\{\n', '\\begin{array}{ll}\n', '\\; x \\qquad x \\geq 0 \\\\\n', + '\\; 0 \\qquad else\n', '\\end{array}\n', '\\right.\n', '\\end{equation*}$$' + ] + + out_string = jinja_filter(''.join(in_source), 'rst', create_ipub_meta({}), {}) + expected = [ + '.. math::', ' :nowrap:', '', ' \\begin{equation*}', ' f(x) = \\left\\{', ' \\begin{array}{ll}', + ' \\; x \\qquad x \\geq 0 \\\\', ' \\; 0 \\qquad else', ' \\end{array}', ' \\right.', + ' \\end{equation*}' + ] + assert out_string.strip() == '\n'.join(expected) From a4eee3f7c08b4b38024426dae3e0c7b5a4ed9fef Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 4 Sep 2019 15:15:29 +0100 Subject: [PATCH 7/7] bump version --- docs/source/releases.rst | 9 +++++++++ ipypublish/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/source/releases.rst b/docs/source/releases.rst index 2ccc9e7..6b34031 100644 --- a/docs/source/releases.rst +++ b/docs/source/releases.rst @@ -13,6 +13,15 @@ Releases Version 0.10 ------------ +v0.10.9 +~~~~~~~ + +Improve ipubpandoc filter conversion of equations: + +- Ensure equations that are already wrapped in a math environment + are not wrapped twice. +- For RST output, ensure multi-line equations are correctly indented + v0.10.6 & v0.10.7 ~~~~~~~~~~~~~~~~~ diff --git a/ipypublish/__init__.py b/ipypublish/__init__.py index c778d51..70d683f 100644 --- a/ipypublish/__init__.py +++ b/ipypublish/__init__.py @@ -1,3 +1,3 @@ from ipypublish.scripts import nb_setup # noqa: F401 -__version__ = '0.10.8' +__version__ = '0.10.9'