Repository: mwouts/jupytext
Pull request #353 (2019-10-18 21:49:37)

Markdown format update
- Markdown cells can start with <!-- #md --> or <!-- #markdown --> (`.md` and `.markdown` files)
- Active/inactive cells behave the same in Markdown files than in the other formats #347 
- Shorter start of cell marker: `"# %% [md]"` for the `percent` format 
- All formats can now parse `key=value` metadata #344 


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 60556ed1..020b42c5 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -8,8 +8,10 @@ Release History
 
 **Improvements**
 
-- Raw cells are now encoded using HTML comments (``<!-- #raw -->`` and ``<!-- #endraw -->``) in Markdown files. And code blocks from Markdown files, when they don't have an explicit language, are displayed as Markdown cells in Jupyter (#321)
-- Markdown and raw cells can use multiline comments in the ``py:percent`` format (#305)
+- Raw cells are now encoded using HTML comments (``<!-- #raw -->`` and ``<!-- #endraw -->``) in Markdown files (#321)
+- Markdown cells can be delimited with any of ``<!-- #region -->``,  ``<!-- #markdown -->`` or ``<!-- #md -->`` (#344)
+- Code blocks from Markdown files, when they don't have an explicit language, below to the text of the Markdown cell in Jupyter (#321)
+- Markdown and raw cells can use multiline comments in the ``py:percent`` format. And Markdown cells can start with just ``# %% [md]`` (#305)
 - ``jupytext notebook.py --to ipynb`` updates the timestamp of ``notebook.py`` so that the paired notebook still works in Jupyter (#335, #254)
 - The Jupyter Notebook extension for Jupytext is compatible with Jupyter Notebook 6.0 (#346)
 - Added support for Rust/Evxcr, by Jonas Bushart (#351)
diff --git a/jupytext/cell_metadata.py b/jupytext/cell_metadata.py
index 3653b044..157f1c1b 100644
--- a/jupytext/cell_metadata.py
+++ b/jupytext/cell_metadata.py
@@ -36,8 +36,8 @@ _IGNORE_CELL_METADATA = ','.join('-{}'.format(name) for name in [
     # (these metadata are preserved in the paired Jupyter notebook).
     'autoscroll', 'collapsed', 'scrolled', 'trusted', 'ExecuteTime'] +
                                  _JUPYTEXT_CELL_METADATA)
-_PERCENT_CELL = re.compile(
-    r'(# |#)%%([^\{\[]*)(|\[raw\]|\[markdown\])([^\{\[]*)(|\{.*\})\s*$')
+
+_IDENTIFIER_RE = re.compile(r'^[a-zA-Z_\\.]+[a-zA-Z0-9_\\.]*$')
 
 
 def _r_logical_values(pybool):
@@ -257,59 +257,6 @@ def metadata_to_md_options(metadata):
                      if metadata[key] is not None else key for key in metadata])
 
 
-def parse_md_code_options(options):
-    """Parse 'python class key="value"' into [('python', None), ('class', None), ('key', 'value')]"""
-
-    metadata = []
-    while options:
-        name_and_value = re.split(r'[\s=]+', options, maxsplit=1)
-        name = name_and_value[0]
-
-        # Equal sign in between name and what's next?
-        if len(name_and_value) == 2:
-            sep = options[len(name):-len(name_and_value[1])]
-            has_value = sep.find('=') >= 0
-            options = name_and_value[1]
-        else:
-            has_value = False
-            options = ''
-
-        if not has_value:
-            metadata.append((name, None))
-            continue
-
-        try:
-            value = loads(options)
-            options = ''
-        except JSONDecodeError as err:
-            try:
-                split = err.colno - 1
-            except AttributeError:
-                # str(err) is like: "ValueError: Extra data: line 1 column 7 - line 1 column 50 (char 6 - 49)"
-                match = re.match(r'.*char ([0-9]*)', str(err))
-                split = int(match.groups()[0])
-
-            value = loads(options[:split])
-            options = options[split:]
-
-        metadata.append((name, value))
-
-    return metadata
-
-
-def md_options_to_metadata(options):
-    """Parse markdown options and return language and metadata"""
-    metadata = parse_md_code_options(options)
-
-    if metadata:
-        language = metadata[0][0]
-        for lang in _JUPYTER_LANGUAGES:
-            if language.lower() == lang.lower():
-                return lang, dict(metadata[1:])
-
-    return None, dict(metadata)
-
-
 def try_eval_metadata(metadata, name):
     """Evaluate given metadata to a python object, if possible"""
     value = metadata[name]
@@ -329,84 +276,168 @@ def try_eval_metadata(metadata, name):
         return
 
 
-def json_options_to_metadata(options, add_brackets=True):
-    """Read metadata from its json representation"""
-    try:
-        options = loads('{' + options + '}' if add_brackets else options)
-        return options
-    except ValueError:
-        return {}
-
-
-def metadata_to_json_options(metadata):
-    """Represent metadata as json text"""
-    for key in _JUPYTEXT_CELL_METADATA:
-        metadata.pop(key, None)
-
-    return dumps(metadata)
-
-
-def is_active(ext, metadata):
+def is_active(ext, metadata, default=True):
     """Is the cell active for the given file extension?"""
     if metadata.get('run_control', {}).get('frozen') is True:
-        return False
+        return ext == '.ipynb'
     for tag in metadata.get('tags', []):
         if tag.startswith('active-'):
             return ext.replace('.', '') in tag.split('-')
     if 'active' not in metadata:
-        return True
+        return default
     return ext.replace('.', '') in re.split('\\.|,', metadata['active'])
 
 
-def double_percent_options_to_metadata(options):
-    """Parse double percent options"""
-    matches = _PERCENT_CELL.findall('# %%' + options)
+def metadata_to_double_percent_options(metadata):
+    """Metadata to double percent lines"""
+    text = []
+    if 'title' in metadata:
+        text.append(metadata.pop('title'))
+    if 'cell_depth' in metadata:
+        text.insert(0, '%' * metadata.pop('cell_depth'))
+    if 'cell_type' in metadata:
+        text.append('[{}]'.format(metadata.pop('region_name', metadata.pop('cell_type'))))
+    return metadata_to_text(' '.join(text), metadata, plain_json=True)
 
-    # Fail safe when regexp matching fails #116
-    # (occurs e.g. if square brackets are found in the title)
-    if not matches:
-        return {'title': options.strip()}
 
-    matches = matches[0]
+def incorrectly_encoded_metadata(text):
+    """Encode a text that Jupytext cannot parse as a cell metadata"""
+    return {'incorrectly_encoded_metadata': text}
 
-    # Fifth match are JSON metadata
-    if matches[4]:
-        metadata = json_options_to_metadata(matches[4], add_brackets=False)
-    else:
-        metadata = {}
 
-    # Third match is cell type
-    cell_type = matches[2]
-    if cell_type:
-        metadata['cell_type'] = cell_type[1:-1]
+def isidentifier(text):
+    """Can this text be a proper key?"""
+    return _IDENTIFIER_RE.match(text)
 
-    # Second and fourth match are description
-    title = [matches[i].strip() for i in [1, 3]]
-    title = [part for part in title if part]
-    if title:
-        title = ' '.join(title)
-        cell_depth = 0
-        while title.startswith('%'):
-            cell_depth += 1
-            title = title[1:]
 
-        if cell_depth:
-            metadata['cell_depth'] = cell_depth
-        metadata['title'] = title.strip()
+def is_jupyter_language(language):
+    """Is this a jupyter language?"""
+    for lang in _JUPYTER_LANGUAGES:
+        if language.lower() == lang.lower():
+            return True
+    return False
 
-    return metadata
 
+def parse_key_equal_value(text):
+    """Parse a string of the form 'key1=value1 key2=value2'"""
+    # Empty metadata?
+    text = text.strip()
+    if not text:
+        return {}
 
-def metadata_to_double_percent_options(metadata):
-    """Metadata to double percent lines"""
-    options = []
-    if 'cell_depth' in metadata:
-        options.append('%' * metadata.pop('cell_depth'))
-    if 'title' in metadata:
-        options.append(metadata.pop('title'))
-    if 'cell_type' in metadata:
-        options.append('[{}]'.format(metadata.pop('cell_type')))
-    metadata = metadata_to_json_options(metadata)
-    if metadata != '{}':
-        options.append(metadata)
-    return ' '.join(options)
+    last_space_pos = text.rfind(' ')
+
+    # Just an identifier?
+    if isidentifier(text[last_space_pos + 1:]):
+        key = text[last_space_pos + 1:]
+        value = None
+        result = {key: value}
+        if last_space_pos > 0:
+            result.update(parse_key_equal_value(text[:last_space_pos]))
+        return result
+
+    # Iterate on the '=' signs, starting from the right
+    equal_sign_pos = None
+    while True:
+        equal_sign_pos = text.rfind('=', None, equal_sign_pos)
+        if equal_sign_pos < 0:
+            return incorrectly_encoded_metadata(text)
+
+        # Do we have an identifier on the left of the equal sign?
+        prev_whitespace = text[:equal_sign_pos].rstrip().rfind(' ')
+        key = text[prev_whitespace + 1:equal_sign_pos].strip()
+        if not isidentifier(key.replace('.', '')):
+            continue
+
+        try:
+            value = relax_json_loads(text[equal_sign_pos + 1:])
+        except (ValueError, SyntaxError):
+            # try with a longer expression
+            continue
+
+        # Combine with remaining metadata
+        metadata = parse_key_equal_value(text[:prev_whitespace]) if prev_whitespace > 0 else {}
+
+        # Append our value
+        metadata[key] = value
+
+        # And return
+        return metadata
+
+
+def relax_json_loads(text, catch=False):
+    """Parse a JSON string or similar"""
+    text = text.strip()
+    try:
+        return loads(text)
+    except JSONDecodeError:
+        pass
+
+    if not catch:
+        return ast.literal_eval(text)
+
+    try:
+        return ast.literal_eval(text)
+    except (ValueError, SyntaxError):
+        pass
+
+    return incorrectly_encoded_metadata(text)
+
+
+def text_to_metadata(text, allow_title=False):
+    """Parse the language/cell title and associated metadata"""
+    # Parse the language or cell title = everything before the last blank space before { or =
+    text = text.strip()
+    first_curly_bracket = text.find('{')
+    first_equal_sign = text.find('=')
+
+    if first_curly_bracket < 0 and first_equal_sign < 0 and allow_title:
+        # no metadata
+        return text, {}
+
+    if first_curly_bracket < 0 or (0 <= first_equal_sign < first_curly_bracket):
+        # this is a key=value metadata line
+        if allow_title:
+            prev_whitespace = text[:first_equal_sign].rstrip().rfind(' ')
+        else:
+            prev_whitespace = text.find(' ')
+
+        # single expression
+        if prev_whitespace < 0:
+            # is is a title or a jupyter language?
+            if (allow_title and first_equal_sign < 0) or is_jupyter_language(text):
+                return text, {}
+
+            # else, parse this expression as a key
+            prev_whitespace = 0
+
+        return text[:prev_whitespace].rstrip(), parse_key_equal_value(text[prev_whitespace:])
+
+    # json metadata line
+    return text[:first_curly_bracket].strip(), relax_json_loads(text[first_curly_bracket:], catch=True)
+
+
+def metadata_to_text(language_or_title, metadata=None, plain_json=False):
+    """Write the cell metadata in the format key=value"""
+    # Was metadata the first argument?
+    if metadata is None:
+        metadata, language_or_title = language_or_title, metadata
+
+    metadata = {key: metadata[key] for key in metadata if key not in _JUPYTEXT_CELL_METADATA}
+    text = [language_or_title] if language_or_title else []
+    if language_or_title is None:
+        if 'title' in metadata and '{' not in metadata['title']:
+            text.append(metadata.pop('title'))
+
+    if plain_json:
+        if metadata:
+            text.append(dumps(metadata))
+    else:
+        for key in metadata:
+            if key == 'incorrectly_encoded_metadata':
+                text.append(metadata[key])
+            elif metadata[key] is None:
+                text.append(key)
+            else:
+                text.append('{}={}'.format(key, dumps(metadata[key])))
+    return ' '.join(text)
diff --git a/jupytext/cell_reader.py b/jupytext/cell_reader.py
index 12145ef0..eee96d51 100644
--- a/jupytext/cell_reader.py
+++ b/jupytext/cell_reader.py
@@ -1,7 +1,6 @@
 """Read notebook cells from their text representation"""
 
 import re
-import json
 from copy import copy
 from nbformat.v4.nbbase import new_code_cell, new_raw_cell, new_markdown_cell
 from .languages import _SCRIPT_EXTENSIONS
@@ -12,8 +11,8 @@ try:
 except (ImportError, SyntaxError):
     rst2md = None
 
-from .cell_metadata import is_active, json_options_to_metadata, md_options_to_metadata, rmd_options_to_metadata, \
-    double_percent_options_to_metadata
+from .cell_metadata import is_active, text_to_metadata, rmd_options_to_metadata
+from .languages import _JUPYTER_LANGUAGES
 from .stringparser import StringParser
 from .magics import uncomment_magic, is_magic, unescape_code_start
 from .pep8 import pep8_lines_between_cells
@@ -193,19 +192,22 @@ class BaseCellReader(object):
         else:
             self.content = self.uncomment_code_and_magics(source)
 
-        # Is this a raw cell?
-        if ('active' in self.metadata and not is_active('ipynb', self.metadata)) or \
-                (self.ext in ['.md', '.markdown'] and self.cell_type == 'code' and self.language is None):
-            if self.metadata.get('active') == '':
-                del self.metadata['active']
-            # Is this a Jupytext document in the Markdown format >= 1.2 ?
-            if self.ext in ['.md', '.markdown'] and self.format_version not in ['1.0', '1.1']:
-                self.cell_type = 'markdown'
-                self.explicit_eoc = False
-                cell_end_marker += 1
-                self.content = lines[:cell_end_marker]
-            else:
+        # Is this an inactive cell?
+        if self.cell_type == 'code':
+            if not is_active('.ipynb', self.metadata):
+                if self.metadata.get('active') == '':
+                    del self.metadata['active']
                 self.cell_type = 'raw'
+            elif self.ext in ['.md', '.markdown'] and not self.language:
+                # Markdown files in version >= 1.3 represent code chunks with no language as Markdown cells
+                if self.format_version not in ['1.0', '1.1']:
+                    self.cell_type = 'markdown'
+                    self.explicit_eoc = False
+                    cell_end_marker += 1
+                    self.content = lines[:cell_end_marker]
+                # Previous versions mapped those to raw cells
+                else:
+                    self.cell_type = 'raw'
 
         # Explicit end of cell marker?
         if (next_cell_start + 1 < len(lines) and
@@ -234,13 +236,12 @@ class BaseCellReader(object):
 class MarkdownCellReader(BaseCellReader):
     """Read notebook cells from Markdown documents"""
     comment = ''
-    start_code_re = re.compile(r"^```(.*)")
-    non_jupyter_code_re = re.compile(r"^```\{")
+    start_code_re = re.compile(r"^```({})(.*)".format('|'.join(
+        _JUPYTER_LANGUAGES + [str.upper(lang) for lang in _JUPYTER_LANGUAGES]).replace('+', '\\+')))
+    non_jupyter_code_re = re.compile(r"^```")
     end_code_re = re.compile(r"^```\s*$")
-    start_region_re = re.compile(r"^<!--\s*#region(.*)-->\s*$")
-    end_region_re = re.compile(r"^<!--\s*#endregion\s*-->\s*$")
-    start_raw_re = re.compile(r"^<!--\s*#raw(.*)-->\s*$")
-    end_raw_re = re.compile(r"^<!--\s*#endraw\s*-->\s*$")
+    start_region_re = re.compile(r"^<!--\s*#(region|markdown|md|raw)(.*)-->\s*$")
+    end_region_re = None
     default_comment_magics = False
 
     def __init__(self, fmt=None, default_language=None):
@@ -248,49 +249,40 @@ class MarkdownCellReader(BaseCellReader):
         self.split_at_heading = (fmt or {}).get('split_at_heading', False)
         self.in_region = False
         self.in_raw = False
+        if self.format_version in ['1.0', '1.1']:
+            self.start_code_re = re.compile(r"^```(.*)")
 
     def metadata_and_language_from_option_line(self, line):
-        region = self.start_region_re.match(line)
-        raw = self.start_raw_re.match(line)
-        if region or raw:
-            if region:
-                self.in_region = True
-            else:
-                self.in_raw = True
-                region = raw
-            options = region.groups()[0].strip()
-            if options:
-                start = options.find('{')
-                if start >= 0:
-                    title = options[:start].strip()
-                    options = options[start:]
-                else:
-                    title = options.strip()
-                    options = "{}"
-                self.metadata = json.loads(options)
-                if title:
-                    self.metadata['title'] = title
+        match_region = self.start_region_re.match(line)
+        if match_region:
+            self.in_region = True
+            groups = match_region.groups()
+            region_name = groups[0]
+            self.end_region_re = re.compile(r"^<!--\s*#end{}\s*-->\s*$".format(region_name))
+            title, self.metadata = text_to_metadata(groups[1], allow_title=True)
+            if region_name == 'raw':
+                self.cell_type = 'raw'
             else:
-                self.metadata = {}
+                self.cell_type = 'markdown'
+            if title:
+                self.metadata['title'] = title
+            if region_name in ['markdown', 'md']:
+                self.metadata['region_name'] = region_name
         elif self.start_code_re.match(line):
             self.language, self.metadata = self.options_to_metadata(self.start_code_re.findall(line)[0])
 
     def options_to_metadata(self, options):
-        return md_options_to_metadata(options)
+        if isinstance(options, tuple):
+            options = ' '.join(options)
+        return text_to_metadata(options)
 
     def find_cell_end(self, lines):
         """Return position of end of cell marker, and position
         of first line after cell"""
         if self.in_region:
-            self.cell_type = 'markdown'
             for i, line in enumerate(lines):
                 if self.end_region_re.match(line):
                     return i, i + 1, True
-        if self.in_raw:
-            self.cell_type = 'raw'
-            for i, line in enumerate(lines):
-                if self.end_raw_re.match(line):
-                    return i, i + 1, True
         elif self.metadata is None:
             # default markdown: (last) two consecutive blank lines, except when in code blocks
             self.cell_type = 'markdown'
@@ -303,11 +295,6 @@ class MarkdownCellReader(BaseCellReader):
                     in_explicit_code_block = False
                     continue
 
-                if self.non_jupyter_code_re and self.non_jupyter_code_re.match(line):
-                    in_explicit_code_block = True
-                    prev_blank = 0
-                    continue
-
                 if prev_blank and line.startswith('    ') and not _BLANK_LINE.match(line):
                     in_indented_code_block = True
                     prev_blank = 0
@@ -319,11 +306,16 @@ class MarkdownCellReader(BaseCellReader):
                 if in_indented_code_block or in_explicit_code_block:
                     continue
 
-                if self.start_code_re.match(line) or self.start_region_re.match(line) or self.start_raw_re.match(line):
+                if self.start_code_re.match(line) or self.start_region_re.match(line):
                     if i > 1 and prev_blank:
                         return i - 1, i, False
                     return i, i, False
 
+                if self.non_jupyter_code_re and self.non_jupyter_code_re.match(line):
+                    in_explicit_code_block = True
+                    prev_blank = 0
+                    continue
+
                 if self.split_at_heading and line.startswith('#') and prev_blank >= 1:
                     return i - 1, i, False
 
@@ -363,7 +355,7 @@ class RMarkdownCellReader(MarkdownCellReader):
         return rmd_options_to_metadata(options)
 
     def uncomment_code_and_magics(self, lines):
-        if self.cell_type == 'code' and self.comment_magics and is_active('.Rmd', self.metadata):
+        if self.cell_type == 'code' and self.comment_magics and is_active(self.ext, self.metadata):
             uncomment_magic(lines, self.language or self.default_language)
 
         return lines
@@ -469,25 +461,14 @@ class LightScriptCellReader(ScriptCellReader):
         self.explicit_end_marker_required = False
         if fmt and 'cell_markers' in fmt and fmt['cell_markers'] != '+,-':
             self.cell_marker_start, self.cell_marker_end = fmt['cell_markers'].split(',', 1)
-            self.start_code_re = re.compile('^' + self.comment + r'\s*' + self.cell_marker_start + r'\s*(.*)$')
+            self.start_code_re = re.compile('^' + self.comment + r'\s*' + self.cell_marker_start + r'(.*)$')
             self.end_code_re = re.compile('^' + self.comment + r'\s*' + self.cell_marker_end + r'\s*$')
         else:
-            self.start_code_re = re.compile('^' + self.comment + r'\s*\+\s*{(.*)}$')
-            self.simple_start_code_re = re.compile('^' + self.comment + r'\s*\+\s*$')
+            self.start_code_re = re.compile('^' + self.comment + r'\s*\+(.*)$')
 
     def options_to_metadata(self, options):
-        if not self.cell_marker_start:
-            return json_options_to_metadata(options)
-
-        bracket = options.find('{')
-        if bracket < 0:
-            title = options
-            metadata = {}
-        else:
-            title = options[:bracket]
-            metadata = json_options_to_metadata(options[bracket:], False)
+        title, metadata = text_to_metadata(options, allow_title=True)
 
-        title = title.strip()
         if title:
             metadata['title'] = title
 
@@ -603,7 +584,33 @@ class DoublePercentScriptCellReader(ScriptCellReader):
             self.metadata = {}
 
     def options_to_metadata(self, options):
-        return None, double_percent_options_to_metadata(options)
+        title, metadata = text_to_metadata(options, allow_title=True)
+
+        # Cell type
+        for cell_type in ['markdown', 'raw', 'md']:
+            code = '[{}]'.format(cell_type)
+            if code in title:
+                title = title.replace(code, '').strip()
+                metadata['cell_type'] = cell_type
+                if cell_type == 'md':
+                    metadata['region_name'] = cell_type
+                    metadata['cell_type'] = 'markdown'
+                break
+
+        # Spyder has sub cells
+        cell_depth = 0
+        while title.startswith('%'):
+            cell_depth += 1
+            title = title[1:]
+
+        if cell_depth:
+            metadata['cell_depth'] = cell_depth
+            title = title.strip()
+
+        if title:
+            metadata['title'] = title
+
+        return None, metadata
 
     def find_cell_content(self, lines):
         """Parse cell till its end and set content, lines_to_next_cell.
@@ -620,7 +627,7 @@ class DoublePercentScriptCellReader(ScriptCellReader):
         source = lines[cell_start:cell_end_marker]
         self.org_content = copy(source)
 
-        if self.cell_type != 'code' or (self.metadata and not is_active('py', self.metadata)) \
+        if self.cell_type != 'code' or (self.metadata and not is_active(self.ext, self.metadata)) \
                 or (self.language is not None and self.language != self.default_language):
             if self.ext == '.py' and self.cell_type != 'code' and self.org_content \
                     and self.org_content[0].lstrip().startswith(('"""', "'''")):
diff --git a/jupytext/cell_to_text.py b/jupytext/cell_to_text.py
index ba7bd3f0..00440dcd 100644
--- a/jupytext/cell_to_text.py
+++ b/jupytext/cell_to_text.py
@@ -1,13 +1,11 @@
 """Export notebook cells as text"""
 
 import re
-import json
 import warnings
 from copy import copy
 from .languages import cell_language, comment_lines
 from .cell_metadata import is_active, _IGNORE_CELL_METADATA
-from .cell_metadata import metadata_to_md_options, metadata_to_rmd_options
-from .cell_metadata import metadata_to_json_options, metadata_to_double_percent_options
+from .cell_metadata import metadata_to_text, metadata_to_rmd_options, metadata_to_double_percent_options
 from .metadata_filter import filter_metadata
 from .magics import comment_magic, escape_code_start
 from .cell_reader import LightScriptCellReader, MarkdownCellReader, RMarkdownCellReader
@@ -115,13 +113,9 @@ class MarkdownCellExporter(BaseCellExporter):
         self.comment = ''
 
     def html_comment(self, metadata, code='region'):
-        """Encore the source text in an HTML region"""
+        """Protect a Markdown or Raw cell with HTML comments"""
         if metadata:
-            region_start = ['<!-- #' + code]
-            if 'title' in metadata and '{' not in metadata['title']:
-                region_start.append(metadata.pop('title'))
-            region_start.append(json.dumps(metadata))
-            region_start.append('-->')
+            region_start = ['<!-- #' + code, metadata_to_text(metadata, plain_json=True), '-->']
             region_start = ' '.join(region_start)
         else:
             region_start = '<!-- #{} -->'.format(code)
@@ -133,7 +127,7 @@ class MarkdownCellExporter(BaseCellExporter):
         if self.cell_type == 'markdown':
             # Is an explicit region required?
             if self.metadata or self.cell_reader(self.fmt).read(self.source)[1] < len(self.source):
-                return self.html_comment(self.metadata)
+                return self.html_comment(self.metadata, self.metadata.pop('region_name', 'region'))
             return self.source
 
         return self.code_to_text()
@@ -143,20 +137,15 @@ class MarkdownCellExporter(BaseCellExporter):
         source = copy(self.source)
         comment_magic(source, self.language, self.comment_magics)
 
-        options = []
-        if self.cell_type == 'code' and self.language:
-            options.append(self.language)
+        if self.metadata.get('active') == '':
+            self.metadata.pop('active')
 
-        filtered_metadata = {key: self.metadata[key] for key in self.metadata
-                             if key not in ['active', 'language']}
+        self.language = self.metadata.pop('language', self.language)
+        if self.cell_type == 'raw' and not is_active(self.ext, self.metadata, False):
+            return self.html_comment(self.metadata, 'raw')
 
-        if filtered_metadata:
-            options.append(metadata_to_md_options(filtered_metadata))
-
-        if self.cell_type == 'raw':
-            return self.html_comment(filtered_metadata, 'raw')
-
-        return ['```{}'.format(' '.join(options))] + source + ['```']
+        options = metadata_to_text(self.language, self.metadata)
+        return ['```' + options] + source + ['```']
 
 
 class RMarkdownCellExporter(MarkdownCellExporter):
@@ -178,7 +167,7 @@ class RMarkdownCellExporter(MarkdownCellExporter):
             comment_magic(source, self.language, self.comment_magics)
 
         lines = []
-        if not is_active('Rmd', self.metadata):
+        if not is_active(self.ext, self.metadata):
             self.metadata['eval'] = False
         options = metadata_to_rmd_options(self.language, self.metadata)
         lines.append('```{{{}}}'.format(options))
@@ -228,10 +217,7 @@ class LightScriptCellExporter(BaseCellExporter):
 
     def code_to_text(self):
         """Return the text representation of a code cell"""
-        active = is_active(self.ext, self.metadata)
-        if self.language != self.default_language and 'active' not in self.metadata:
-            active = False
-
+        active = is_active(self.ext, self.metadata, self.language == self.default_language)
         source = copy(self.source)
         escape_code_start(source, self.ext, self.language)
 
@@ -252,14 +238,11 @@ class LightScriptCellExporter(BaseCellExporter):
             del self.metadata['endofcell']
 
         cell_start = [self.comment, self.cell_marker_start or '+']
-        if not self.cell_marker_start:
-            cell_start.append(metadata_to_json_options(self.metadata))
-        elif self.metadata:
-            if 'title' in self.metadata:
-                cell_start.append(self.metadata.pop('title'))
-            if self.metadata:
-                cell_start.append(metadata_to_json_options(self.metadata))
-
+        cell_metadata = metadata_to_text(self.metadata, plain_json=True)
+        if cell_metadata:
+            cell_start.append(cell_metadata)
+        elif not self.cell_marker_start:
+            cell_start.append('{}')
         lines.append(' '.join(cell_start))
         lines.extend(source)
         lines.append(self.comment + ' {}'.format(endofcell))
@@ -348,7 +331,7 @@ class RScriptCellExporter(BaseCellExporter):
             source = ['# ' + line if line else '#' for line in source]
 
         lines = []
-        if not is_active('R', self.metadata):
+        if not is_active(self.ext, self.metadata):
             self.metadata['eval'] = False
         options = metadata_to_rmd_options(None, self.metadata)
         if options:
@@ -371,9 +354,7 @@ class DoublePercentCellExporter(BaseCellExporter):  # pylint: disable=W0223
         if self.cell_type != 'code':
             self.metadata['cell_type'] = self.cell_type
 
-        active = is_active('py', self.metadata)
-        if self.language != self.default_language and 'active' not in self.metadata:
-            active = False
+        active = is_active(self.ext, self.metadata, self.language == self.default_language)
         if self.cell_type == 'raw' and 'active' in self.metadata and self.metadata['active'] == '':
             del self.metadata['active']
 
diff --git a/jupytext/compare.py b/jupytext/compare.py
index 260f31e4..280d62e4 100644
--- a/jupytext/compare.py
+++ b/jupytext/compare.py
@@ -85,12 +85,8 @@ def same_content(ref_source, test_source, allow_removed_final_blank_line):
     return _BLANK_LINE.match(ref_source[-1])
 
 
-def compare_notebooks(notebook_expected,
-                      notebook_actual,
-                      fmt=None,
-                      allow_expected_differences=True,
-                      raise_on_first_difference=True,
-                      compare_outputs=False):
+def compare_notebooks(notebook_actual, notebook_expected, fmt=None, allow_expected_differences=True,
+                      raise_on_first_difference=True, compare_outputs=False):
     """Compare the two notebooks, and raise with a meaningful message
     that explains the differences, if any"""
     fmt = long_form_one_format(fmt)
@@ -245,5 +241,5 @@ def test_round_trip_conversion(notebook, fmt, update, allow_expected_differences
     if update:
         combine_inputs_with_outputs(round_trip, notebook, fmt)
 
-    compare_notebooks(notebook, round_trip, fmt, allow_expected_differences,
+    compare_notebooks(round_trip, notebook, fmt, allow_expected_differences,
                       raise_on_first_difference=stop_on_first_error)
diff --git a/jupytext/jupytext.py b/jupytext/jupytext.py
index 847c10ed..f082526b 100644
--- a/jupytext/jupytext.py
+++ b/jupytext/jupytext.py
@@ -169,8 +169,8 @@ class TextNotebookConverter(NotebookReader, NotebookWriter):
             # two blank lines between markdown cells in Rmd when those do not have explicit region markers
             if self.ext in ['.md', '.markdown', '.Rmd'] and not cell.is_code():
                 if (i + 1 < len(cell_exporters) and not cell_exporters[i + 1].is_code() and
-                        not texts[i][0].startswith('<!-- #region') and
-                        not texts[i + 1][0].startswith('<!-- #region') and
+                        not texts[i][0].startswith('<!-- #') and
+                        not texts[i + 1][0].startswith('<!-- #') and
                         not texts[i][0].startswith('```') and
                         not texts[i + 1][0].startswith('```') and
                         (not split_at_heading or not (texts[i + 1] and texts[i + 1][0].startswith('#')))):


Test patch:
diff --git a/tests/notebooks/md/jupytext_markdown.md b/tests/notebooks/md/jupytext_markdown.md
index 35e78ae9..8ffbaaa4 100644
--- a/tests/notebooks/md/jupytext_markdown.md
+++ b/tests/notebooks/md/jupytext_markdown.md
@@ -33,10 +33,10 @@ Indented code is accepted, and consecutive blank lines there do not break Markdo
 
 ## Raw cells
 
-Raw cells starts with ` ``` `, like here:
-```
-THIS IS A RAW CELL
-```
+Raw cells are delimited with `<!-- #raw -->` and <!-- #endraw -->, like here:
+<!-- #raw -->
+this is a raw cell
+<!-- #endraw -->
 
 # Protected Markdown cells
 
@@ -62,7 +62,7 @@ Metadata are supported for all cell types.
 A cell with a title and additional metadata.
 <!-- #endregion -->
 
-## Code and raw cells
+## Code cells
 
 ```python tags=["parameters"]
 a = 2
diff --git a/tests/notebooks/mirror/md_to_ipynb/jupytext_markdown.ipynb b/tests/notebooks/mirror/md_to_ipynb/jupytext_markdown.ipynb
index 329fe9f8..633517fc 100644
--- a/tests/notebooks/mirror/md_to_ipynb/jupytext_markdown.ipynb
+++ b/tests/notebooks/mirror/md_to_ipynb/jupytext_markdown.ipynb
@@ -43,16 +43,14 @@
    "source": [
     "## Raw cells\n",
     "\n",
-    "Raw cells starts with ` ``` `, like here:"
+    "Raw cells are delimited with `<!-- #raw -->` and <!-- #endraw -->, like here:"
    ]
   },
   {
-   "cell_type": "markdown",
+   "cell_type": "raw",
    "metadata": {},
    "source": [
-    "```\n",
-    "THIS IS A RAW CELL\n",
-    "```"
+    "this is a raw cell"
    ]
   },
   {
@@ -102,7 +100,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## Code and raw cells"
+    "## Code cells"
    ]
   },
   {
diff --git a/tests/notebooks/mirror/md_to_ipynb/plain_markdown.ipynb b/tests/notebooks/mirror/md_to_ipynb/plain_markdown.ipynb
index 2ba73714..5cbe4a4a 100644
--- a/tests/notebooks/mirror/md_to_ipynb/plain_markdown.ipynb
+++ b/tests/notebooks/mirror/md_to_ipynb/plain_markdown.ipynb
@@ -44,28 +44,15 @@
     "1 + 1"
    ]
   },
-  {
-   "cell_type": "markdown",
-   "metadata": {
-    "lines_to_next_cell": 0
-   },
-   "source": [
-    "Code snippet without an explicit language"
-   ]
-  },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
+    "Code snippet without an explicit language\n",
     "```\n",
     "echo 'Hello world'\n",
-    "```"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
+    "```\n",
+    "\n",
     "Markdown comments\n",
     "<!-- #comment -->\n",
     "\n",
diff --git a/tests/test_active_cells.py b/tests/test_active_cells.py
index 71e956eb..ee11da1f 100644
--- a/tests/test_active_cells.py
+++ b/tests/test_active_cells.py
@@ -17,7 +17,13 @@ HEADER = {'.py': '''# ---
 # ---
 
 ''',
+          '.md': '''---
+jupyter:
+  jupytext:
+    main_language: python
+---
 
+''',
           '.Rmd': '''---
 jupyter:
   jupytext:
@@ -32,6 +38,10 @@ ACTIVE_ALL = {'.py': """# + {"active": "ipynb,py,R,Rmd"}
               '.Rmd': """```{python active="ipynb,py,R,Rmd"}
 # This cell is active in all extensions
 ```
+""",
+              '.md': """```python active="ipynb,py,R,Rmd"
+# This cell is active in all extensions
+```
 """,
               '.R': """# + {"active": "ipynb,py,R,Rmd"}
 # This cell is active in all extensions
@@ -43,7 +53,7 @@ ACTIVE_ALL = {'.py': """# + {"active": "ipynb,py,R,Rmd"}
                          'outputs': []}}
 
 
-@pytest.mark.parametrize('ext', ['.Rmd', '.py', '.R'])
+@pytest.mark.parametrize('ext', ['.Rmd', '.md', '.py', '.R'])
 def test_active_all(ext, no_jupytext_version_number):
     nb = jupytext.reads(HEADER[ext] + ACTIVE_ALL[ext], ext)
     assert len(nb.cells) == 1
@@ -59,6 +69,11 @@ ACTIVE_IPYNB = {'.py': """# + {"active": "ipynb"}
 # This cell is active only in ipynb
 %matplotlib inline
 ```
+""",
+                '.md': """```python active="ipynb"
+# This cell is active only in ipynb
+%matplotlib inline
+```
 """,
                 '.R': """# + {"active": "ipynb"}
 # # This cell is active only in ipynb
@@ -73,7 +88,7 @@ ACTIVE_IPYNB = {'.py': """# + {"active": "ipynb"}
 
 
 @skip_if_dict_is_not_ordered
-@pytest.mark.parametrize('ext', ['.Rmd', '.py', '.R'])
+@pytest.mark.parametrize('ext', ['.Rmd', '.md', '.py', '.R'])
 def test_active_ipynb(ext, no_jupytext_version_number):
     nb = jupytext.reads(HEADER[ext] + ACTIVE_IPYNB[ext], ext)
     assert len(nb.cells) == 1
@@ -89,6 +104,11 @@ ACTIVE_IPYNB_RMD_USING_TAG = {'.py': """# + {"tags": ["active-ipynb-Rmd"]}
 # This cell is active only in ipynb and Rmd
 # %matplotlib inline
 ```
+""",
+                              '.md': """```python tags=["active-ipynb-Rmd"]
+# This cell is active only in ipynb and Rmd
+%matplotlib inline
+```
 """,
                               '.R': """# + {"tags": ["active-ipynb-Rmd"]}
 # # This cell is active only in ipynb and Rmd
@@ -103,7 +123,7 @@ ACTIVE_IPYNB_RMD_USING_TAG = {'.py': """# + {"tags": ["active-ipynb-Rmd"]}
 
 
 @skip_if_dict_is_not_ordered
-@pytest.mark.parametrize('ext', ['.Rmd', '.py', '.R'])
+@pytest.mark.parametrize('ext', ['.Rmd', '.md', '.py', '.R'])
 def test_active_ipynb_rmd_using_tags(ext, no_jupytext_version_number):
     nb = jupytext.reads(HEADER[ext] + ACTIVE_IPYNB_RMD_USING_TAG[ext], ext)
     assert len(nb.cells) == 1
@@ -137,6 +157,10 @@ ACTIVE_PY_IPYNB = {'.py': """# + {"active": "ipynb,py"}
                    '.Rmd': """```{python active="ipynb,py", eval=FALSE}
 # This cell is active in py and ipynb extensions
 ```
+""",
+                   '.md': """```python active="ipynb,py"
+# This cell is active in py and ipynb extensions
+```
 """,
                    '.R': """# + {"active": "ipynb,py"}
 # # This cell is active in py and ipynb extensions
@@ -150,7 +174,7 @@ ACTIVE_PY_IPYNB = {'.py': """# + {"active": "ipynb,py"}
 
 
 @skip_if_dict_is_not_ordered
-@pytest.mark.parametrize('ext', ['.Rmd', '.py', '.R'])
+@pytest.mark.parametrize('ext', ['.Rmd', '.md', '.py', '.R'])
 def test_active_py_ipynb(ext, no_jupytext_version_number):
     nb = jupytext.reads(HEADER[ext] + ACTIVE_PY_IPYNB[ext], ext)
     assert len(nb.cells) == 1
diff --git a/tests/test_cell_metadata.py b/tests/test_cell_metadata.py
index fb1ad142..1668dc3d 100644
--- a/tests/test_cell_metadata.py
+++ b/tests/test_cell_metadata.py
@@ -2,8 +2,7 @@ import pytest
 from jupytext.compare import compare
 from jupytext.cell_metadata import rmd_options_to_metadata, metadata_to_rmd_options, parse_rmd_options
 from jupytext.cell_metadata import _IGNORE_CELL_METADATA, RMarkdownOptionParsingError, try_eval_metadata
-from jupytext.cell_metadata import json_options_to_metadata, metadata_to_json_options
-from jupytext.cell_metadata import md_options_to_metadata, metadata_to_md_options
+from jupytext.cell_metadata import text_to_metadata, metadata_to_text
 from jupytext.metadata_filter import filter_metadata
 from .utils import skip_if_dict_is_not_ordered
 
@@ -81,30 +80,75 @@ def test_try_eval_metadata():
     assert metadata == {'list': ['a', 5], 'c': [1, 2, 3]}
 
 
-def test_parse_wrong_json():
-    assert json_options_to_metadata("""{"key":'incorrect value'}""") == {}
+pytestmark = skip_if_dict_is_not_ordered
 
 
-def test_parse_md_options():
-    assert md_options_to_metadata('python') == ('python', {})
-    assert md_options_to_metadata('not_a_language') == (None, {'not_a_language': None})
+def test_language_no_metadata(text='python', value=('python', {})):
+    compare(text_to_metadata(text), value)
+    assert metadata_to_text(*value) == text.strip()
 
 
-def test_round_trip_md_options(metadata={'.class': None,
-                                         'long-and-str$ange.name': None,
-                                         'string': "Hello",
-                                         'number': .21,
-                                         'array': ['First', 'Second', 3, "string with single ' in it'"],
-                                         'dict': {"a": 5, "b": [1.2, "four"]},
-                                         '.another_class': None}):
-    options = metadata_to_md_options(metadata)
-    language, metadata2 = md_options_to_metadata('python ' + options)
-    assert language == 'python'
-    compare(metadata, metadata2)
+def test_only_metadata(text='key="value"', value=('', {'key': 'value'})):
+    compare(text_to_metadata(text), value)
+    assert metadata_to_text(*value) == text
 
 
-def test_write_parse_json():
-    metadata = {"tags": ["parameters"]}
-    options = metadata_to_json_options(metadata)
-    metadata2 = json_options_to_metadata(options, add_brackets=False)
-    assert metadata == metadata2
+def test_only_metadata_2(text='key="value"', value=('', {'key': 'value'})):
+    compare(text_to_metadata(text, allow_title=True), value)
+    assert metadata_to_text(*value) == text
+
+
+def test_no_language(text='.class', value=('', {'.class': None})):
+    compare(text_to_metadata(text), value)
+    assert metadata_to_text(*value) == text
+
+
+def test_language_metadata_no_space(text='python{"a":1}', value=('python', {'a': 1})):
+    compare(text_to_metadata(text), value)
+    assert metadata_to_text(*value) == 'python a=1'
+
+
+def test_title_no_metadata(text='title', value=('title', {})):
+    compare(text_to_metadata(text, allow_title=True), value)
+    assert metadata_to_text(*value) == text.strip()
+
+
+def test_simple_metadata(text='python string="value" number=1.0 array=["a", "b"]',
+                         value=('python', {'string': 'value', 'number': 1.0, 'array': ['a', 'b']})):
+    compare(text_to_metadata(text), value)
+    assert metadata_to_text(*value) == text
+
+
+def test_simple_metadata_with_spaces(text='python string = "value" number = 1.0 array = ["a",  "b"]',
+                                     value=('python', {'string': 'value', 'number': 1.0, 'array': ['a', 'b']})):
+    compare(text_to_metadata(text), value)
+    assert metadata_to_text(*value) == 'python string="value" number=1.0 array=["a", "b"]'
+
+
+def test_title_and_relax_json(text="cell title string='value' number=1.0 array=['a', \"b\"]",
+                              value=('cell title', {'string': 'value', 'number': 1.0, 'array': ['a', 'b']})):
+    compare(text_to_metadata(text, allow_title=True), value)
+    assert metadata_to_text(*value) == 'cell title string="value" number=1.0 array=["a", "b"]'
+
+
+def test_title_and_json_dict(text='cell title {"string": "value", "number": 1.0, "array": ["a", "b"]}',
+                             value=('cell title', {'string': 'value', 'number': 1.0, 'array': ['a', 'b']})):
+    compare(text_to_metadata(text, allow_title=True), value)
+    assert metadata_to_text(*value) == 'cell title string="value" number=1.0 array=["a", "b"]'
+
+
+def test_tags(text="python .class",
+              value=('python', {'.class': None})):
+    compare(text_to_metadata(text), value)
+    assert metadata_to_text(*value) == text
+
+
+def test_values_with_equal_signs_inside(text='python string="value=5"',
+                                        value=('python', {'string': 'value=5'})):
+    compare(text_to_metadata(text), value)
+    assert metadata_to_text(*value) == text
+
+
+def test_incorrectly_encoded(text="this is an incorrect expression d={{4 b=3"):
+    value = text_to_metadata(text, allow_title=True)
+    assert metadata_to_text(*value) == text
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 05acf7fb..6ec83d5d 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -60,7 +60,7 @@ def test_convert_single_file_in_place(nb_file, tmpdir):
     nb1 = read(nb_org)
     nb2 = read(nb_other)
 
-    compare_notebooks(nb1, nb2)
+    compare_notebooks(nb2, nb1)
 
 
 @pytest.mark.parametrize('nb_file', list_notebooks('ipynb') + list_notebooks('Rmd'))
@@ -137,7 +137,7 @@ def test_convert_multiple_file(nb_files, tmpdir):
     for nb_org, nb_other in zip(nb_orgs, nb_others):
         nb1 = read(nb_org)
         nb2 = read(nb_other)
-        compare_notebooks(nb1, nb2)
+        compare_notebooks(nb2, nb1)
 
 
 def test_error_not_notebook_ext_input(tmpdir, capsys):
@@ -291,7 +291,7 @@ def test_convert_to_percent_format(nb_file, tmpdir):
     nb1 = read(tmp_ipynb)
     nb2 = read(tmp_nbpy)
 
-    compare_notebooks(nb1, nb2)
+    compare_notebooks(nb2, nb1)
 
 
 @pytest.mark.parametrize('nb_file', list_notebooks('ipynb_py'))
@@ -312,7 +312,7 @@ def test_convert_to_percent_format_and_keep_magics(nb_file, tmpdir):
     nb1 = read(tmp_ipynb)
     nb2 = read(tmp_nbpy)
 
-    compare_notebooks(nb1, nb2)
+    compare_notebooks(nb2, nb1)
 
 
 def git_in_tmpdir(tmpdir):
@@ -617,22 +617,22 @@ def test_sync(nb_file, tmpdir, capsys):
     jupytext(['--sync', tmp_ipynb])
 
     assert os.path.isfile(tmp_py)
-    compare_notebooks(nb, read(tmp_py))
+    compare_notebooks(read(tmp_py), nb)
 
     assert os.path.isfile(tmp_rmd)
-    compare_notebooks(nb, read(tmp_rmd), 'Rmd')
+    compare_notebooks(read(tmp_rmd), nb, 'Rmd')
 
     write(nb, tmp_rmd, 'Rmd')
     jupytext(['--sync', tmp_ipynb])
 
     nb2 = read(tmp_ipynb)
-    compare_notebooks(nb, nb2, 'Rmd', compare_outputs=True)
+    compare_notebooks(nb2, nb, 'Rmd', compare_outputs=True)
 
     write(nb, tmp_py, 'py')
     jupytext(['--sync', tmp_ipynb])
 
     nb2 = read(tmp_ipynb)
-    compare_notebooks(nb, nb2, compare_outputs=True)
+    compare_notebooks(nb2, nb, compare_outputs=True)
 
     # Finally we recreate the ipynb
     os.remove(tmp_ipynb)
@@ -641,7 +641,7 @@ def test_sync(nb_file, tmpdir, capsys):
     jupytext(['--sync', tmp_py])
 
     nb2 = read(tmp_ipynb)
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
     # ipynb must be older than py file, otherwise our Contents Manager will complain
     assert os.path.getmtime(tmp_ipynb) <= os.path.getmtime(tmp_py)
@@ -668,7 +668,7 @@ def test_sync_pandoc(nb_file, tmpdir, capsys):
     jupytext(['--sync', tmp_ipynb])
 
     assert os.path.isfile(tmp_md)
-    compare_notebooks(nb, read(tmp_md), 'md:pandoc')
+    compare_notebooks(read(tmp_md), nb, 'md:pandoc')
 
     with open(tmp_md) as fp:
         assert 'pandoc' in fp.read()
@@ -687,13 +687,13 @@ def test_cli_can_infer_jupytext_format(nb_file, ext, tmpdir):
     write(nb, tmp_text)
     jupytext(['--to', 'notebook', tmp_text])
     nb2 = read(tmp_ipynb)
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
     # Percent format to Jupyter notebook
     write(nb, tmp_text, ext + ':percent')
     jupytext(['--to', 'notebook', tmp_text])
     nb2 = read(tmp_ipynb)
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
 
 @pytest.mark.parametrize('nb_file,ext',
@@ -707,7 +707,7 @@ def test_cli_to_script(nb_file, ext, tmpdir):
     write(nb, tmp_ipynb)
     jupytext(['--to', 'script', tmp_ipynb])
     nb2 = read(tmp_text)
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
 
 @pytest.mark.parametrize('nb_file,ext',
@@ -721,7 +721,7 @@ def test_cli_to_auto(nb_file, ext, tmpdir):
     write(nb, tmp_ipynb)
     jupytext(['--to', 'auto', tmp_ipynb])
     nb2 = read(tmp_text)
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
 
 @pytest.mark.parametrize('nb_file', list_notebooks('ipynb_py'))
@@ -735,25 +735,25 @@ def test_cli_can_infer_jupytext_format_from_stdin(nb_file, tmpdir):
     with open(nb_file) as fp, mock.patch('sys.stdin', fp):
         jupytext(['--to', 'py:percent', '-o', tmp_py])
     nb2 = read(tmp_py)
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
     # read python notebook on stdin, write to ipynb
     with open(tmp_py) as fp, mock.patch('sys.stdin', fp):
         jupytext(['-o', tmp_ipynb])
     nb2 = read(tmp_ipynb)
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
     # read ipynb notebook on stdin, write to R markdown
     with open(nb_file) as fp, mock.patch('sys.stdin', fp):
         jupytext(['-o', tmp_rmd])
     nb2 = read(tmp_rmd)
-    compare_notebooks(nb, nb2, 'Rmd')
+    compare_notebooks(nb2, nb, 'Rmd')
 
     # read markdown notebook on stdin, write to ipynb
     with open(tmp_rmd) as fp, mock.patch('sys.stdin', fp):
         jupytext(['-o', tmp_ipynb])
     nb2 = read(tmp_ipynb)
-    compare_notebooks(nb, nb2, 'Rmd')
+    compare_notebooks(nb2, nb, 'Rmd')
 
 
 def test_set_kernel_works_with_pipes_326(capsys):
diff --git a/tests/test_compare.py b/tests/test_compare.py
index af600ac5..59a3d34c 100644
--- a/tests/test_compare.py
+++ b/tests/test_compare.py
@@ -92,7 +92,7 @@ def test_raise_on_different_metadata():
     test = new_notebook(metadata={'kernelspec': {'language': 'R', 'name': 'R', 'display_name': 'R'}},
                         cells=[new_markdown_cell('Cell one')])
     with pytest.raises(NotebookDifference):
-        compare_notebooks(ref, test, 'md')
+        compare_notebooks(test, ref, 'md')
 
 
 @pytest.mark.parametrize('raise_on_first_difference', [True, False])
@@ -100,7 +100,7 @@ def test_raise_on_different_cell_type(raise_on_first_difference):
     ref = new_notebook(cells=[new_markdown_cell('Cell one'), new_code_cell('Cell two')])
     test = new_notebook(cells=[new_markdown_cell('Cell one'), new_raw_cell('Cell two')])
     with pytest.raises(NotebookDifference):
-        compare_notebooks(ref, test, 'md', raise_on_first_difference=raise_on_first_difference)
+        compare_notebooks(test, ref, 'md', raise_on_first_difference=raise_on_first_difference)
 
 
 @pytest.mark.parametrize('raise_on_first_difference', [True, False])
@@ -108,14 +108,14 @@ def test_raise_on_different_cell_content(raise_on_first_difference):
     ref = new_notebook(cells=[new_markdown_cell('Cell one'), new_code_cell('Cell two')])
     test = new_notebook(cells=[new_markdown_cell('Cell one'), new_code_cell('Modified cell two')])
     with pytest.raises(NotebookDifference):
-        compare_notebooks(ref, test, 'md', raise_on_first_difference=raise_on_first_difference)
+        compare_notebooks(test, ref, 'md', raise_on_first_difference=raise_on_first_difference)
 
 
 def test_raise_on_incomplete_markdown_cell():
     ref = new_notebook(cells=[new_markdown_cell('Cell one\n\n\nsecond line')])
     test = new_notebook(cells=[new_markdown_cell('Cell one')])
     with pytest.raises(NotebookDifference):
-        compare_notebooks(ref, test, 'md')
+        compare_notebooks(test, ref, 'md')
 
 
 def test_does_raise_on_split_markdown_cell():
@@ -123,14 +123,14 @@ def test_does_raise_on_split_markdown_cell():
     test = new_notebook(cells=[new_markdown_cell('Cell one'),
                                new_markdown_cell('second line')])
     with pytest.raises(NotebookDifference):
-        compare_notebooks(ref, test, 'md')
+        compare_notebooks(test, ref, 'md')
 
 
 def test_raise_on_different_cell_metadata():
     ref = new_notebook(cells=[new_code_cell('1+1')])
     test = new_notebook(cells=[new_code_cell('1+1', metadata={'metakey': 'value'})])
     with pytest.raises(NotebookDifference):
-        compare_notebooks(ref, test, 'py:light')
+        compare_notebooks(test, ref, 'py:light')
 
 
 @pytest.mark.parametrize('raise_on_first_difference', [True, False])
@@ -139,23 +139,23 @@ def test_raise_on_different_cell_count(raise_on_first_difference):
     test = new_notebook(cells=[new_code_cell('1'),
                                new_code_cell('2')])
     with pytest.raises(NotebookDifference):
-        compare_notebooks(ref, test, 'py:light', raise_on_first_difference=raise_on_first_difference)
+        compare_notebooks(test, ref, 'py:light', raise_on_first_difference=raise_on_first_difference)
 
     with pytest.raises(NotebookDifference):
-        compare_notebooks(test, ref, 'py:light', raise_on_first_difference=raise_on_first_difference)
+        compare_notebooks(ref, test, 'py:light', raise_on_first_difference=raise_on_first_difference)
 
 
 def test_does_not_raise_on_blank_line_removed():
     ref = new_notebook(cells=[new_code_cell('1+1\n    ')])
     test = new_notebook(cells=[new_code_cell('1+1')])
-    compare_notebooks(ref, test, 'py:light')
+    compare_notebooks(test, ref, 'py:light')
 
 
 def test_strict_raise_on_blank_line_removed():
     ref = new_notebook(cells=[new_code_cell('1+1\n')])
     test = new_notebook(cells=[new_code_cell('1+1')])
     with pytest.raises(NotebookDifference):
-        compare_notebooks(ref, test, 'py:light', allow_expected_differences=False)
+        compare_notebooks(test, ref, 'py:light', allow_expected_differences=False)
 
 
 def test_dont_raise_on_different_outputs():
@@ -172,7 +172,7 @@ def test_dont_raise_on_different_outputs():
             "output_type": "execute_result"
         }
     ])])
-    compare_notebooks(ref, test, 'md')
+    compare_notebooks(test, ref, 'md')
 
 
 @pytest.mark.parametrize('raise_on_first_difference', [True, False])
@@ -191,7 +191,7 @@ def test_raise_on_different_outputs(raise_on_first_difference):
         }
     ])])
     with pytest.raises(NotebookDifference):
-        compare_notebooks(ref, test, 'md', compare_outputs=True, raise_on_first_difference=raise_on_first_difference)
+        compare_notebooks(test, ref, 'md', raise_on_first_difference=raise_on_first_difference, compare_outputs=True)
 
 
 def test_test_round_trip_conversion():
@@ -217,7 +217,7 @@ def test_mutiple_cells_differ():
     nb2 = new_notebook(cells=[new_code_cell('1+1'),
                               new_code_cell('2\n2')])
     with pytest.raises(NotebookDifference) as exception_info:
-        compare_notebooks(nb1, nb2, raise_on_first_difference=False)
+        compare_notebooks(nb2, nb1, raise_on_first_difference=False)
     assert 'Cells 1,2 differ' in exception_info.value.args[0]
 
 
@@ -227,7 +227,7 @@ def test_cell_metadata_differ():
     nb2 = new_notebook(cells=[new_code_cell('1'),
                               new_code_cell('2', metadata={'additional': 'metadata2'})])
     with pytest.raises(NotebookDifference) as exception_info:
-        compare_notebooks(nb1, nb2, raise_on_first_difference=False)
+        compare_notebooks(nb2, nb1, raise_on_first_difference=False)
     assert "Cell metadata 'additional' differ" in exception_info.value.args[0]
 
 
@@ -238,5 +238,5 @@ def test_notebook_metadata_differ():
                               new_code_cell('2')],
                        metadata={'kernelspec': {'language': 'python', 'name': 'python', 'display_name': 'Python'}})
     with pytest.raises(NotebookDifference) as exception_info:
-        compare_notebooks(nb1, nb2, raise_on_first_difference=False, )
+        compare_notebooks(nb2, nb1, raise_on_first_difference=False)
     assert "Notebook metadata differ" in exception_info.value.args[0]
diff --git a/tests/test_contentsmanager.py b/tests/test_contentsmanager.py
index 283d2e46..bf271fa2 100644
--- a/tests/test_contentsmanager.py
+++ b/tests/test_contentsmanager.py
@@ -85,7 +85,7 @@ def test_pair_unpair_notebook(tmpdir):
 
     # reload and get outputs
     nb2 = cm.get(tmp_md)['content']
-    compare_notebooks(nb2, nb)
+    compare_notebooks(nb, nb2)
 
     # unpair and save as md
     del nb['metadata']['jupytext']
@@ -93,7 +93,7 @@ def test_pair_unpair_notebook(tmpdir):
     nb2 = cm.get(tmp_md)['content']
 
     # we get no outputs here
-    compare_notebooks(nb2, nb, compare_outputs=False)
+    compare_notebooks(nb, nb2, compare_outputs=False)
     assert len(nb2.cells[0]['outputs']) == 0
 
 
@@ -111,7 +111,7 @@ def test_load_save_rename(nb_file, tmpdir):
     nb = jupytext.read(nb_file)
     cm.save(model=dict(type='notebook', content=nb), path=tmp_rmd)
     nb_rmd = cm.get(tmp_rmd)
-    compare_notebooks(nb, nb_rmd['content'], 'Rmd')
+    compare_notebooks(nb_rmd['content'], nb, 'Rmd')
 
     # save ipynb
     cm.save(model=dict(type='notebook', content=nb), path=tmp_ipynb)
@@ -157,7 +157,7 @@ def test_save_load_paired_md_notebook(nb_file, tmpdir):
     cm.save(model=dict(type='notebook', content=nb), path=tmp_ipynb)
     nb_md = cm.get(tmp_md)
 
-    compare_notebooks(nb, nb_md['content'], 'md')
+    compare_notebooks(nb_md['content'], nb, 'md')
     assert nb_md['content'].metadata['jupytext']['formats'] == 'ipynb,md'
 
 
@@ -178,7 +178,7 @@ def test_save_load_paired_md_pandoc_notebook(nb_file, tmpdir):
     cm.save(model=dict(type='notebook', content=nb), path=tmp_ipynb)
     nb_md = cm.get(tmp_md)
 
-    compare_notebooks(nb, nb_md['content'], 'md:pandoc')
+    compare_notebooks(nb_md['content'], nb, 'md:pandoc')
     assert nb_md['content'].metadata['jupytext']['formats'] == 'ipynb,md:pandoc'
 
 
@@ -210,7 +210,7 @@ def test_pair_plain_script(py_file, tmpdir):
 
     # reopen py file with the cm
     nb2 = cm.get(tmp_py)['content']
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
     assert nb2.metadata['jupytext']['formats'] == 'ipynb,py:hydrogen'
 
     # remove the pairing and save
@@ -219,7 +219,7 @@ def test_pair_plain_script(py_file, tmpdir):
 
     # reopen py file with the cm
     nb2 = cm.get(tmp_py)['content']
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
     assert 'formats' not in nb2.metadata['jupytext']
 
 
@@ -237,7 +237,7 @@ def test_load_save_rename_nbpy(nb_file, tmpdir):
     nb = jupytext.read(nb_file)
     cm.save(model=dict(type='notebook', content=nb), path=tmp_nbpy)
     nbpy = cm.get(tmp_nbpy)
-    compare_notebooks(nb, nbpy['content'])
+    compare_notebooks(nbpy['content'], nb)
 
     # save ipynb
     cm.save(model=dict(type='notebook', content=nb), path=tmp_ipynb)
@@ -295,7 +295,7 @@ def test_load_save_rename_notebook_with_dot(nb_file, tmpdir):
     nb = jupytext.read(nb_file)
     cm.save(model=dict(type='notebook', content=nb), path=tmp_nbpy)
     nbpy = cm.get(tmp_nbpy)
-    compare_notebooks(nb, nbpy['content'])
+    compare_notebooks(nbpy['content'], nb)
 
     # save ipynb
     cm.save(model=dict(type='notebook', content=nb), path=tmp_ipynb)
@@ -324,11 +324,11 @@ def test_load_save_rename_nbpy_default_config(nb_file, tmpdir):
 
     cm.save(model=dict(type='notebook', content=nb), path=tmp_nbpy)
     nbpy = cm.get(tmp_nbpy)
-    compare_notebooks(nb, nbpy['content'])
+    compare_notebooks(nbpy['content'], nb)
 
     # open ipynb
     nbipynb = cm.get(tmp_ipynb)
-    compare_notebooks(nb, nbipynb['content'])
+    compare_notebooks(nbipynb['content'], nb)
 
     # save ipynb
     cm.save(model=dict(type='notebook', content=nb), path=tmp_ipynb)
@@ -365,11 +365,11 @@ def test_load_save_rename_non_ascii_path(nb_file, tmpdir):
 
     cm.save(model=dict(type='notebook', content=nb), path=tmp_nbpy)
     nbpy = cm.get(tmp_nbpy)
-    compare_notebooks(nb, nbpy['content'])
+    compare_notebooks(nbpy['content'], nb)
 
     # open ipynb
     nbipynb = cm.get(tmp_ipynb)
-    compare_notebooks(nb, nbipynb['content'])
+    compare_notebooks(nbipynb['content'], nb)
 
     # save ipynb
     cm.save(model=dict(type='notebook', content=nb), path=tmp_ipynb)
@@ -455,8 +455,8 @@ def test_reload_notebook_after_jupytext_cli(nb_file, tmpdir):
     nb1 = cm.get('notebook.py')['content']
     nb2 = cm.get('notebook.ipynb')['content']
 
-    compare_notebooks(nb1, nb)
-    compare_notebooks(nb2, nb)
+    compare_notebooks(nb, nb1)
+    compare_notebooks(nb, nb2)
 
 
 @skip_if_dict_is_not_ordered
@@ -591,12 +591,12 @@ def test_open_using_preferred_and_default_format_174(nb_file, tmpdir):
 
     # read py file
     model2 = cm.get('python/notebook.py')
-    compare_notebooks(model['content'], model2['content'])
+    compare_notebooks(model2['content'], model['content'])
 
     # move py file to the another folder
     shutil.move(tmp_py, tmp_py2)
     model2 = cm.get('other/notebook.py')
-    compare_notebooks(model['content'], model2['content'])
+    compare_notebooks(model2['content'], model['content'])
     cm.save(model=model, path='other/notebook.py')
     assert not os.path.isfile(tmp_ipynb)
     assert not os.path.isfile(str(tmpdir.join('other/notebook.ipynb')))
@@ -626,7 +626,7 @@ def test_kernelspec_are_preserved(nb_file, tmpdir):
 
     # read ipynb
     model2 = cm.get('notebook.ipynb')
-    compare_notebooks(model['content'], model2['content'])
+    compare_notebooks(model2['content'], model['content'])
 
 
 @skip_if_dict_is_not_ordered
@@ -657,16 +657,16 @@ def test_save_to_light_percent_sphinx_format(nb_file, tmpdir):
         assert read_format_from_metadata(stream.read(), '.py') == 'sphinx'
 
     model = cm.get(path=tmp_pct_py)
-    compare_notebooks(nb, model['content'])
+    compare_notebooks(model['content'], nb)
 
     model = cm.get(path=tmp_lgt_py)
-    compare_notebooks(nb, model['content'])
+    compare_notebooks(model['content'], nb)
 
     model = cm.get(path=tmp_spx_py)
     # (notebooks not equal as we insert %matplotlib inline in sphinx)
 
     model = cm.get(path=tmp_ipynb)
-    compare_notebooks(nb, model['content'])
+    compare_notebooks(model['content'], nb)
 
 
 @skip_if_dict_is_not_ordered
@@ -693,11 +693,11 @@ def test_pair_notebook_with_dot(nb_file, tmpdir):
 
     model = cm.get(path=tmp_py)
     assert model['name'] == 'file.5.1.py'
-    compare_notebooks(nb, model['content'])
+    compare_notebooks(model['content'], nb)
 
     model = cm.get(path=tmp_ipynb)
     assert model['name'] == 'file.5.1.ipynb'
-    compare_notebooks(nb, model['content'])
+    compare_notebooks(model['content'], nb)
 
 
 @pytest.mark.parametrize('nb_file', list_notebooks('ipynb_py')[:1])
@@ -728,7 +728,7 @@ def test_preferred_format_allows_to_read_others_format(nb_file, tmpdir):
     assert model['content']['metadata']['jupytext']['formats'] == 'ipynb,py:light'
 
     # Check contents
-    compare_notebooks(nb, model['content'])
+    compare_notebooks(model['content'], nb)
 
     # Change save format and save
     model['content']['metadata']['jupytext']['formats'] == 'ipynb,py'
@@ -737,7 +737,7 @@ def test_preferred_format_allows_to_read_others_format(nb_file, tmpdir):
 
     # Read notebook
     model = cm.get(tmp_nbpy)
-    compare_notebooks(nb, model['content'])
+    compare_notebooks(model['content'], nb)
 
     # Check that format is explicit
     assert model['content']['metadata']['jupytext']['formats'] == 'ipynb,py:percent'
@@ -790,7 +790,7 @@ def test_save_in_auto_extension_global(nb_file, tmpdir):
     # saving should not create a format entry #95
     assert 'formats' not in model['content'].metadata.get('jupytext', {})
 
-    compare_notebooks(nb, model['content'])
+    compare_notebooks(model['content'], nb)
 
 
 def test_global_auto_pairing_works_with_empty_notebook(tmpdir):
@@ -815,7 +815,7 @@ def test_global_auto_pairing_works_with_empty_notebook(tmpdir):
     assert 'notebook.ipynb' not in cm.paired_notebooks
 
     model = cm.get(path='notebook.ipynb')
-    compare_notebooks(nb, model['content'])
+    compare_notebooks(model['content'], nb)
 
     # add language information to the notebook
     nb.metadata['language_info'] = {
@@ -876,7 +876,7 @@ def test_save_in_auto_extension_global_with_format(nb_file, tmpdir):
     # saving should not create a format entry #95
     assert 'formats' not in model['content'].metadata.get('jupytext', {})
 
-    compare_notebooks(nb, model['content'])
+    compare_notebooks(model['content'], nb)
 
 
 @pytest.mark.parametrize('nb_file', list_notebooks('ipynb'))
@@ -903,7 +903,7 @@ def test_save_in_auto_extension_local(nb_file, tmpdir):
     # reload and compare with original notebook
     model = cm.get(path=tmp_script)
 
-    compare_notebooks(nb, model['content'])
+    compare_notebooks(model['content'], nb)
 
 
 @pytest.mark.parametrize('nb_file', list_notebooks('ipynb'))
@@ -972,7 +972,7 @@ def test_metadata_filter_is_effective(nb_file, tmpdir):
     # read paired notebook
     nb3 = cm.get(tmp_script)['content']
 
-    compare_notebooks(nb, nb3)
+    compare_notebooks(nb3, nb)
 
 
 def test_no_metadata_added_to_scripts_139(tmpdir):
@@ -1032,7 +1032,7 @@ def test_local_format_can_deactivate_pairing(nb_file, ext, tmpdir):
     assert os.path.isfile(str(tmpdir.join('notebook.py'))) == (ext == '.py')
     assert os.path.isfile(str(tmpdir.join('notebook.ipynb'))) == (ext == '.ipynb')
     nb2 = cm.get('notebook' + ext)['content']
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
     # resave, check again
     cm.save(model=dict(type='notebook', content=nb2), path='notebook' + ext)
@@ -1040,7 +1040,7 @@ def test_local_format_can_deactivate_pairing(nb_file, ext, tmpdir):
     assert os.path.isfile(str(tmpdir.join('notebook.py'))) == (ext == '.py')
     assert os.path.isfile(str(tmpdir.join('notebook.ipynb'))) == (ext == '.ipynb')
     nb3 = cm.get('notebook' + ext)['content']
-    compare_notebooks(nb, nb3)
+    compare_notebooks(nb3, nb)
 
 
 @pytest.mark.parametrize('nb_file', list_notebooks('Rmd'))
@@ -1062,7 +1062,7 @@ def test_global_pairing_allows_to_save_other_file_types(nb_file, tmpdir):
     assert not os.path.isfile(str(tmpdir.join('notebook.ipynb')))
 
     nb2 = cm.get('notebook.Rmd')['content']
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
 
 @skip_if_dict_is_not_ordered
@@ -1332,7 +1332,7 @@ def test_vim_folding_markers(tmpdir):
     assert os.path.isfile(tmp_py)
 
     nb2 = cm.get('nb.ipynb')['content']
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
     nb3 = read(tmp_py)
     assert nb3.metadata['jupytext']['cell_markers'] == '{{{,}}}'
@@ -1379,7 +1379,7 @@ def test_vscode_pycharm_folding_markers(tmpdir):
     assert os.path.isfile(tmp_py)
 
     nb2 = cm.get('nb.ipynb')['content']
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
     nb3 = read(tmp_py)
     assert nb3.metadata['jupytext']['cell_markers'] == 'region,endregion'
@@ -1474,7 +1474,7 @@ def test_save_file_with_default_cell_markers(tmpdir):
     compare('\n'.join(text.splitlines()), '\n'.join(text2.splitlines()[-len(text.splitlines()):]))
 
     nb2 = cm.get('nb.py')['content']
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
     assert nb2.metadata['jupytext']['cell_markers'] == '+,-'
 
 
diff --git a/tests/test_escape_magics.py b/tests/test_escape_magics.py
index 60a69c0c..daec4de1 100644
--- a/tests/test_escape_magics.py
+++ b/tests/test_escape_magics.py
@@ -52,7 +52,7 @@ def test_magics_commented_default(fmt, commented):
     if 'sphinx' in fmt:
         nb2.cells = nb2.cells[1:]
 
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
 
 @pytest.mark.parametrize('fmt', ['md', 'Rmd', 'py:light', 'py:percent', 'py:sphinx', 'R', 'ss:light', 'ss:percent'])
@@ -69,7 +69,7 @@ def test_magics_are_commented(fmt):
     if 'sphinx' in fmt:
         nb2.cells = nb2.cells[1:]
 
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
 
 @pytest.mark.parametrize('fmt', ['md', 'Rmd', 'py:light', 'py:percent', 'py:sphinx', 'R', 'ss:light', 'ss:percent'])
@@ -86,7 +86,7 @@ def test_magics_are_not_commented(fmt):
     if 'sphinx' in fmt:
         nb2.cells = nb2.cells[1:]
 
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
 
 def test_force_comment_using_contents_manager(tmpdir):
@@ -150,4 +150,4 @@ def g(x):
 def g(x):
     return x+1
 """, text)
-    compare_notebooks(nb, jupytext.reads(text, 'py'))
+    compare_notebooks(jupytext.reads(text, 'py'), nb)
diff --git a/tests/test_ipynb_to_R.py b/tests/test_ipynb_to_R.py
index 9071be95..715e40ec 100644
--- a/tests/test_ipynb_to_R.py
+++ b/tests/test_ipynb_to_R.py
@@ -19,4 +19,4 @@ def test_identity_source_write_read(nb_file, ext):
     R = jupytext.writes(nb1, ext)
     nb2 = jupytext.reads(R, ext)
 
-    compare_notebooks(nb1, nb2)
+    compare_notebooks(nb2, nb1)
diff --git a/tests/test_ipynb_to_py.py b/tests/test_ipynb_to_py.py
index 84bbfbba..024ee78c 100644
--- a/tests/test_ipynb_to_py.py
+++ b/tests/test_ipynb_to_py.py
@@ -16,4 +16,4 @@ def test_identity_source_write_read(nb_file):
     py = jupytext.writes(nb1, 'py')
     nb2 = jupytext.reads(py, 'py')
 
-    compare_notebooks(nb1, nb2)
+    compare_notebooks(nb2, nb1)
diff --git a/tests/test_ipynb_to_rmd.py b/tests/test_ipynb_to_rmd.py
index 39d9dd36..1cd84cf8 100644
--- a/tests/test_ipynb_to_rmd.py
+++ b/tests/test_ipynb_to_rmd.py
@@ -16,4 +16,4 @@ def test_identity_source_write_read(nb_file):
     rmd = jupytext.writes(nb1, 'Rmd')
     nb2 = jupytext.reads(rmd, 'Rmd')
 
-    compare_notebooks(nb1, nb2, 'Rmd')
+    compare_notebooks(nb2, nb1, 'Rmd')
diff --git a/tests/test_jupytext_read.py b/tests/test_jupytext_read.py
index 8c4e09a0..652eeb72 100644
--- a/tests/test_jupytext_read.py
+++ b/tests/test_jupytext_read.py
@@ -19,4 +19,4 @@ def test_read_file_with_explicit_fmt(tmpdir):
     nb1 = jupytext.read(tmp_py)
     nb2 = jupytext.read(tmp_py, fmt='py:percent')
 
-    compare_notebooks(nb1, nb2)
+    compare_notebooks(nb2, nb1)
diff --git a/tests/test_mirror.py b/tests/test_mirror.py
index 33cbe48e..c7e81b25 100644
--- a/tests/test_mirror.py
+++ b/tests/test_mirror.py
@@ -62,7 +62,7 @@ def assert_conversion_same_as_mirror(nb_file, fmt, mirror_name, compare_notebook
 
     if not actual.endswith('\n'):
         actual = actual + '\n'
-    compare(expected, actual)
+    compare(actual, expected)
 
     # Compare the two notebooks
     if ext != '.ipynb':
@@ -76,10 +76,10 @@ def assert_conversion_same_as_mirror(nb_file, fmt, mirror_name, compare_notebook
             for cell in nb_mirror.cells:
                 cell.metadata = {}
 
-        compare_notebooks(notebook, nb_mirror, fmt)
+        compare_notebooks(nb_mirror, notebook, fmt)
 
         combine_inputs_with_outputs(nb_mirror, notebook)
-        compare_notebooks(notebook, nb_mirror, fmt, compare_outputs=True)
+        compare_notebooks(nb_mirror, notebook, fmt, compare_outputs=True)
 
 
 @pytest.mark.parametrize('nb_file', list_notebooks('julia') + list_notebooks('python') + list_notebooks('R'))
diff --git a/tests/test_preserve_empty_cells.py b/tests/test_preserve_empty_cells.py
index 07594e8f..627db42c 100644
--- a/tests/test_preserve_empty_cells.py
+++ b/tests/test_preserve_empty_cells.py
@@ -27,4 +27,4 @@ def test_notebook_with_empty_cells(blank_cells):
     script = jupytext.writes(notebook, 'py')
     notebook2 = jupytext.reads(script, 'py')
 
-    compare_notebooks(notebook, notebook2)
+    compare_notebooks(notebook2, notebook)
diff --git a/tests/test_read_simple_markdown.py b/tests/test_read_simple_markdown.py
index e5883edf..8fd514ff 100644
--- a/tests/test_read_simple_markdown.py
+++ b/tests/test_read_simple_markdown.py
@@ -1,5 +1,5 @@
-from nbformat.v4.nbbase import new_code_cell, new_raw_cell, new_markdown_cell
-from jupytext.compare import compare
+from nbformat.v4.nbbase import new_code_cell, new_raw_cell, new_markdown_cell, new_notebook
+from jupytext.compare import compare, compare_notebooks
 import jupytext
 from jupytext.combine import combine_inputs_with_outputs
 
@@ -18,10 +18,22 @@ x = np.arange(0,1,eps)
 y = np.abs(x)-.5
 ```
 
+This is
+a Markdown cell
+
+```
+# followed by a code cell with no language info
+```
+
 ```
-# this is a code cell with no language info
+# another code cell
+
+
+# with two blank lines
 ```
 
+And the same markdown cell continues
+
 <!-- #raw -->
 this is a raw cell
 <!-- #endraw -->
@@ -52,7 +64,21 @@ cat(stringi::stri_rand_lipsum(3), sep='\n\n')
                         'outputs': []},
                        {'cell_type': 'markdown',
                         'metadata': {},
-                        'source': '```\n# this is a code cell with no language info\n```'},
+                        'source': '''This is
+a Markdown cell
+
+```
+# followed by a code cell with no language info
+```
+
+```
+# another code cell
+
+
+# with two blank lines
+```
+
+And the same markdown cell continues'''},
                        {'cell_type': 'raw',
                         'metadata': {},
                         'source': 'this is a raw cell'},
@@ -72,6 +98,42 @@ cat(stringi::stri_rand_lipsum(3), sep='\n\n')
     compare(markdown, markdown2)
 
 
+def test_read_md_and_markdown_regions(markdown="""Some text
+
+<!-- #md -->
+A
+
+
+long
+cell
+<!-- #endmd -->
+
+<!-- #markdown -->
+Another
+
+
+long
+cell
+<!-- #endmarkdown -->
+"""):
+    nb = jupytext.reads(markdown, 'md')
+    assert nb.metadata['jupytext']['main_language'] == 'python'
+    compare(nb.cells, [new_markdown_cell('Some text'),
+                       new_markdown_cell("""A
+
+
+long
+cell""", metadata={'region_name': 'md'}),
+                       new_markdown_cell("""Another
+
+
+long
+cell""", metadata={'region_name': 'markdown'})])
+
+    markdown2 = jupytext.writes(nb, 'md')
+    compare(markdown, markdown2)
+
+
 def test_read_mostly_R_markdown_file(markdown="""```R
 ls()
 ```
@@ -369,3 +431,27 @@ a = 1
 
     text2 = jupytext.writes(nb, 'md')
     compare(text.replace('```IDL', '```idl'), text2)
+
+
+def test_inactive_cell(text='''```python active="md"
+# This becomes a raw cell in Jupyter
+```
+''', expected=new_notebook(
+    cells=[new_raw_cell('# This becomes a raw cell in Jupyter',
+                        metadata={'active': 'md'})])):
+    nb = jupytext.reads(text, 'md')
+    compare_notebooks(nb, expected)
+    text2 = jupytext.writes(nb, 'md')
+    compare(text2, text)
+
+
+def test_inactive_cell_using_tag(text='''```python tags=["active-md"]
+# This becomes a raw cell in Jupyter
+```
+''', expected=new_notebook(
+    cells=[new_raw_cell('# This becomes a raw cell in Jupyter',
+                        metadata={'tags': ['active-md']})])):
+    nb = jupytext.reads(text, 'md')
+    compare_notebooks(nb, expected)
+    text2 = jupytext.writes(nb, 'md')
+    compare(text2, text)
diff --git a/tests/test_read_simple_pandoc.py b/tests/test_read_simple_pandoc.py
index 93c39acb..e2514224 100644
--- a/tests/test_read_simple_pandoc.py
+++ b/tests/test_read_simple_pandoc.py
@@ -20,7 +20,7 @@ print("hello")
     markdown2 = jupytext.writes(nb, 'md')
 
     nb2 = jupytext.reads(markdown2, 'md')
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
     markdown3 = jupytext.writes(nb2, 'md')
     compare(markdown2, markdown3)
@@ -58,4 +58,4 @@ This is the greek letter $\\pi$: π''')])):
     markdown = jupytext.writes(nb, 'md:pandoc')
     nb2 = jupytext.reads(markdown, 'md:pandoc')
     nb2.metadata.pop('jupytext')
-    compare_notebooks(nb2, nb, 'md:pandoc')
+    compare_notebooks(nb, nb2, 'md:pandoc')
diff --git a/tests/test_read_simple_percent.py b/tests/test_read_simple_percent.py
index d26cd787..ffb766a1 100644
--- a/tests/test_read_simple_percent.py
+++ b/tests/test_read_simple_percent.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 import os
-from nbformat.v4.nbbase import new_notebook, new_code_cell, new_markdown_cell
+from nbformat.v4.nbbase import new_notebook, new_code_cell, new_markdown_cell, new_raw_cell
 from jupytext.compare import compare, compare_notebooks
 import jupytext
 
@@ -13,6 +13,9 @@ def test_read_simple_file(script="""# ---
 # %% [markdown]
 # This is a markdown cell
 
+# %% [md]
+# This is also a markdown cell
+
 # %% [raw]
 # This is a raw cell
 
@@ -31,27 +34,19 @@ def test_read_simple_file(script="""# ---
 7
 """):
     nb = jupytext.reads(script, 'py:percent')
-    assert len(nb.cells) == 6
-    assert nb.cells[0].cell_type == 'raw'
-    assert nb.cells[0].source == '---\ntitle: Simple file\n---'
-    assert nb.cells[1].cell_type == 'markdown'
-    assert nb.cells[1].source == 'This is a markdown cell'
-    assert nb.cells[2].cell_type == 'raw'
-    assert nb.cells[2].source == 'This is a raw cell'
-    assert nb.cells[3].cell_type == 'code'
-    assert nb.cells[3].source == '# This is a sub-cell'
-    assert nb.cells[3].metadata['title'] == 'sub-cell title'
-    assert nb.cells[4].cell_type == 'code'
-    assert nb.cells[4].source == '# This is a sub-sub-cell'
-    assert nb.cells[4].metadata['title'] == 'sub-sub-cell title'
-    assert nb.cells[5].cell_type == 'code'
-    compare(nb.cells[5].source, '''1 + 2 + 3 + 4
+    compare_notebooks(new_notebook(cells=[
+        new_raw_cell('---\ntitle: Simple file\n---'),
+        new_markdown_cell('This is a markdown cell'),
+        new_markdown_cell('This is also a markdown cell', metadata={'region_name': 'md'}),
+        new_raw_cell('This is a raw cell'),
+        new_code_cell('# This is a sub-cell', metadata={'title': 'sub-cell title', 'cell_depth': 1}),
+        new_code_cell('# This is a sub-sub-cell', metadata={'title': 'sub-sub-cell title', 'cell_depth': 2}),
+        new_code_cell('''1 + 2 + 3 + 4
 5
 6
 %%magic # this is a commented magic, not a cell
 
-7''')
-    assert nb.cells[5].metadata == {'title': 'And now a code cell'}
+7''', metadata={'title': 'And now a code cell'})]), nb)
 
     script2 = jupytext.writes(nb, 'py:percent')
     compare(script, script2)
@@ -329,7 +324,7 @@ cell
 """)
 
     nb2 = jupytext.read(tmp_py)
-    compare_notebooks(nb2, nb)
+    compare_notebooks(nb, nb2)
 
 
 def test_default_cell_markers_in_contents_manager(tmpdir):
@@ -363,7 +358,7 @@ cell
 """)
 
     nb2 = jupytext.read(tmp_py)
-    compare_notebooks(nb2, nb)
+    compare_notebooks(nb, nb2)
 
 
 def test_default_cell_markers_in_contents_manager_does_not_impact_light_format(tmpdir):
@@ -393,4 +388,4 @@ def test_default_cell_markers_in_contents_manager_does_not_impact_light_format(t
 """)
 
     nb2 = jupytext.read(tmp_py)
-    compare_notebooks(nb2, nb)
+    compare_notebooks(nb, nb2)
diff --git a/tests/test_read_simple_python.py b/tests/test_read_simple_python.py
index feb46930..83f8c3e9 100644
--- a/tests/test_read_simple_python.py
+++ b/tests/test_read_simple_python.py
@@ -716,7 +716,7 @@ def test_round_trip_markdown_cell_with_magic():
                             metadata={'jupytext': {'main_language': 'python'}})
     text = jupytext.writes(notebook, 'py')
     notebook2 = jupytext.reads(text, 'py')
-    compare_notebooks(notebook, notebook2)
+    compare_notebooks(notebook2, notebook)
 
 
 def test_round_trip_python_with_js_cell():
@@ -726,7 +726,7 @@ notebook.nbextensions.install_nbextension('jupytext.js', user=True)'''),
 Jupyter.utils.load_extensions('jupytext')''')])
     text = jupytext.writes(notebook, 'py')
     notebook2 = jupytext.reads(text, 'py')
-    compare_notebooks(notebook, notebook2)
+    compare_notebooks(notebook2, notebook)
 
 
 def test_round_trip_python_with_js_cell_no_cell_metadata():
@@ -738,4 +738,4 @@ Jupyter.utils.load_extensions('jupytext')''')],
                                                    'cell_metadata_filter': '-all'}})
     text = jupytext.writes(notebook, 'py')
     notebook2 = jupytext.reads(text, 'py')
-    compare_notebooks(notebook, notebook2)
+    compare_notebooks(notebook2, notebook)
diff --git a/tests/test_save_multiple.py b/tests/test_save_multiple.py
index 7a856cb6..f6fbb330 100644
--- a/tests/test_save_multiple.py
+++ b/tests/test_save_multiple.py
@@ -24,7 +24,7 @@ def test_rmd_is_ok(nb_file, tmpdir):
 
     nb2 = jupytext.read(str(tmpdir.join(tmp_rmd)))
 
-    compare_notebooks(nb, nb2, 'Rmd')
+    compare_notebooks(nb2, nb, 'Rmd')
 
 
 @pytest.mark.parametrize('nb_file', list_notebooks('Rmd'))
@@ -40,7 +40,7 @@ def test_ipynb_is_ok(nb_file, tmpdir):
     cm.save(model=dict(type='notebook', content=nb), path=tmp_rmd)
 
     nb2 = jupytext.read(str(tmpdir.join(tmp_ipynb)))
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
 
 @pytest.mark.parametrize('nb_file', list_notebooks('ipynb_py', skip='66'))
@@ -57,10 +57,10 @@ def test_all_files_created(nb_file, tmpdir):
     cm.save(model=dict(type='notebook', content=nb), path=tmp_ipynb)
 
     nb2 = jupytext.read(str(tmpdir.join(tmp_py)))
-    compare_notebooks(nb, nb2)
+    compare_notebooks(nb2, nb)
 
     nb3 = jupytext.read(str(tmpdir.join(tmp_rmd)))
-    compare_notebooks(nb, nb3, 'Rmd')
+    compare_notebooks(nb3, nb, 'Rmd')
 
 
 def test_no_files_created_on_no_format(tmpdir):