diff --git a/Authors.rst b/Authors.rst
index 9610662..7052407 100644
--- a/Authors.rst
+++ b/Authors.rst
@@ -12,3 +12,4 @@ Contributors
* Dave MacNamara
* Justin Michalicek
+* Erin Kirby
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 993a6dc..dabaf36 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -101,7 +101,7 @@ Before you submit a pull request, check that it meets these guidelines:
2. If the pull request adds functionality, the docs should be updated. Put
your new functionality into a function with a docstring, and add the
feature to the list in README.rst.
-3. The pull request should work for Python 2.7, 3.2, 3.3, 3.4 and 3.5, and for PyPy. Check
+3. The pull request should work for Python 2.7, 3.4, 3.5, 3.6 and 3.7, and for PyPy. Check
https://travis-ci.org/livio/DocDown-Python/pull_requests
and make sure that the tests pass for all supported Python versions.
diff --git a/HISTORY.rst b/HISTORY.rst
index 7c254a3..2ca04ed 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -2,6 +2,16 @@
History
=======
+0.3.1 (2021-04-20)
+------------------
+
+* Place a hard cap below 3.0 on Markdown to address compatibility issues
+
+0.3.0 (2021-04-19)
+------------------
+
+* Add Scoped Code Tabs Markdown Extension
+
0.2.7 (2019-11-20)
------------------
diff --git a/docdown/__init__.py b/docdown/__init__.py
index de04dd3..18c5518 100644
--- a/docdown/__init__.py
+++ b/docdown/__init__.py
@@ -2,4 +2,4 @@
__author__ = """Jason Emerick"""
__email__ = 'jason@mobelux.com'
-__version__ = '0.2.7'
+__version__ = '0.3.1'
diff --git a/docdown/scoped_code_tabs.py b/docdown/scoped_code_tabs.py
new file mode 100644
index 0000000..48b9fa0
--- /dev/null
+++ b/docdown/scoped_code_tabs.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+
+"""
+scoped_code_tabs
+----------------------------------
+
+docdown.scoped_code_tabs Markdown extension module
+"""
+import re
+
+from markdown.preprocessors import Preprocessor
+from markdown_fenced_code_tabs import CodeTabsExtension
+
+
+class ScopedCodeTabsPreprocessor(Preprocessor):
+ RE_FENCE_START = r'^ *\|\~\s*$' # start line, e.g., ` |~ `
+ RE_FENCE_END = r'^\s*\~\|\s*$' # last non-blank line, e.g, '~|\n \n\n'
+
+ def __init__(self, md, code_tabs_preprocessor):
+ self.code_tabs_preprocessor = code_tabs_preprocessor
+ super(ScopedCodeTabsPreprocessor, self).__init__(md)
+
+ def run(self, lines):
+ new_lines = []
+ fenced_code_tab = []
+ starting_line = None
+ in_tab = False
+
+ for line in lines:
+ if re.search(self.RE_FENCE_START, line):
+ # Start block pattern, save line in case of no end fence
+ in_tab = True
+ starting_line = line
+ elif re.search(self.RE_FENCE_END, line):
+ # End of code block, run through fenced code tabs pre-processor and reset code tab list
+ new_lines += self.code_tabs_preprocessor.run(fenced_code_tab)
+ fenced_code_tab = []
+ in_tab = False
+ elif in_tab:
+ # Still in tab -- append to tab list
+ fenced_code_tab.append(line)
+ else:
+ # Not in a fenced code tab, and not starting/ending one -- pass as usual
+ new_lines.append(line)
+
+ # Non-terminated code tab block, append matching starting fence and remaining lines without processing
+ if fenced_code_tab:
+ new_lines += [starting_line] + fenced_code_tab
+ return new_lines
+
+
+class ScopedCodeTabExtension(CodeTabsExtension):
+
+ def __init__(self, **kwargs):
+ """
+ A Markdown extension that serves to scope where Fenced Code Tabs are rendered by way of |~ ... ~| fences.
+
+ Example:
+
+ ## A set of code tabs in Python and Java
+ |~
+ ```python
+ def main():
+ print("This would be passed through markdown_fenced_code_tabs")
+ ```
+
+ ```java
+ public static void main(String[] args) {
+ System.out.println("This would be passed through markdown_fenced_code_tabs");
+ }
+ ```
+ ~|
+
+ ## A regular, non-tabbed code block in Bash
+ ```bash
+ codeblockinfo() {
+ echo("This would NOT be passed through markdown_fenced_code tabs");
+ }
+ ```
+ """
+ super(ScopedCodeTabExtension, self).__init__(**kwargs)
+
+ def extendMarkdown(self, md, md_globals):
+ super(ScopedCodeTabExtension, self).extendMarkdown(md, md_globals)
+ md.registerExtension(self)
+
+ md.preprocessors.add('scoped_code_tabs',
+ ScopedCodeTabsPreprocessor(md,
+ code_tabs_preprocessor=md.preprocessors['fenced_code_block']),
+ ">normalize_whitespace")
+ del md.preprocessors['fenced_code_block']
+
+
+def makeExtension(*args, **kwargs):
+ return ScopedCodeTabExtension(*args, **kwargs)
diff --git a/docs/docdown.rst b/docs/docdown.rst
index 10806d9..2ee0d48 100644
--- a/docs/docdown.rst
+++ b/docs/docdown.rst
@@ -59,6 +59,14 @@ docdown.platform_section module
:undoc-members:
:show-inheritance:
+docdown.scoped_code_tabs module
+-------------------------------
+
+.. automodule:: docdown.scoped_code_tabs
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
docdown.sequence module
-----------------------
diff --git a/docs/extensions/scoped_code_tabs.rst b/docs/extensions/scoped_code_tabs.rst
new file mode 100644
index 0000000..ebd137a
--- /dev/null
+++ b/docs/extensions/scoped_code_tabs.rst
@@ -0,0 +1,109 @@
+######################
+Scoped Code Tabs
+######################
+
+Scoped Code Tabs allows for the explicit annotation of when and where to tabulate a set of code blocks versus rendering them
+separately.
+
+A scoped code tab is delimited by an opening ``|~`` fence and closing ``~|`` fence. The code blocks within the fences
+are defined as typical code blocks, using backticks, with the opening backtick fence specifying the code language contained
+within the block.
+
+The configuration for rendering the tabs is as directly defined by the `markdown_fenced_code_tabs`_ extension.
+
+
+=============
+Dependencies
+=============
+The ``docdown.scoped_code_tabs`` extension requires the third-party extension `markdown_fenced_code_tabs`_ in order to process
+the tabulated fenced code blocks.
+
+==============
+Configuration
+==============
+
+single_block_as_tab
+ Whether a single code block should still be rendered as a code tab. Default: ``False``
+active_class
+ The CSS class to apply to the active tab. Default: ``active``
+template
+ Which template to use to render code tabs. One of: [``default``, ``bootstrap3``, ``bootstrap4``]. Default: ``default``
+ Please see the *-template.html files in the `markdown_fenced_code_tabs`_ extension.
+
+
+=======
+Usage
+=======
+In documents
+-------------
+
+.. code-block:: md
+
+ ### Hello World Examples
+ |~
+ ```bash
+ helloWorld() {
+ greeting=${1:-World}
+ echo(`Hello ${greeting}`)
+ }
+ ```
+ ~|
+
+ ```python
+ def hello_world(greeting: str = "World") -> None:
+ print(f"Hello {greeting}")
+ ```
+
+Python
+--------------
+
+.. code-block:: python
+
+ config = {
+ 'docdown.scoped_code_tabs': {
+ 'single_block_as_tab': True,
+ 'template': 'bootstrap4',
+ 'active_class': 'tab-active'
+ }
+ }
+
+ text = """\
+ ### Hello World Examples
+ |~
+ ```bash
+ helloWorld() {
+ greeting=${1:-World}
+ echo(`Hello ${greeting}`)
+ }
+ ```
+ ~|
+ ```python
+ def hello_world(greeting: str = "World") -> None:
+ print(f"Hello {greeting}")
+ ```
+ """
+
+ html = markdown.markdown(
+ text,
+ extensions=['docdown.scoped_code_tabs'],
+ extension_configs=config,
+ output_format='html5')
+
+=======
+Output
+=======
+Note the extra classes and divs for tabulation around the ``|~`` ``~|`` code block.
+
+.. code-block:: html
+
+ Hello World Examples
+
helloWorld() {
+ greeting=${1:-World}
+ echo(`Hello ${greeting}`)
+ }
+
+ python
+ def hello_world(greeting: str = "World") -> None:
+ print(f"Hello {greeting}")
+
+.. _`markdown_fenced_code_tabs`: https://github.com/yacir/markdown-fenced-code-tabs
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 09c5b14..4a53d9d 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,14 +1,15 @@
pip==9.0.1
bumpversion==0.5.3
wheel==0.29.0
-watchdog==0.8.3
+#watchdog==0.8.3
flake8==2.6.0
tox==2.5.0
coverage==4.1
Sphinx==1.4.8
twine==1.13.0
-Markdown==2.6.6
+Markdown<3.0.0
+markdown-fenced-code-tabs==1.0.5
unicodecsv==0.14.1
# note_block templating
diff --git a/setup.cfg b/setup.cfg
index e9fb5e3..151e76f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.2.7
+current_version = 0.3.1
commit = True
tag = True
diff --git a/setup.py b/setup.py
index 21417ff..0550dfc 100644
--- a/setup.py
+++ b/setup.py
@@ -11,7 +11,9 @@
requirements = [
# TODO: put package requirements here
+ 'Markdown < 3.0.0',
'unicodecsv >= 0.14.1',
+ 'markdown-fenced-code-tabs >= 1.0.5',
]
test_requirements = [
@@ -20,7 +22,7 @@
setup(
name='docdown',
- version='0.2.7',
+ version='0.3.1',
description="DocDown is a Markdown extension for source code documentation.",
long_description=readme + '\n\n' + history,
author="Jason Emerick, Justin Michalicek",
@@ -42,9 +44,10 @@
"Programming Language :: Python :: 2",
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
],
test_suite='tests',
tests_require=test_requirements
diff --git a/tests/test_platform_section_extension.py b/tests/test_platform_section_extension.py
index 5bca1e3..912c653 100644
--- a/tests/test_platform_section_extension.py
+++ b/tests/test_platform_section_extension.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
"""
-test_note_blocks_extension
+test_platform_section_extension
----------------------------------
-Tests for `docdown.note_blocks` module.
+Tests for `docdown.platform_section` module.
"""
from __future__ import absolute_import, print_function, unicode_literals
diff --git a/tests/test_scoped_code_tabs.py b/tests/test_scoped_code_tabs.py
new file mode 100644
index 0000000..6b93f2c
--- /dev/null
+++ b/tests/test_scoped_code_tabs.py
@@ -0,0 +1,133 @@
+# flake8: noqa E501
+import unittest
+
+import markdown
+
+
+class ScopedCodeTabsExtensionTest(unittest.TestCase):
+ """
+ Integration test with markdown for :class:`docdown.scoped_code_tabs.ScopedCodeTabsExtension`
+ """
+ MARKDOWN_EXTENSIONS = ['docdown.scoped_code_tabs']
+
+ def test_mixed_code_blocks(self):
+ """
+ Tests mixed code blocks to ensure extension is only run on those with |~ ... ~| fences
+ """
+ text = """\
+### A set of code tabs in Python and Java
+[comment]: # (This should render as two code tabs)
+|~
+```python
+def main():
+ print("This would be passed through markdown_fenced_code_tabs")
+```
+
+```java
+public static void main(String[] args) {
+ System.out.println("This would be passed through markdown_fenced_code_tabs");
+}
+```
+~|
+
+### A regular, non-tabbed code block in Bash
+[comment]: # (This should render as two, non-tabbed code blocks)
+```bash
+codeblockinfo() {
+ echo("This would NOT be passed through markdown_fenced_code_tabs");
+}
+```
+
+```clojure
+(defn code-block-info []
+ (println "This should also render as a normal code block"))
+(hello-world)
+```
+
+#### (White-space fences are OK)
+[comment]: # (This should render as two more fenced code tabs even with whitespace around the fences)
+ |~
+```html
+
+
+
+ Hello {{ greeting|default:"World" }}!
+
+
+```
+
+```python
+def hello_world(name: str = None):
+ greeting = name or 'World'
+ return f'Hello {greeting}!'
+```
+ ~|
+"""
+ expected_output = """\
+A set of code tabs in Python and Java
+
+
+A regular, non-tabbed code block in Bash
+bash
+codeblockinfo() {
+ echo("This would NOT be passed through markdown_fenced_code_tabs");
+}
+clojure
+(defn code-block-info []
+ (println "This should also render as a normal code block"))
+(hello-world)
+(White-space fences are OK)
+"""
+
+ html = markdown.markdown(
+ text,
+ extensions=['docdown.scoped_code_tabs'],
+ output_format='html5')
+
+ self.maxDiff = len(html) * 2
+ self.assertEqual(html, expected_output)
+
+ def test_custom_config_values(self):
+ config = {
+ 'docdown.scoped_code_tabs': {
+ 'single_block_as_tab': True,
+ 'template': 'bootstrap4'
+ }
+ }
+
+ text = """\
+|~
+```python
+def hello_world(greeting: str = 'World'):
+ return f'Hello {greeting}!'
+```
+ ~|
+"""
+
+ expected_output = """\
+
def hello_world(greeting: str = 'World'):
+ return f'Hello {greeting}!'
+
"""
+
+ html = markdown.markdown(
+ text,
+ extensions=['docdown.scoped_code_tabs'],
+ extension_configs=config,
+ output_format='html5')
+
+ self.maxDiff = len(html) * 2
+ self.assertEqual(html, expected_output)