-
Notifications
You must be signed in to change notification settings - Fork 393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add mystnb format #456
Merged
Merged
Add mystnb format #456
Changes from 8 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
d453238
initial implementation of myst read/write
chrisjsewell f55ea19
add newline before code/raw cells
chrisjsewell a5192b7
Add new line before markdown
chrisjsewell fb25e41
add logger
chrisjsewell 1ef5f4f
add lexer to code cells arguments
chrisjsewell 3b10c00
Make python 2.7 compliant
chrisjsewell 10a2c7f
fix flake8
chrisjsewell c84d9f6
move myst install out of requirements-dev
chrisjsewell 17267a2
add MystMetadataParsingError and tests
chrisjsewell 2929eaa
fix python 3.5 tests
chrisjsewell 63107b0
typo
chrisjsewell 25b1182
typo
chrisjsewell d627398
Introduce a more compact metadata style
chrisjsewell ca45c6e
flake8 fix
chrisjsewell 237a108
Add additional myst extension
chrisjsewell 8999b1d
add myst-parser to environment.yaml
chrisjsewell 4c206ec
Add MyST-NB to nbextension
chrisjsewell 43624c8
Add benchmarking
chrisjsewell File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
""" | ||
This module contains round-trip conversion between | ||
myst formatted text documents and notebooks. | ||
""" | ||
import json | ||
import logging | ||
|
||
import nbformat as nbf | ||
import yaml | ||
|
||
MYST_FORMAT_NAME = "mystnb" | ||
CODE_DIRECTIVE = "nb-code" | ||
RAW_DIRECTIVE = "nb-raw" | ||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
def is_myst_available(): | ||
try: | ||
import myst_parser # noqa | ||
except ImportError: | ||
return False | ||
return True | ||
|
||
|
||
def myst_version(): | ||
from myst_parser import __version__ | ||
|
||
return __version__ | ||
|
||
|
||
def myst_extensions(): | ||
return [".mystnb"] | ||
|
||
|
||
def from_nbnode(value): | ||
"""Recursively convert NotebookNode to dict.""" | ||
if isinstance(value, nbf.NotebookNode): | ||
return {k: from_nbnode(v) for k, v in value.items()} | ||
return value | ||
|
||
|
||
class MockDirective: | ||
option_spec = {"options": True} | ||
required_arguments = 0 | ||
optional_arguments = 1 | ||
has_content = True | ||
|
||
|
||
def _fmt_md(text): | ||
text = text.rstrip() | ||
while text and text.startswith("\n"): | ||
text = text[1:] | ||
return text | ||
|
||
|
||
def myst_to_notebook( | ||
text, code_directive=CODE_DIRECTIVE, raw_directive=RAW_DIRECTIVE, logger=None, | ||
): | ||
"""Convert text written in the myst format to a notebook. | ||
|
||
:param text: the file text | ||
:directive: the name of the directive to search for. | ||
|
||
NOTE: we assume here that all of these directives are at the top-level, | ||
i.e. not nested in other directives. | ||
""" | ||
from mistletoe.base_elements import SourceLines | ||
from mistletoe.parse_context import ( | ||
ParseContext, | ||
get_parse_context, | ||
set_parse_context, | ||
) | ||
from mistletoe.block_tokens import Document, CodeFence | ||
|
||
from myst_parser.block_tokens import BlockBreak | ||
from myst_parser.parse_directives import parse_directive_text | ||
from myst_parser.docutils_renderer import DocutilsRenderer | ||
|
||
code_directive = "{{{0}}}".format(code_directive) | ||
raw_directive = "{{{0}}}".format(raw_directive) | ||
logger = logger or LOGGER | ||
|
||
original_context = get_parse_context() | ||
parse_context = ParseContext( | ||
find_blocks=DocutilsRenderer.default_block_tokens, | ||
find_spans=DocutilsRenderer.default_span_tokens, | ||
) | ||
|
||
if isinstance(text, SourceLines): | ||
lines = text | ||
else: | ||
lines = SourceLines(text, standardize_ends=True) | ||
|
||
try: | ||
set_parse_context(parse_context) | ||
doc = Document.read(lines, front_matter=True) | ||
|
||
metadata_nb = doc.front_matter.get_data() if doc.front_matter else {} | ||
nbformat = metadata_nb.pop("nbformat", None) | ||
nbformat_minor = metadata_nb.pop("nbformat_minor", None) | ||
kwargs = {"metadata": nbf.from_dict(metadata_nb)} | ||
if nbformat is not None: | ||
kwargs["nbformat"] = nbformat | ||
if nbformat_minor is not None: | ||
kwargs["nbformat_minor"] = nbformat_minor | ||
|
||
notebook = nbf.v4.new_notebook(**kwargs) | ||
|
||
current_line = 0 if not doc.front_matter else doc.front_matter.position.line_end | ||
md_metadata = {} | ||
|
||
for item in doc.walk(["CodeFence", "BlockBreak"]): | ||
if isinstance(item.node, BlockBreak): | ||
token = item.node # type: BlockBreak | ||
source = _fmt_md( | ||
"".join(lines.lines[current_line:token.position.line_start - 1]) | ||
) | ||
if source: | ||
notebook.cells.append( | ||
nbf.v4.new_markdown_cell( | ||
source=source, metadata=nbf.from_dict(md_metadata), | ||
) | ||
) | ||
if token.content: | ||
try: | ||
md_metadata = json.loads(token.content.strip()) | ||
except Exception: | ||
logger.warning( | ||
"markdown cell metadata could not be read: {}".format( | ||
token.position | ||
) | ||
) | ||
md_metadata = {} | ||
if not isinstance(md_metadata, dict): | ||
logger.warning( | ||
"markdown cell metadata is not a dict: {}".format( | ||
token.position | ||
) | ||
) | ||
md_metadata = {} | ||
else: | ||
md_metadata = {} | ||
current_line = token.position.line_start | ||
if isinstance(item.node, CodeFence) and item.node.language in [ | ||
code_directive, | ||
raw_directive, | ||
]: | ||
token = item.node # type: CodeFence | ||
# Note: we ignore anything after the directive on the first line | ||
# this is reserved for the optional lexer name | ||
# TODO: could log warning about if token.arguments != lexer name | ||
|
||
_, options, body_lines = parse_directive_text( | ||
directive_class=MockDirective, | ||
argument_str="", | ||
content=token.children[0].content, | ||
validate_options=False, | ||
) | ||
|
||
md_source = _fmt_md( | ||
"".join(lines.lines[current_line:token.position.line_start - 1]) | ||
) | ||
if md_source: | ||
notebook.cells.append( | ||
nbf.v4.new_markdown_cell( | ||
source=md_source, metadata=nbf.from_dict(md_metadata), | ||
) | ||
) | ||
current_line = token.position.line_end | ||
md_metadata = {} | ||
|
||
if item.node.language == code_directive: | ||
notebook.cells.append( | ||
nbf.v4.new_code_cell( | ||
source="\n".join(body_lines), | ||
metadata=nbf.from_dict(options), | ||
) | ||
) | ||
if item.node.language == raw_directive: | ||
notebook.cells.append( | ||
nbf.v4.new_raw_cell( | ||
source="\n".join(body_lines), | ||
metadata=nbf.from_dict(options), | ||
) | ||
) | ||
|
||
# add the final markdown cell (if present) | ||
if lines.lines[current_line:]: | ||
notebook.cells.append( | ||
nbf.v4.new_markdown_cell( | ||
source=_fmt_md("".join(lines.lines[current_line:])), | ||
metadata=nbf.from_dict(md_metadata), | ||
) | ||
) | ||
|
||
finally: | ||
set_parse_context(original_context) | ||
|
||
return notebook | ||
|
||
|
||
def notebook_to_myst( | ||
nb, code_directive=CODE_DIRECTIVE, raw_directive=RAW_DIRECTIVE, default_lexer=None | ||
): | ||
string = "" | ||
|
||
nb_metadata = from_nbnode(nb.metadata) | ||
nb_metadata["nbformat"] = nb.nbformat | ||
nb_metadata["nbformat_minor"] = nb.nbformat_minor | ||
|
||
# we add the pygments lexer as a directive argument, for use by syntax highlighters | ||
pygments_lexer = nb_metadata.get("language_info", {}).get("pygments_lexer", None) | ||
if pygments_lexer is None: | ||
pygments_lexer = default_lexer | ||
|
||
string += "---\n" | ||
string += yaml.safe_dump(nb_metadata) | ||
string += "---\n" | ||
|
||
last_cell_md = False | ||
for i, cell in enumerate(nb.cells): | ||
|
||
if cell.cell_type == "markdown": | ||
metadata = from_nbnode(cell.metadata) | ||
if metadata or last_cell_md: | ||
if metadata: | ||
string += "\n+++ {}\n".format(json.dumps(metadata)) | ||
else: | ||
string += "\n+++\n" | ||
string += "\n" + cell.source | ||
if not cell.source.endswith("\n"): | ||
string += "\n" | ||
last_cell_md = True | ||
|
||
elif cell.cell_type in ["code", "raw"]: | ||
string += "\n```{{{}}}".format( | ||
code_directive if cell.cell_type == "code" else raw_directive | ||
) | ||
if pygments_lexer and cell.cell_type == "code": | ||
string += " {}".format(pygments_lexer) | ||
string += "\n" | ||
metadata = from_nbnode(cell.metadata) | ||
if metadata: | ||
string += "---\n" | ||
string += yaml.safe_dump(metadata) | ||
string += "---\n" | ||
elif cell.source.startswith("---") or cell.source.startswith(":"): | ||
string += "\n" | ||
string += cell.source | ||
if not cell.source.endswith("\n"): | ||
string += "\n" | ||
string += "```\n" | ||
last_cell_md = False | ||
|
||
else: | ||
raise NotImplementedError("cell {}, type: {}".format(i, cell.cell_type)) | ||
|
||
return string.rstrip() + "\n" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
tests/notebooks/mirror/ipynb_to_myst/Line_breaks_in_LateX_305.mystnb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
--- | ||
kernelspec: | ||
display_name: Python 3 | ||
language: python | ||
name: python3 | ||
nbformat: 4 | ||
nbformat_minor: 2 | ||
--- | ||
|
||
This cell uses no particular cell marker | ||
|
||
$$ | ||
\begin{align} | ||
\dot{x} & = \sigma(y-x)\\ | ||
\dot{y} & = \rho x - y - xz \\ | ||
\dot{z} & = -\beta z + xy | ||
\end{align} | ||
$$ | ||
|
||
+++ | ||
|
||
This cell uses no particular cell marker, and a single slash in the $\LaTeX$ equation | ||
|
||
$$ | ||
\begin{align} | ||
\dot{x} & = \sigma(y-x) \ | ||
\dot{y} & = \rho x - y - xz \ | ||
\dot{z} & = -\beta z + xy | ||
\end{align} | ||
$$ | ||
|
||
+++ | ||
|
||
This cell uses the triple quote cell markers introduced at https://github.com/mwouts/jupytext/issues/305 | ||
|
||
$$ | ||
\begin{align} | ||
\dot{x} & = \sigma(y-x)\\ | ||
\dot{y} & = \rho x - y - xz \\ | ||
\dot{z} & = -\beta z + xy | ||
\end{align} | ||
$$ |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved the install to an extra, if thats ok