diff --git a/README.rst b/README.rst index 059a68f..87b394d 100644 --- a/README.rst +++ b/README.rst @@ -100,6 +100,12 @@ newline_style newline). While the latter convention is non-standard, it is commonly preferred and supported by a lot of interpreters. +code_fence + Defines the opening and closing fence for ``
`` sections. Use a string
+  of at least three backticks (the default) or at least three tildes.
+  For example, ``code_fence="~~~"`` produces tilde-fenced code blocks.
+  The same option is available as ``--code-fence`` on the command line.
+
 code_language
   Defines the language that should be assumed for all ``
`` sections.
   Useful, if all code on a page is in the same programming language and
diff --git a/markdownify/__init__.py b/markdownify/__init__.py
index 28cdaf6..18281fa 100644
--- a/markdownify/__init__.py
+++ b/markdownify/__init__.py
@@ -178,6 +178,7 @@ class DefaultOptions:
         autolinks = True
         bs4_options = 'html.parser'
         bullets = '*+-'  # An iterable of bullet types.
+        code_fence = '```'
         code_language = ''
         code_language_callback = None
         convert = None
@@ -211,6 +212,10 @@ def __init__(self, **options):
             raise ValueError('You may specify either tags to strip or tags to'
                              ' convert, but not both.')
 
+        code_fence = self.options['code_fence']
+        if not isinstance(code_fence, six.string_types) or not re.match(r'(?:`{3,}|~{3,})\Z', code_fence):
+            raise ValueError('Invalid value for code_fence: %s' % code_fence)
+
         # If a string or list is passed to bs4_options, assume it is a 'features' specification
         if not isinstance(self.options['bs4_options'], dict):
             self.options['bs4_options'] = {'features': self.options['bs4_options']}
@@ -702,7 +707,8 @@ def convert_pre(self, el, text, parent_tags):
         else:
             raise ValueError('Invalid value for strip_pre: %s' % self.options['strip_pre'])
 
-        return '\n\n```%s\n%s\n```\n\n' % (code_language, text)
+        code_fence = self.options['code_fence']
+        return '\n\n%s%s\n%s\n%s\n\n' % (code_fence, code_language, text, code_fence)
 
     def convert_q(self, el, text, parent_tags):
         return '"' + text + '"'
diff --git a/markdownify/__init__.pyi b/markdownify/__init__.pyi
index 2494011..4c3dba8 100644
--- a/markdownify/__init__.pyi
+++ b/markdownify/__init__.pyi
@@ -39,6 +39,7 @@ def markdownify(
     table_infer_header: bool = ...,
     wrap: bool = ...,
     wrap_width: Union[int, None] = ...,
+    code_fence: str = ...,
 ) -> str: ...
 
 
@@ -67,6 +68,7 @@ class MarkdownConverter:
         table_infer_header: bool = ...,
         wrap: bool = ...,
         wrap_width: Union[int, None] = ...,
+        code_fence: str = ...,
     ) -> None:
         ...
   
diff --git a/markdownify/main.py b/markdownify/main.py
index ba70671..1145502 100755
--- a/markdownify/main.py
+++ b/markdownify/main.py
@@ -46,6 +46,9 @@ def main(argv=sys.argv[1:]):
                         choices=(SPACES, BACKSLASH),
                         help="Defines the style of 
conversions: two spaces " "or backslash at the and of the line thet should break.") + parser.add_argument('--code-fence', default='```', + help="Fence for '
' sections: at least three backticks "
+                        "or at least three tildes. Defaults to three backticks.")
     parser.add_argument('--code-language', default='',
                         help="Defines the language that should be assumed for all "
                         "'
' sections.")
diff --git a/tests/test_code_fence.py b/tests/test_code_fence.py
new file mode 100644
index 0000000..4b346ae
--- /dev/null
+++ b/tests/test_code_fence.py
@@ -0,0 +1,62 @@
+import pytest
+
+from markdownify import MarkdownConverter, STRIP, STRIP_ONE, markdownify
+
+
+@pytest.mark.parametrize('fence', ['```', '~~~', '````', '~~~~'])
+def test_code_fence(fence):
+    assert markdownify('
hello\nworld
', code_fence=fence) == ( + fence + '\nhello\nworld\n' + fence + ) + + +def test_tilde_fence_with_language(): + assert markdownify('
print(1)
', code_fence='~~~', code_language='python') == ( + '~~~python\nprint(1)\n~~~' + ) + + +def test_tilde_fence_with_language_callback(): + assert markdownify( + '
print(1)
', + code_fence='~~~', + code_language_callback=lambda el: el['class'][0], + ) == '~~~python\nprint(1)\n~~~' + + +@pytest.mark.parametrize('strip_pre, content', [ + (None, '\n\nhello\n\n'), + (STRIP, 'hello'), + (STRIP_ONE, '\nhello'), +]) +def test_tilde_fence_preserves_strip_pre(strip_pre, content): + assert markdownify( + '
\n\nhello\n\n
', code_fence='~~~', strip_pre=strip_pre, + ) == '~~~\n' + content + '\n~~~' + + +def test_code_fence_subclass_option(): + class TildeConverter(MarkdownConverter): + class Options: + code_fence = '~~~' + + assert TildeConverter().convert('
hello
') == '~~~\nhello\n~~~' + + +def test_tilde_fence_does_not_change_inline_code_or_empty_pre(): + assert markdownify('hello
', code_fence='~~~') == '`hello`'
+
+
+@pytest.mark.parametrize('fence', ['', '``', '~~', '```~', '~~~\n', 'abc', None, 3])
+def test_invalid_code_fence(fence):
+    with pytest.raises(ValueError, match='Invalid value for code_fence'):
+        MarkdownConverter(code_fence=fence)
+
+
+def test_code_fence_cli(monkeypatch, capsys):
+    from io import StringIO
+    from markdownify.main import main
+
+    monkeypatch.setattr('sys.stdin', StringIO('
print(1)
')) + main(['--code-fence', '~~~', '--code-language', 'python']) + assert capsys.readouterr().out == '~~~python\nprint(1)\n~~~\n' diff --git a/tests/types.py b/tests/types.py index 7424978..54106fb 100644 --- a/tests/types.py +++ b/tests/types.py @@ -13,6 +13,7 @@ autolinks=True, bs4_options='html.parser', bullets='*+-', + code_fence='```', code_language='', code_language_callback=None, convert=None, @@ -38,6 +39,7 @@ MarkdownConverter( strip_document=None, bullets="-", + code_fence="~~~", escape_asterisks=True, escape_underscores=True, escape_misc=True, @@ -68,3 +70,5 @@ def callback(el: BeautifulSoup) -> Union[str, None]: markdownify('
test\n    foo\nbar
', code_language_callback=callback) markdownify('
test\n    foo\nbar
', code_language_callback=lambda el: None) + +markdownify("
test
", code_fence="~~~") == "~~~\ntest\n~~~"