Skip to content

Commit 7f34252

Browse files
committed
fix(rst): Escape asterisks in glob patterns to prevent emphasis warnings
Glob patterns like "django-*" in argparse help text trigger RST "Inline emphasis start-string without end-string" warnings. Add escape_rst_emphasis() utility that escapes problematic asterisks (e.g., "django-*" → "django-\*") and apply it in the renderer's _parse_text() method.
1 parent 77c4393 commit 7f34252

3 files changed

Lines changed: 132 additions & 1 deletion

File tree

docs/_ext/sphinx_argparse_neo/renderer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
ParserInfo,
2727
SubcommandInfo,
2828
)
29+
from sphinx_argparse_neo.utils import escape_rst_emphasis
2930

3031
if t.TYPE_CHECKING:
3132
from docutils.parsers.rst.states import RSTState
@@ -481,6 +482,9 @@ def _parse_text(self, text: str) -> list[nodes.Node]:
481482
if not text:
482483
return []
483484

485+
# Escape RST emphasis patterns before parsing (e.g., "django-*" -> "django-\*")
486+
text = escape_rst_emphasis(text)
487+
484488
if self.state is None:
485489
# No state machine available, return as paragraph
486490
para = nodes.paragraph(text=text)

docs/_ext/sphinx_argparse_neo/utils.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,40 @@ def strip_ansi(text: str) -> str:
3939
'bold blue'
4040
"""
4141
return _ANSI_RE.sub("", text)
42+
43+
44+
# RST emphasis pattern: matches -* that would trigger inline emphasis errors.
45+
# Pattern matches: non-whitespace/non-backslash char, followed by -*, NOT followed by
46+
# another * (which would be strong emphasis **).
47+
_RST_EMPHASIS_RE = re.compile(r"(?<=[^\s\\])-\*(?!\*)")
48+
49+
50+
def escape_rst_emphasis(text: str) -> str:
51+
r"""Escape asterisks that would trigger RST inline emphasis.
52+
53+
RST interprets ``*text*`` as emphasis. When argparse help text contains
54+
glob patterns like ``django-*``, the ``-*`` sequence triggers RST
55+
"Inline emphasis start-string without end-string" warnings.
56+
57+
This function escapes such asterisks to prevent RST parsing errors.
58+
59+
Parameters
60+
----------
61+
text : str
62+
Text potentially containing problematic asterisks.
63+
64+
Returns
65+
-------
66+
str
67+
Text with asterisks escaped where needed.
68+
69+
Examples
70+
--------
71+
>>> escape_rst_emphasis('tmuxp load "my-*"')
72+
'tmuxp load "my-\\*"'
73+
>>> escape_rst_emphasis("plain text")
74+
'plain text'
75+
>>> escape_rst_emphasis("*emphasis* is ok")
76+
'*emphasis* is ok'
77+
"""
78+
return _RST_EMPHASIS_RE.sub(r"-\*", text)

tests/docs/_ext/sphinx_argparse_neo/test_utils.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import typing as t
66

77
import pytest
8-
from sphinx_argparse_neo.utils import strip_ansi
8+
from sphinx_argparse_neo.utils import escape_rst_emphasis, strip_ansi
99

1010
# --- strip_ansi tests ---
1111

@@ -70,3 +70,93 @@ class StripAnsiFixture(t.NamedTuple):
7070
def test_strip_ansi(test_id: str, input_text: str, expected: str) -> None:
7171
"""Test ANSI escape code stripping."""
7272
assert strip_ansi(input_text) == expected
73+
74+
75+
# --- escape_rst_emphasis tests ---
76+
77+
78+
class EscapeRstEmphasisFixture(t.NamedTuple):
79+
"""Test fixture for escape_rst_emphasis function."""
80+
81+
test_id: str
82+
input_text: str
83+
expected: str
84+
85+
86+
ESCAPE_RST_EMPHASIS_FIXTURES: list[EscapeRstEmphasisFixture] = [
87+
EscapeRstEmphasisFixture(
88+
test_id="glob_pattern_quoted",
89+
input_text='tmuxp load "my-*"',
90+
expected='tmuxp load "my-\\*"',
91+
),
92+
EscapeRstEmphasisFixture(
93+
test_id="glob_pattern_django",
94+
input_text="django-*",
95+
expected="django-\\*",
96+
),
97+
EscapeRstEmphasisFixture(
98+
test_id="glob_pattern_flask",
99+
input_text="flask-*",
100+
expected="flask-\\*",
101+
),
102+
EscapeRstEmphasisFixture(
103+
test_id="multiple_patterns",
104+
input_text="match django-* or flask-* packages",
105+
expected="match django-\\* or flask-\\* packages",
106+
),
107+
EscapeRstEmphasisFixture(
108+
test_id="plain_text",
109+
input_text="plain text without patterns",
110+
expected="plain text without patterns",
111+
),
112+
EscapeRstEmphasisFixture(
113+
test_id="rst_emphasis_unchanged",
114+
input_text="*emphasis* is ok",
115+
expected="*emphasis* is ok",
116+
),
117+
EscapeRstEmphasisFixture(
118+
test_id="already_escaped",
119+
input_text="django-\\*",
120+
expected="django-\\*",
121+
),
122+
EscapeRstEmphasisFixture(
123+
test_id="empty_string",
124+
input_text="",
125+
expected="",
126+
),
127+
EscapeRstEmphasisFixture(
128+
test_id="pattern_at_end",
129+
input_text="ending with pattern-*",
130+
expected="ending with pattern-\\*",
131+
),
132+
EscapeRstEmphasisFixture(
133+
test_id="hyphen_without_asterisk",
134+
input_text="word-with-hyphens",
135+
expected="word-with-hyphens",
136+
),
137+
EscapeRstEmphasisFixture(
138+
test_id="asterisk_without_hyphen",
139+
input_text="asterisk * alone",
140+
expected="asterisk * alone",
141+
),
142+
EscapeRstEmphasisFixture(
143+
test_id="double_asterisk",
144+
input_text="glob-** pattern",
145+
expected="glob-** pattern",
146+
),
147+
EscapeRstEmphasisFixture(
148+
test_id="space_after_asterisk",
149+
input_text="word-* followed by space",
150+
expected="word-\\* followed by space",
151+
),
152+
]
153+
154+
155+
@pytest.mark.parametrize(
156+
EscapeRstEmphasisFixture._fields,
157+
ESCAPE_RST_EMPHASIS_FIXTURES,
158+
ids=[f.test_id for f in ESCAPE_RST_EMPHASIS_FIXTURES],
159+
)
160+
def test_escape_rst_emphasis(test_id: str, input_text: str, expected: str) -> None:
161+
"""Test RST emphasis escaping for glob patterns."""
162+
assert escape_rst_emphasis(input_text) == expected

0 commit comments

Comments
 (0)