--- a
+++ b/docs/scripts/cards.py
@@ -0,0 +1,276 @@
+"""
+Adapted from pymdownx.tabbed (https://github.com/facelessuser/pymdown-extensions/)
+"""
+import re
+import xml.etree.ElementTree as etree
+
+from markdown import Extension
+from markdown.blockprocessors import BlockProcessor
+from markdown.extensions.attr_list import AttrListTreeprocessor, get_attrs
+
+
+def assign_attrs(elem, attrs):
+    """Assign `attrs` to element."""
+    for k, v in get_attrs(attrs):
+        if k == ".":
+            # add to class
+            cls = elem.get("class")
+            if cls:
+                elem.set("class", "{} {}".format(cls, v))
+            else:
+                elem.set("class", v)
+        else:
+            # assign attribute `k` with `v`
+            elem.set(AttrListTreeprocessor.NAME_RE.sub("_", k), v)
+
+
+class CardProcessor(BlockProcessor):
+    """card block processor."""
+
+    START = re.compile(r"(?:^|\n)={3} *(card)?(?: +({:.*?}) *(?:\n|$))?")
+    COMPRESS_SPACES = re.compile(r" {2,}")
+
+    def __init__(self, parser, config):
+        """Initialize."""
+
+        super().__init__(parser)
+        self.card_group_count = 0
+        self.current_sibling = None
+        self.content_indention = 0
+
+    def detab_by_length(self, text, length):
+        """Remove a card from the front of each line of the given text."""
+
+        newtext = []
+        lines = text.split("\n")
+        for line in lines:
+            if line.startswith(" " * length):
+                newtext.append(line[length:])
+            elif not line.strip():
+                newtext.append("")  # pragma: no cover
+            else:
+                break
+        return "\n".join(newtext), "\n".join(lines[len(newtext) :])
+
+    def parse_content(self, parent, block):
+        """
+        Get sibling card.
+
+        Retrieve the appropriate sibling element. This can get tricky when
+        dealing with lists.
+
+        """
+
+        old_block = block
+        non_cards = ""
+        card_set = "card-set"
+
+        # We already acquired the block via test
+        if self.current_sibling is not None:
+            sibling = self.current_sibling
+            block, non_cards = self.detab_by_length(block, self.content_indent)
+            self.current_sibling = None
+            self.content_indent = 0
+            return sibling, block, non_cards
+
+        sibling = self.lastChild(parent)
+
+        if (
+            sibling is None
+            or sibling.tag.lower() != "div"
+            or sibling.attrib.get("class", "") != card_set
+        ):
+            sibling = None
+        else:
+            # If the last child is a list and the content is indented sufficient
+            # to be under it, then the content's is sibling is in the list.
+            last_child = self.lastChild(sibling)
+            card_content = "card-content"
+            child_class = (
+                last_child.attrib.get("class", "") if last_child is not None else ""
+            )
+            indent = 0
+            while last_child is not None:
+                if (
+                    sibling is not None
+                    and block.startswith(" " * self.tab_length * 2)
+                    and last_child is not None
+                    and (
+                        last_child.tag in ("ul", "ol", "dl")
+                        or (last_child.tag == "div" and child_class == card_content)
+                    )
+                ):
+                    # Handle nested card content
+                    if last_child.tag == "div" and child_class == card_content:
+                        temp_child = self.lastChild(last_child)
+                        if temp_child is None or temp_child.tag not in (
+                            "ul",
+                            "ol",
+                            "dl",
+                        ):
+                            break
+                        last_child = temp_child
+
+                    # The expectation is that we'll find an `<li>`.
+                    # We should get it's last child as well.
+                    sibling = self.lastChild(last_child)
+                    last_child = (
+                        self.lastChild(sibling) if sibling is not None else None
+                    )
+                    child_class = (
+                        last_child.attrib.get("class", "")
+                        if last_child is not None
+                        else ""
+                    )
+
+                    # Context has been lost at this point, so we must adjust the
+                    # text's indentation level so it will be evaluated correctly
+                    # under the list.
+                    block = block[self.tab_length :]
+                    indent += self.tab_length
+                else:
+                    last_child = None
+
+            if not block.startswith(" " * self.tab_length):
+                sibling = None
+
+            if sibling is not None:
+                indent += self.tab_length
+                block, non_cards = self.detab_by_length(old_block, indent)
+                self.current_sibling = sibling
+                self.content_indent = indent
+
+        return sibling, block, non_cards
+
+    def test(self, parent, block):
+        """Test block."""
+
+        if self.START.search(block):
+            return True
+        else:
+            return self.parse_content(parent, block)[0] is not None
+
+    def run(self, parent, blocks):
+        """Convert to card block."""
+
+        block = blocks.pop(0)
+        m = self.START.search(block)
+        card_set = "card-set"
+
+        if m:
+            # removes the first line
+            if m.start() > 0:
+                self.parser.parseBlocks(parent, [block[: m.start()]])
+            block = block[m.end() :]
+            sibling = self.lastChild(parent)
+            block, non_cards = self.detab(block)
+        else:
+            sibling, block, non_cards = self.parse_content(parent, block)
+
+        if m:
+            if (
+                sibling is not None
+                and sibling.tag.lower() == "div"
+                and sibling.attrib.get("class", "") == card_set
+            ):
+                card_group = sibling
+            else:
+                self.card_group_count += 1
+                card_group = etree.SubElement(
+                    parent,
+                    "div",
+                    {
+                        "class": card_set,
+                        "data-cards": "%d:0" % self.card_group_count,
+                    },
+                )
+
+            data = card_group.attrib["data-cards"].split(":")
+            card_set = int(data[0])
+            card_count = int(data[1]) + 1
+
+            div = etree.SubElement(
+                card_group,
+                "div",
+                {
+                    "class": "card-content",
+                },
+            )
+            attributes = m.group(2)
+
+            if attributes:
+                attr_m = AttrListTreeprocessor.INLINE_RE.search(attributes)
+                if attr_m:
+                    assign_attrs(div, attr_m.group(1))
+                    if div.get("href"):
+                        div.tag = "a"
+
+            card_group.attrib["data-cards"] = "%d:%d" % (card_set, card_count)
+        else:
+            if sibling.tag in ("li", "dd") and sibling.text:
+                # Sibling is a list item, but we need to wrap it's content should be
+                # wrapped in <p>
+                text = sibling.text
+                sibling.text = ""
+                p = etree.SubElement(sibling, "p")
+                p.text = text
+                div = sibling
+            elif sibling.tag == "div" and sibling.attrib.get("class", "") == card_set:
+                # Get `card-content` under `card-set`
+                div = self.lastChild(sibling)
+            else:
+                # Pass anything else as the parent
+                div = sibling
+
+        self.parser.parseChunk(div, block)
+
+        if non_cards:
+            # Insert the card content back into blocks
+            blocks.insert(0, non_cards)
+
+
+class CardExtension(Extension):
+    """Add card extension."""
+
+    def __init__(self, *args, **kwargs):
+        """Initialize."""
+
+        self.config = {
+            "slugify": [
+                0,
+                "Slugify function used to create card specific IDs - Default: None",
+            ],
+            "combine_header_slug": [
+                False,
+                "Combine the card slug with the slug of the parent header - "
+                "Default: False",
+            ],
+            "separator": ["-", "Slug separator - Default: '-'"],
+        }
+
+        super(CardExtension, self).__init__(*args, **kwargs)
+
+    def extendMarkdown(self, md):
+        """Add card to Markdown instance."""
+        md.registerExtension(self)
+
+        config = self.getConfigs()
+
+        self.card_processor = CardProcessor(md.parser, config)
+
+        md.parser.blockprocessors.register(
+            self.card_processor,
+            "card",
+            105,
+        )
+
+    def reset(self):
+        """Reset."""
+
+        self.card_processor.card_group_count = 0
+
+
+def makeExtension(*args, **kwargs):
+    """Return extension."""
+
+    return CardExtension(*args, **kwargs)