|
22 | 22 |
|
23 | 23 | from docutils.nodes import ( |
24 | 24 | Element, |
| 25 | + FixedTextElement, |
25 | 26 | Node, |
26 | 27 | Text, |
27 | 28 | bullet_list, |
28 | 29 | container, |
29 | 30 | fully_normalize_name, |
| 31 | + inline, |
30 | 32 | list_item, |
31 | 33 | literal, |
32 | 34 | literal_block, |
@@ -228,7 +230,9 @@ def _pre_format(self, block: None | str) -> None | paragraph | literal_block: |
228 | 230 | lit = literal_block("", Text(block), classes=["sphinx-argparse-cli-wrap"]) |
229 | 231 | lit["language"] = "none" |
230 | 232 | return lit |
231 | | - return paragraph("", Text(block)) |
| 233 | + para = paragraph("", Text(block)) |
| 234 | + _protect_option_dashes(para) |
| 235 | + return para |
232 | 236 |
|
233 | 237 | def _mk_option_group(self, group: _ArgumentGroup, prefix: str, prog: str) -> section: |
234 | 238 | sub_title_prefix: str = self.options["group_sub_title_prefix"] |
@@ -302,6 +306,7 @@ def _mk_option_line(self, action: Action, prefix: str) -> list_item: |
302 | 306 | line += Text(" (default: ") |
303 | 307 | line += literal(text=str(action.default).replace(str(Path.cwd()), "{cwd}")) |
304 | 308 | line += Text(")") |
| 309 | + _protect_option_dashes(line) |
305 | 310 | return point |
306 | 311 |
|
307 | 312 | def _mk_option_name(self, line: paragraph, prefix: str, opt: str) -> None: |
@@ -365,6 +370,7 @@ def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentPar |
365 | 370 | command_desc = (parser.description or help_msg or "").strip() |
366 | 371 | if command_desc: |
367 | 372 | desc_paragraph = paragraph("", Text(command_desc)) |
| 373 | + _protect_option_dashes(desc_paragraph) |
368 | 374 | group_section += desc_paragraph |
369 | 375 |
|
370 | 376 | if "usage_first" not in self.options: |
@@ -444,6 +450,38 @@ def load_help_text(help_text: str) -> str: |
444 | 450 | return CURLY_BRACES.sub("``{\\1}``", double_quote) |
445 | 451 |
|
446 | 452 |
|
| 453 | +_OPTION_TOKEN = re.compile(r"((?<!\w)--[a-zA-Z0-9][\w-]*)") |
| 454 | + |
| 455 | + |
| 456 | +def _protect_option_dashes(node: Element) -> None: |
| 457 | + """ |
| 458 | + Wrap ``--option`` tokens so smart quotes can't rewrite their ``--`` to an en dash. |
| 459 | +
|
| 460 | + Each token becomes an inline exempted via ``support_smartquotes``; surrounding text is |
| 461 | + left untouched. The node is modified in place. |
| 462 | + """ |
| 463 | + for text in list(node.findall(Text)): |
| 464 | + parent = text.parent |
| 465 | + if isinstance(parent, (literal, FixedTextElement)): |
| 466 | + continue |
| 467 | + # Capturing group => split() yields the option tokens at odd indices, |
| 468 | + # interleaved with the surrounding text. |
| 469 | + parts = _OPTION_TOKEN.split(text) |
| 470 | + if len(parts) == 1: |
| 471 | + continue |
| 472 | + replacement: list[Node] = [] |
| 473 | + for i, part in enumerate(parts): |
| 474 | + if not part: |
| 475 | + continue |
| 476 | + if i % 2: |
| 477 | + inline_node = inline("", part) |
| 478 | + inline_node["support_smartquotes"] = False |
| 479 | + replacement.append(inline_node) |
| 480 | + else: |
| 481 | + replacement.append(Text(part)) |
| 482 | + parent.replace(text, replacement) |
| 483 | + |
| 484 | + |
447 | 485 | class HookError(Exception): |
448 | 486 | def __init__(self, parser: ArgumentParser) -> None: |
449 | 487 | self.parser = parser |
|
0 commit comments