Repository: executablebooks/sphinx-panels

Issue:
Default vertical padding?
Currently, the first example in the docs (https://sphinx-panels.readthedocs.io/en/latest/) shows:

![image](https://user-images.githubusercontent.com/1020496/81395801-8c7bdf80-9124-11ea-862a-630795e26331.png)

So with horizontal spacing between the columns, but no vertical space between the card rows.

My *feeling* says that for default use cases of a few panels, many people will want some vertical space as well (unless you are doing something accordion like). So therefore wondering if the default classes could include some general padding. 
(maybe like the example at the bottom https://sphinx-panels.readthedocs.io/en/latest/#additional-examples, I suppose it's the "p-2" added to `columns` that does the trick?)

Discussion:
chrisjsewell: ok cool, will merge and release as version `0.3`

Patch:
diff --git a/README.md b/README.md
index 5332587..8c13946 100644
--- a/README.md
+++ b/README.md
@@ -14,15 +14,15 @@ and [cards layout](https://getbootstrap.com/docs/4.0/components/card/).
 
     Content of the top-left panel
 
-    ...
+    ---
 
     Content of the top-right panel
 
-    ...
+    ---
 
     Content of the bottom-left panel
 
-    ...
+    ---
 
     Content of the bottom-right panel
 ```
diff --git a/docs/index.rst b/docs/index.rst
index 6e96db8..489752a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -15,15 +15,15 @@ and `cards layout <https://getbootstrap.com/docs/4.0/components/card/>`_.
 
         Content of the top-left panel
 
-        ...
+        ---
 
         Content of the top-right panel
 
-        ...
+        ---
 
         Content of the bottom-left panel
 
-        ...
+        ---
 
         Content of the bottom-right panel
 
@@ -31,15 +31,15 @@ and `cards layout <https://getbootstrap.com/docs/4.0/components/card/>`_.
 
     Content of the top-left panel
 
-    ...
+    ---
 
     Content of the top-right panel
 
-    ...
+    ---
 
     Content of the bottom-left panel
 
-    ...
+    ---
 
     Content of the bottom-right panel
 
@@ -77,12 +77,12 @@ This extension includes the bootstrap 4 CSS classes relevant to panels.
 They will be loaded by default but, if you are already using a bootstrap theme,
 you can disable this by adding ``panels_add_boostrap_css = False`` to your ``conf.py``.
 
-You can also change the delimiters used by adding ``panel_delimiters`` to your ``conf.py``,
+You can also change the delimiter regexes used by adding ``panel_delimiters`` to your ``conf.py``,
 e.g. the default value (panels, header, footer) is:
 
 .. code-block:: python
 
-    panels_delimiters = (".", "^", "+")
+    panels_delimiters = (r"^\-{3,}$", r"^\^{3,}$", r"^\+{3,}$")
 
 
 Detailed Examples
@@ -91,11 +91,23 @@ Detailed Examples
 Grid Layout
 -----------
 
-Panels are split by three or more `...` characters.
+Panels are split by three or more `-` characters.
 The layout of panels is then set by using the bootstrap classes.
 Default classes for all panels may be set in the directive options,
 then panel specific classes can be added at the start of each panel.
-`=` will reset the classes, or `+=` will add to the default classes.
+
+By default the new classes will override those set previously
+(as defaults or in the top level options),
+but starting the option value with `+` will make the classes additive.
+For example the following options will set the first panel's card to have both the `shadow` and `bg-info` classes:
+
+.. code-block:: rst
+
+    .. panels::
+        :card: shadow
+
+        ---
+        :card: + bg-info
 
 .. seealso::
 
@@ -110,35 +122,35 @@ then panel specific classes can be added at the start of each panel.
 
         .. panels::
             :container: container pb-4
-            :column: col-lg-6 col-md-6 col-sm-6 col-xs-12
+            :column: col-lg-6 col-md-6 col-sm-6 col-xs-12 p-2
             :card: shadow
 
 .. code-block:: rst
 
     .. panels::
         :container: container-lg pb-3
-        :column: col-lg-4 col-md-4 col-sm-6 col-xs-12
+        :column: col-lg-4 col-md-4 col-sm-6 col-xs-12 p-2
 
         panel1
-        ...
+        ---
         panel2
-        ...
+        ---
         panel3
-        ...
-        column = col-lg-12
+        ---
+        :column: col-lg-12 p-2
         panel4
 
 .. panels::
     :container: container-lg pb-3
-    :column: col-lg-4 col-md-4 col-sm-6 col-xs-12
+    :column: col-lg-4 col-md-4 col-sm-6 col-xs-12 p-2
 
     panel1
-    ...
+    ---
     panel2
-    ...
+    ---
     panel3
-    ...
-    column = col-lg-12
+    ---
+    :column: col-lg-12 p-2
     panel4
 
 Card Layout
@@ -166,7 +178,7 @@ split by three or more `^^^` and `+++` respectively.
         ++++++++++++++
         panel 1 footer
 
-        ...
+        ---
 
         panel 2 header
         ^^^^^^^^^^^^^^
@@ -188,7 +200,7 @@ split by three or more `^^^` and `+++` respectively.
     ++++++++++++++
     panel 1 footer
 
-    ...
+    ---
 
     panel 2 header
     ^^^^^^^^^^^^^^
@@ -218,7 +230,7 @@ You can add your own CSS (see
 but it is advised you use the built-in bootstrap classes:
 
 - `Card colouring <https://getbootstrap.com/docs/4.0/utilities/colors/>`_  contextual classes: `bg-primary`, `bg-success`, `bg-info`, `bg-warning`, `bg-danger`, `bg-secondary`, `bg-dark` and `bg-light`.
-- `Padding and margins <https://getbootstrap.com/docs/4.0/utilities/spacing/>`_: `border-0`, `p-2`, `m-2`, ...
+- `Padding and margins <https://getbootstrap.com/docs/4.0/utilities/spacing/>`_: `border-0`, `p-2`, `m-2`, ---
 - `Text alignment <https://getbootstrap.com/docs/4.0/utilities/text/#text-alignment>`_: `text-justify`, `text-left`, `text-center`, `text-right`
 
 .. code-block:: rst
@@ -228,8 +240,8 @@ but it is advised you use the built-in bootstrap classes:
         :header: text-center
         :footer: text-right
 
-        ...
-        column += p-1
+        ---
+        :column: + p-1
 
         panel 1 header
         ^^^^^^^^^^^^^^
@@ -239,11 +251,11 @@ but it is advised you use the built-in bootstrap classes:
         ++++++++++++++
         panel 1 footer
 
-        ...
-        column += p-1 text-center border-0
-        body = bg-info
-        header = bg-success
-        footer = bg-secondary
+        ---
+        :column: + p-1 text-center border-0
+        :body: bg-info
+        :header: bg-success
+        :footer: bg-secondary
 
         panel 2 header
         ^^^^^^^^^^^^^^
@@ -258,8 +270,8 @@ but it is advised you use the built-in bootstrap classes:
     :header: text-center
     :footer: text-right
 
-    ...
-    column += p-1
+    ---
+    :column: + p-1
 
     panel 1 header
     ^^^^^^^^^^^^^^
@@ -269,11 +281,11 @@ but it is advised you use the built-in bootstrap classes:
     ++++++++++++++
     panel 1 footer
 
-    ...
-    column += p-1 text-center border-0
-    body = bg-info
-    header = bg-success
-    footer = bg-secondary
+    ---
+    :column: + p-1 text-center border-0
+    :body: bg-info
+    :header: bg-success
+    :footer: bg-secondary
 
     panel 2 header
     ^^^^^^^^^^^^^^
@@ -296,9 +308,9 @@ but classes can also be used to add padding:
     .. panels::
         :img-top-cls: pl-5 pr-5
 
-        ...
-        img-top = _static/ebp-logo.png
-        img-bottom = _static/footer-banner.jpg
+        ---
+        :img-top: _static/ebp-logo.png
+        :img-bottom: _static/footer-banner.jpg
 
         header 1
         ^^^^^^^^
@@ -310,10 +322,10 @@ but classes can also be used to add padding:
         ++++++
         tail 1
 
-        ...
-        img-top = _static/sphinx-logo.png
-        img-top-cls += bg-success
-        img-bottom = _static/footer-banner.jpg
+        ---
+        :img-top: _static/sphinx-logo.png
+        :img-top-cls: + bg-success
+        :img-bottom: _static/footer-banner.jpg
 
         header 2
         ^^^^^^^^
@@ -327,9 +339,9 @@ but classes can also be used to add padding:
     :img-top-cls: pl-5 pr-5
     :body: text-center
 
-    ...
-    img-top = _static/ebp-logo.png
-    img-bottom = _static/footer-banner.jpg
+    ---
+    :img-top: _static/ebp-logo.png
+    :img-bottom: _static/footer-banner.jpg
 
     header 1
     ^^^^^^^^
@@ -341,10 +353,10 @@ but classes can also be used to add padding:
     ++++++
     tail 1
 
-    ...
-    img-top = _static/sphinx-logo.png
-    img-top-cls += bg-success
-    img-bottom = _static/footer-banner.jpg
+    ---
+    :img-top: _static/sphinx-logo.png
+    :img-top-cls: + bg-success
+    :img-bottom: _static/footer-banner.jpg
 
     header 2
     ^^^^^^^^
@@ -366,8 +378,8 @@ Additional Examples
         :header: border-0
         :footer: border-0
 
-        ...
-        card += bg-warning
+        ---
+        :card: + bg-warning
 
         header
         ^^^^^^
@@ -377,9 +389,9 @@ Additional Examples
         ++++++
         footer
 
-        ...
-        card += bg-info
-        footer += bg-danger
+        ---
+        :card: + bg-info
+        :footer: + bg-danger
 
         header
         ^^^^^^
@@ -389,9 +401,9 @@ Additional Examples
         ++++++
         footer
 
-        ...
-        column = col-lg-12
-        card += bg-success text-center
+        ---
+        :column: col-lg-12 p-3
+        :card: + bg-success text-center
 
         Content of the bottom panel
 
@@ -403,8 +415,8 @@ Additional Examples
     :header: border-0
     :footer: border-0
 
-    ...
-    card += bg-warning
+    ---
+    :card: + bg-warning
 
     header
     ^^^^^^
@@ -414,9 +426,9 @@ Additional Examples
     ++++++
     footer
 
-    ...
-    card += bg-info
-    footer += bg-danger
+    ---
+    :card: + bg-info
+    :footer: + bg-danger
 
     header
     ^^^^^^
@@ -426,9 +438,9 @@ Additional Examples
     ++++++
     footer
 
-    ...
-    column = col-lg-12 p-3
-    card += bg-success text-center
+    ---
+    :column: col-lg-12 p-3
+    :card: + bg-success text-center
 
     Content of the bottom panel
 
diff --git a/sphinx_panels/__init__.py b/sphinx_panels/__init__.py
index fd162ed..a01a034 100644
--- a/sphinx_panels/__init__.py
+++ b/sphinx_panels/__init__.py
@@ -1,10 +1,9 @@
 """"A small sphinx extension to add a ``panels`` directive.
 
-This directive creates panels of content in a 2 x N layout.
+This directive creates panels of content in an M x N layout.
 The panels are separated by `---`::
 
     .. panels::
-        :centred:
 
         Content of the top-left panel
 
@@ -21,7 +20,6 @@ The panels are separated by `---`::
         Content of the bottom-right panel
 
 The content can be any valid rST.
-`:centred:` indicates that the panel contents should be horizontally centred.
 """
 import os
 import re
@@ -29,15 +27,15 @@ from docutils import nodes
 from docutils.parsers.rst import directives
 from sphinx.util.docutils import SphinxDirective
 
-__version__ = "0.2.0"
+__version__ = "0.3.0"
 
 DEFAULT_CONTAINER = "container pb-4"
-DEFAULT_COLUMN = "col-lg-6 col-md-6 col-sm-6 col-xs-12"
+DEFAULT_COLUMN = "col-lg-6 col-md-6 col-sm-6 col-xs-12 p-2"
 DEFAULT_CARD = "shadow"
 
 RE_OPTIONS = re.compile(
-    r"(column|card|body|header|footer|"
-    r"img-top|img-bottom|img-top-cls|img-bottom-cls)\s*(\+?=)\s*(.*)"
+    r"\:(column|card|body|header|footer|"
+    r"img-top|img-bottom|img-top-cls|img-bottom-cls)\:\s*(\+?)\s*(.*)"
 )
 
 LOCAL_FOLDER = os.path.dirname(os.path.abspath(__file__))
@@ -47,17 +45,12 @@ def parse_panels(
     content,
     content_offset,
     default_classes,
-    panel_char=".",
-    head_char="^",
-    foot_char="+",
+    panel_regex=None,
+    head_regex=None,
+    foot_regex=None,
 ):
     """split a block of content into panels.
 
-    - each panel is split by ``---``
-    - an initial ``---`` before the first panel is optional
-    - within each panel, the content can be further split into a header (before ``===``)
-      and footer (after ``...``)
-
     example::
 
         ---
@@ -70,6 +63,10 @@ def parse_panels(
         next panel
 
     """
+    panel_regex = panel_regex or re.compile(r"^\-{3,}$")
+    head_regex = head_regex or re.compile(r"^\^{3,}$")
+    foot_regex = foot_regex or re.compile(r"^\+{3,}$")
+
     if isinstance(content, str):
         content = content.splitlines()
 
@@ -77,7 +74,7 @@ def parse_panels(
     start_line = 0
     header_split = footer_split = None
     for i, line in enumerate(content):
-        if line.startswith(panel_char * 3):
+        if panel_regex.match(line.strip()):
             if i != 0:
                 panel_blocks.append(
                     parse_single_panel(
@@ -91,9 +88,9 @@ def parse_panels(
                 )
             start_line = i + 1
             header_split = footer_split = None
-        if line.startswith(head_char * 3) and footer_split is None:
+        if head_regex.match(line.strip()) and footer_split is None:
             header_split = i - start_line
-        if line.startswith(foot_char * 3):
+        if foot_regex.match(line.strip()):
             footer_split = i - start_line
         # TODO warn if multiple header_split or footer_split
         # TODO assert header_split is before footer_split
@@ -131,7 +128,7 @@ def parse_single_panel(
         if opt_match.group(1) in ["img-top", "img-bottom"]:
             output[opt_match.group(1)] = opt_match.group(3)
             continue
-        if opt_match.group(2) == "+=":
+        if opt_match.group(2) == "+":
             classes[opt_match.group(1)] = (
                 classes.get(opt_match.group(1), []) + opt_match.group(3).split()
             )
@@ -185,30 +182,39 @@ class Panels(SphinxDirective):
     }
 
     def run(self):
-
-        container_classes = self.options.get("container", DEFAULT_CONTAINER).split()
         default_classes = {
-            "column": self.options.get("column", DEFAULT_COLUMN).split(),
-            "card": self.options.get("card", DEFAULT_CARD).split(),
-            "body": self.options.get("body", "").split(),
-            "header": self.options.get("header", "").split(),
-            "footer": self.options.get("footer", "").split(),
-            "img-top-cls": self.options.get("img-top-cls", "").split(),
-            "img-bottom-cls": self.options.get("img-bottom-cls", "").split(),
+            "container": DEFAULT_CONTAINER.split(),
+            "column": DEFAULT_COLUMN.split(),
+            "card": DEFAULT_CARD.split(),
+            "body": [],
+            "header": [],
+            "footer": [],
+            "img-top-cls": [],
+            "img-bottom-cls": [],
         }
 
+        # set classes from the directive options
+        for key, value in default_classes.items():
+            if key not in self.options:
+                continue
+            option_value = self.options[key].strip()
+            if option_value.startswith("+"):
+                default_classes[key] += option_value[1:].split()
+            else:
+                default_classes[key] = option_value.split()
+
         # split the block into panels
         panel_blocks = parse_panels(
             self.content,
             self.content_offset,
             default_classes,
-            panel_char=self.env.app.config.panels_delimiters[0],
-            head_char=self.env.app.config.panels_delimiters[1],
-            foot_char=self.env.app.config.panels_delimiters[2],
+            panel_regex=self.env.app.config.panels_delimiters[0],
+            head_regex=self.env.app.config.panels_delimiters[1],
+            foot_regex=self.env.app.config.panels_delimiters[2],
         )
 
         # set the top-level containers
-        parent = nodes.container(in_panel=True, classes=container_classes)
+        parent = nodes.container(in_panel=True, classes=default_classes["container"])
         rows = nodes.container(in_panel=True, classes=["row"])
         parent += rows
 
@@ -284,11 +290,16 @@ def validate_config(app, config):
         )
     if len(set(app.config.panels_delimiters)) != 3:
         raise AssertionError("panels_delimiters config must contain unique values")
-    for delim in app.config.panels_delimiters:
-        if not (isinstance(delim, str) and len(delim) == 1):
-            raise AssertionError(
-                "panels_delimiters config must contain only length 1 strings"
+    try:
+        app.config.panels_delimiters = tuple(
+            [re.compile(s) for s in app.config.panels_delimiters]
+        )
+    except Exception as err:
+        raise AssertionError(
+            "panels_delimiters config must contain only compilable regexes: {}".format(
+                err
             )
+        )
 
 
 def add_static_paths(app):
@@ -313,7 +324,9 @@ def depart_container(self, node):
 
 def setup(app):
     app.add_directive("panels", Panels)
-    app.add_config_value("panels_delimiters", (".", "^", "+"), "env")
+    app.add_config_value(
+        "panels_delimiters", (r"^\-{3,}$", r"^\^{3,}$", r"^\+{3,}$"), "env"
+    )
     app.connect("config-inited", validate_config)
     app.add_config_value("panels_add_boostrap_css", True, "env")
     app.connect("builder-inited", add_static_paths)
@@ -323,4 +336,8 @@ def setup(app):
         nodes.container, override=True, html=(visit_container, depart_container)
     )
 
-    return {"version": "0.1", "parallel_read_safe": True, "parallel_write_safe": True}
+    return {
+        "version": __version__,
+        "parallel_read_safe": True,
+        "parallel_write_safe": True,
+    }


Test patch:
diff --git a/tests/test_panels.py b/tests/test_panels.py
index bf9f1b6..2ae5bfd 100644
--- a/tests/test_panels.py
+++ b/tests/test_panels.py
@@ -7,15 +7,15 @@ from sphinx_panels import parse_panels
     "content,expected",
     (
         ("a", [{"body": (["a"], 0)}]),
-        ("...\na", [{"body": (["a"], 1)}]),
+        ("---\na", [{"body": (["a"], 1)}]),
         ("a\n^^^", [{"body": ([], 2), "header": (["a"], 0)}]),
         ("a\n+++", [{"body": (["a"], 0), "footer": ([], 1)}]),
         (
             "a\n^^^\nb\n+++\nc",
             [{"body": (["b"], 2), "footer": (["c"], 3), "header": (["a"], 0)}],
         ),
-        ("...\ncard = a", [{"body": ([], 2), "classes": {"card": ["a"]}}]),
-        ("a\n...\nb", [{"body": (["a"], 0)}, {"body": (["b"], 2)}]),
+        ("---\n:card: a", [{"body": ([], 2), "classes": {"card": ["a"]}}]),
+        ("a\n---\nb", [{"body": (["a"], 0)}, {"body": (["b"], 2)}]),
     ),
 )
 def test_parse_panels(content, expected):