Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changelog/_unreleased.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,13 @@ type = "fix"
description = "Fix `escape_except_blockquotes` option for greater than 9 blockquotes in a docstring"
author = "@jackgerrits"
pr = "https://github.com/NiklasRosenstein/pydoc-markdown/pull/317"

[[entries]]
id = "f3ee1a25-3e4d-4917-b3b9-7ace18f6f9f0"
type = "improvement"
description = "Add option to escape { in markdown for compatibility with MDX >= 2"
author = "[email protected]"
pr = "https://github.com/NiklasRosenstein/pydoc-markdown/pull/326"
issues = [
"https://github.com/nonprofittechy/pydoc-markdown/issues/325",
]
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ Homepage = "https://github.com/NiklasRosenstein/pydoc-markdown"
[tool.poetry.dependencies]
python = "^3.8"
click = ">=7.1,<9.0"
"databind.core" = "^4.4.2"
"databind.json" = "^4.4.2"
databind = "^4.4.2"
docspec = "^2.2.1"
docspec-python = "^2.2.1"
docstring-parser = "^0.11"
Expand Down
7 changes: 6 additions & 1 deletion src/pydoc_markdown/contrib/renderers/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
SourceLinker,
)
from pydoc_markdown.util.docspec import ApiSuite, format_function_signature, is_method
from pydoc_markdown.util.misc import escape_except_blockquotes
from pydoc_markdown.util.misc import escape_except_blockquotes, escape_curly_brackets


def dotted_name(obj: docspec.ApiObject) -> str:
Expand Down Expand Up @@ -212,6 +212,9 @@ class MarkdownRenderer(Renderer, SinglePageRenderer, SingleObjectRenderer):
#: Escape html in docstring. Default to False.
escape_html_in_docstring: bool = False

#: Escape { and } in docstring. Default to False.
escape_curly_braces_in_docstring: bool = False

#: Render Novella `@anchor` tags before headings.
render_novella_anchors: bool = False

Expand Down Expand Up @@ -374,6 +377,8 @@ def _render_object(self, fp: t.TextIO, level: int, obj: docspec.ApiObject):
if self.escape_html_in_docstring
else obj.docstring.content
)
if self.escape_curly_braces_in_docstring:
docstring = escape_curly_brackets(docstring)
lines = docstring.split("\n")
if self.docstrings_as_blockquote:
lines = ["> " + x for x in lines]
Expand Down
25 changes: 25 additions & 0 deletions src/pydoc_markdown/util/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,28 @@ def escape_except_blockquotes(string: str) -> str:
escaped_string = escaped_string.replace(f"BLOCKQUOTE_TOKEN_{i}_END", match)

return escaped_string

def escape_curly_brackets(string: str) -> str:
"""
Escape curly brackets in a string, except those inside code blocks or inline code.
"""

# Define regex patterns to match code blocks and inline code
single_quote_pattern = r"`[^`]*`"
triple_quote_pattern = r"```[\s\S]*?```"

# Find all code blocks and inline code in the string
code_matches = re.findall(f"({triple_quote_pattern}|{single_quote_pattern})", string)

# Replace all code blocks/inline code with placeholder tokens to preserve their contents
for i, match in enumerate(code_matches):
string = string.replace(match, f"CODE_TOKEN_{i}_END")

# Escape curly brackets in the remaining string
escaped_string = string.replace("{", "\\{").replace("}", "\\}")

# Replace the placeholder tokens with their original contents
for i, match in enumerate(code_matches):
escaped_string = escaped_string.replace(f"CODE_TOKEN_{i}_END", match)

return escaped_string
51 changes: 50 additions & 1 deletion src/pydoc_markdown/util/misc_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydoc_markdown.util.misc import escape_except_blockquotes
from pydoc_markdown.util.misc import escape_except_blockquotes, escape_curly_brackets


def test__escape_except_blockquotes() -> None:
Expand All @@ -22,3 +22,52 @@ def test__escape_except_blockquotes() -> None:
"""
)
)


def test__escape_curly_brackets() -> None:
# Test basic escaping
assert escape_curly_brackets("Hello {world}") == "Hello \\{world\\}"

# Test escaping with inline code - curly brackets should NOT be escaped inside backticks
assert escape_curly_brackets("Use `{variable}` in your code") == "Use `{variable}` in your code"

# Test escaping with code blocks - curly brackets should NOT be escaped inside triple backticks
code_block_input = """Here is an example:

```python
def format_string():
return f"Hello {name}!"
```

But outside code blocks, \\{these\\} should be escaped."""

expected_output = """Here is an example:

```python
def format_string():
return f"Hello {name}!"
```

But outside code blocks, \\\\\\{these\\\\\\} should be escaped."""

assert escape_curly_brackets(code_block_input) == expected_output

# Test mixed case with both inline code and regular text
mixed_input = "Regular {bracket} and `code {bracket}` and more {regular}"
expected_mixed = "Regular \\{bracket\\} and `code {bracket}` and more \\{regular\\}"
assert escape_curly_brackets(mixed_input) == expected_mixed

# Test multiple code blocks
multi_code_input = """First `{inline}` and then:
```
{block_code}
```
And {regular} text."""

expected_multi = """First `{inline}` and then:
```
{block_code}
```
And \\{regular\\} text."""

assert escape_curly_brackets(multi_code_input) == expected_multi