Repository: sphinx-doc/sphinx

Issue:
samp text role lacks ability to escape bracket characters [with patch]
I know this is somewhat of a minor edge case, but as of 1.1, there is no way to embed literal "{" characters in the :samp: text role.

I gave a shot at fixing this myself, and came up with the attached patch, which rewrites sphinx.roles.emph_literal_role() so it behaves as follows:
- '{' and '}' still start and end emphasized variable sections
-  only now the backslash acts as an escape character... a backslash followed by a '{', '}', or another backslash, inserts a literal character. 
- empty '{}' groups, unterminated '{' sections, and blackslashes followed by any any other character will all now result in an error.

---

This allows embedding special characters anywhere in :samp:, and shouldn't affect any existing documents, as long as they don't currently embed any literal backslashes or have malformed samp strings such as `:samp:`a{b` `.

Also, just to note: since docutils already processes backslash escapes, this requires :samp: text in rst files to use a double-escape to enable this feature... For example, `:samp:`a{b\{x\}}c` `in a document would result in the nodes` Text("a"), emphasis("b{x}"), Text("c") `. 

Despite the double-escape requirement, I couldn't think of anything else that seemed appropriate as an escape character. I did also try  "{{"-style escapes, ala Python's `str.format()` command, but this quickly proved to have way to ambiguous a grammar for the purposes of `:samp:`.

I hope this patch is acceptable, if not, please let me know if there's anything I can do to fix it. 

---
- Bitbucket: https://bitbucket.org/birkenfeld/sphinx/issue/789
- Originally reported by: Eli Collins
- Originally created at: 2011-10-11T22:42:37.547


Discussion:
picbox474: Hi
Envoyé à partir de Yahoo Courriel sur Android 
 
  Le ven., avr. 27 2018 à 22:39, Takeshi KOMIYA<notifications@github.com> a écrit :   
Feature or Bugfix
   
   - Feature

Purpose
   
   - refs: #789

You can view, comment on, or merge this pull request online at:

  https://github.com/sphinx-doc/sphinx/pull/4909

Commit Summary
   
   - Fix #789: ``:samp:`` role supports to escape curly braces with backslash

File Changes
   
   -  M CHANGES (1) 
   -  M doc/usage/restructuredtext/roles.rst (3) 
   -  M sphinx/roles.py (47) 

Patch Links:
   
   - https://github.com/sphinx-doc/sphinx/pull/4909.patch
   - https://github.com/sphinx-doc/sphinx/pull/4909.diff

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or mute the thread.
     


Patch:
diff --git a/CHANGES b/CHANGES
index fb965a3d0..9032dc3d2 100644
--- a/CHANGES
+++ b/CHANGES
@@ -25,6 +25,7 @@ Incompatible changes
   reading phase
 * ``docutils.conf`` on ``$HOME`` and ``/etc`` directories are ignored.  Only
   ``docutils.conf`` on confdir is refered.
+* #789: ``:samp:`` role supports to escape curly braces with backslash
 
 Deprecated
 ----------
diff --git a/doc/usage/restructuredtext/roles.rst b/doc/usage/restructuredtext/roles.rst
index 7e68b3e2a..04991c3a9 100644
--- a/doc/usage/restructuredtext/roles.rst
+++ b/doc/usage/restructuredtext/roles.rst
@@ -406,6 +406,9 @@ different style:
    If you don't need the "variable part" indication, use the standard
    ````code```` instead.
 
+   .. versionchanged:: 1.8
+      Allowed to escape curly braces with backslash
+
 There is also an :rst:role:`index` role to generate index entries.
 
 The following roles generate external links:
diff --git a/sphinx/roles.py b/sphinx/roles.py
index 2971c8fc3..453eb5aa0 100644
--- a/sphinx/roles.py
+++ b/sphinx/roles.py
@@ -284,6 +284,7 @@ def menusel_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
 
 
 _litvar_re = re.compile('{([^}]+)}')
+parens_re = re.compile(r'(\\*{|\\*})')
 
 
 def emph_literal_role(typ, rawtext, text, lineno, inliner,
@@ -296,17 +297,43 @@ def emph_literal_role(typ, rawtext, text, lineno, inliner,
     else:
         typ = typ.lower()
 
-    text = utils.unescape(text)
-    pos = 0
     retnode = nodes.literal(role=typ.lower(), classes=[typ])
-    for m in _litvar_re.finditer(text):  # type: ignore
-        if m.start() > pos:
-            txt = text[pos:m.start()]
-            retnode += nodes.Text(txt, txt)
-        retnode += nodes.emphasis(m.group(1), m.group(1))
-        pos = m.end()
-    if pos < len(text):
-        retnode += nodes.Text(text[pos:], text[pos:])
+    parts = list(parens_re.split(utils.unescape(text)))
+    stack = ['']
+    for part in parts:
+        matched = parens_re.match(part)
+        if matched:
+            backslashes = len(part) - 1
+            if backslashes % 2 == 1:    # escaped
+                stack[-1] += "\\" * int((backslashes - 1) / 2) + part[-1]
+            elif part[-1] == '{':       # rparen
+                stack[-1] += "\\" * int(backslashes / 2)
+                if len(stack) >= 2 and stack[-2] == "{":
+                    # nested
+                    stack[-1] += "{"
+                else:
+                    # start emphasis
+                    stack.append('{')
+                    stack.append('')
+            else:                       # lparen
+                stack[-1] += "\\" * int(backslashes / 2)
+                if len(stack) == 3 and stack[1] == "{" and len(stack[2]) > 0:
+                    # emphasized word found
+                    if stack[0]:
+                        retnode += nodes.Text(stack[0], stack[0])
+                    retnode += nodes.emphasis(stack[2], stack[2])
+                    stack = ['']
+                else:
+                    # emphasized word not found; the rparen is not a special symbol
+                    stack.append('}')
+                    stack = [''.join(stack)]
+        else:
+            stack[-1] += part
+    if ''.join(stack):
+        # remaining is treated as Text
+        text = ''.join(stack)
+        retnode += nodes.Text(text, text)
+
     return [retnode], []
 
 


Test patch:
diff --git a/tests/test_roles.py b/tests/test_roles.py
new file mode 100644
index 000000000..13b4e194d
--- /dev/null
+++ b/tests/test_roles.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+"""
+    test_roles
+    ~~~~~~~~~~
+
+    Test sphinx.roles
+
+    :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from docutils import nodes
+from mock import Mock
+
+from sphinx.roles import emph_literal_role
+from sphinx.testing.util import assert_node
+
+
+def test_samp():
+    # normal case
+    text = 'print 1+{variable}'
+    ret, msg = emph_literal_role('samp', text, text, 0, Mock())
+    assert_node(ret, ([nodes.literal, ("print 1+",
+                                       [nodes.emphasis, "variable"])],))
+    assert msg == []
+
+    # two emphasis items
+    text = 'print {1}+{variable}'
+    ret, msg = emph_literal_role('samp', text, text, 0, Mock())
+    assert_node(ret, ([nodes.literal, ("print ",
+                                       [nodes.emphasis, "1"],
+                                       "+",
+                                       [nodes.emphasis, "variable"])],))
+    assert msg == []
+
+    # empty curly brace
+    text = 'print 1+{}'
+    ret, msg = emph_literal_role('samp', text, text, 0, Mock())
+    assert_node(ret, ([nodes.literal, "print 1+{}"],))
+    assert msg == []
+
+    # half-opened variable
+    text = 'print 1+{variable'
+    ret, msg = emph_literal_role('samp', text, text, 0, Mock())
+    assert_node(ret, ([nodes.literal, "print 1+{variable"],))
+    assert msg == []
+
+    # nested
+    text = 'print 1+{{variable}}'
+    ret, msg = emph_literal_role('samp', text, text, 0, Mock())
+    assert_node(ret, ([nodes.literal, ("print 1+",
+                                       [nodes.emphasis, "{variable"],
+                                       "}")],))
+    assert msg == []
+
+    # emphasized item only
+    text = '{variable}'
+    ret, msg = emph_literal_role('samp', text, text, 0, Mock())
+    assert_node(ret, ([nodes.literal, nodes.emphasis, "variable"],))
+    assert msg == []
+
+    # escaping
+    text = r'print 1+\{variable}'
+    ret, msg = emph_literal_role('samp', text, text, 0, Mock())
+    assert_node(ret, ([nodes.literal, "print 1+{variable}"],))
+    assert msg == []
+
+    # escaping (2)
+    text = r'print 1+\{{variable}\}'
+    ret, msg = emph_literal_role('samp', text, text, 0, Mock())
+    assert_node(ret, ([nodes.literal, ("print 1+{",
+                                       [nodes.emphasis, "variable"],
+                                       "}")],))
+    assert msg == []
+
+    # escape a backslash
+    text = r'print 1+\\{variable}'
+    ret, msg = emph_literal_role('samp', text, text, 0, Mock())
+    assert_node(ret, ([nodes.literal, ("print 1+\\",
+                                       [nodes.emphasis, "variable"])],))
+    assert msg == []