Repository: alvinwan/TexSoup

Issue:
replace_with does not work for arguments of a node
```python3
soup = TexSoup(r"\somecommand{\anothercommand}")
some_obj = TexNode(TexText("new text"))
soup.somecommand.anothercommand.replace_with(some_obj)
```
Gives

```python3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/wilco/.cache/pypoetry/virtualenvs/viaduct-CDUJVlHg-py3.8/lib/python3.8/site-packages/TexSoup/data.py", line 573, in replace_with
    self.parent.replace(self, *nodes)
  File "/home/wilco/.cache/pypoetry/virtualenvs/viaduct-CDUJVlHg-py3.8/lib/python3.8/site-packages/TexSoup/data.py", line 597, in replace
    self.expr.remove(child.expr),
  File "/home/wilco/.cache/pypoetry/virtualenvs/viaduct-CDUJVlHg-py3.8/lib/python3.8/site-packages/TexSoup/data.py", line 838, in remove
    self._assert_supports_contents()
  File "/home/wilco/.cache/pypoetry/virtualenvs/viaduct-CDUJVlHg-py3.8/lib/python3.8/site-packages/TexSoup/data.py", line 1084, in _assert_supports_contents
    raise TypeError(
TypeError: Command "somecommand" has no children. `add_contents` is only validfor: 1. environments like `itemize` and 2. `\item`. Alternatively, you can add, edit, or delete arguments by modifying `.args`, which behaves like a list.
```
The `replace_with` method tries to replace a child of 'somecommand' but we are dealing with an argument instead. The documentation does not mention this limitation. 

In my use case I am parsing all `newcommand`s, then replacing these commands with their values. I.e.
```latex
\newcommand{\test}{Just a test}
\stuff{\test}
```
Should be converted to
```latex
\newcommand{\test}{Just a test}
\stuff{Just a test}
```
In this case I do not know I am operating on an argument of a node instead of a child of this node. Is there a workaround?

Discussion:

Patch:
diff --git a/TexSoup/data.py b/TexSoup/data.py
index 58cd070..8e04c24 100644
--- a/TexSoup/data.py
+++ b/TexSoup/data.py
@@ -593,10 +593,15 @@ class TexNode(object):
         \item Bye
         \end{itemize}
         """
+        for arg in self.expr.args:
+            if child.expr in arg._contents:
+                arg.insert(arg.remove(child.expr), *nodes)
+                return
         self.expr.insert(
             self.expr.remove(child.expr),
             *nodes)
 
+
     def search_regex(self, pattern):
         for node in self.text:
             for match in re.finditer(pattern, node):
@@ -1082,7 +1087,7 @@ class TexCmd(TexExpr):
     def _assert_supports_contents(self):
         if not self._supports_contents():
             raise TypeError(
-                'Command "{}" has no children. `add_contents` is only valid'
+                'Command "{}" has no children. `add_contents` is only valid '
                 'for: 1. environments like `itemize` and 2. `\\item`. '
                 'Alternatively, you can add, edit, or delete arguments by '
                 'modifying `.args`, which behaves like a list.'
diff --git a/TexSoup/reader.py b/TexSoup/reader.py
index fd53989..57bd38e 100644
--- a/TexSoup/reader.py
+++ b/TexSoup/reader.py
@@ -31,7 +31,11 @@ SIGNATURES = {
     'textbf': (1, 0),
     'section': (1, 1),
     'label': (1, 0),
+    'cap': (0, 0),
     'cup': (0, 0),
+    'in': (0, 0),
+    'notin': (0, 0),
+    'infty': (0, 0),
     'noindent': (0, 0),
 }
 
@@ -97,7 +101,7 @@ def read_expr(src, skip_envs=(), tolerance=0, mode=MODE_NON_MATH):
     elif c.category == TC.Escape:
         name, args = read_command(src, tolerance=tolerance, mode=mode)
         if name == 'item':
-            assert mode != MODE_MATH, 'Command \item invalid in math mode.'
+            assert mode != MODE_MATH, r'Command \item invalid in math mode.'
             contents = read_item(src)
             expr = TexCmd(name, contents, args, position=c.position)
         elif name == 'begin':
@@ -278,7 +282,7 @@ def read_env(src, expr, skip_envs=(), tolerance=0, mode=MODE_NON_MATH):
     while src.hasNext():
         if src.peek().category == TC.Escape:
             name, args = make_read_peek(read_command)(
-                src, 1, skip=1, tolerance=tolerance, mode=mode)
+                src, skip=1, tolerance=tolerance, mode=mode)
             if name == 'end':
                 break
         contents.append(read_expr(src, skip_envs=skip_envs, tolerance=tolerance, mode=mode))
diff --git a/TexSoup/tokens.py b/TexSoup/tokens.py
index 492c50e..50e97e2 100644
--- a/TexSoup/tokens.py
+++ b/TexSoup/tokens.py
@@ -123,8 +123,8 @@ def tokenize_escaped_symbols(text, prev=None):
             and text.peek(1) \
             and text.peek(1).category in (
                 CC.Escape, CC.GroupBegin, CC.GroupEnd, CC.MathSwitch,
-                CC.Alignment, CC.Macro, CC.Superscript, CC.Subscript,
-                CC.Spacer, CC.Active, CC.Comment, CC.Other):
+                CC.Alignment, CC.EndOfLine, CC.Macro, CC.Superscript,
+                CC.Subscript, CC.Spacer, CC.Active, CC.Comment, CC.Other):
         result = text.forward(2)
         result.category = TC.EscapedComment
         return result


Test patch:
diff --git a/tests/test_api.py b/tests/test_api.py
index 5c95831..7eb44fb 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -129,6 +129,13 @@ def test_replace_multiple(chikin):
     assert len(list(chikin.find_all('subsection'))) == 5
 
 
+def test_replace_in_args():
+    """Replace an element in an argument"""
+    soup = TexSoup(r'\Fig{\ref{a_label}}')
+    soup.ref.replace_with('2')
+    assert str(soup) == r'\Fig{2}'
+
+
 def test_append(chikin):
     """Add a child to the parse tree"""
     chikin.itemize.append('asdfghjkl')
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 60ea596..e958c58 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -195,6 +195,13 @@ def test_escaped_characters():
     assert '\\$4-\\$5' in str(soup), 'Escaped characters not properly rendered.'
 
 
+def test_newline_after_backslash():
+    """Tests that newlines after backslashes are preserved."""
+    text = 'a\\\nb'
+    soup = TexSoup(text)
+    assert str(soup) == text
+
+
 def test_math_environment_weirdness():
     """Tests that math environment interacts correctly with other envs."""
     soup = TexSoup(r"""\begin{a} \end{a}$ b$""")
@@ -385,7 +392,7 @@ def test_def_item():
 
 
 def test_def_without_braces():
-    """Tests that def without braces around the new command parses correctly"""
+    """Tests that def without braces around the new command parses correctly."""
     soup = TexSoup(r"\def\acommandname{replacement text}")
     assert len(soup.find("def").args) == 2
     assert str(soup.find("def").args[0]) == r"\acommandname"
@@ -398,6 +405,19 @@ def test_grouping_optional_argument():
     assert len(soup.Theorem.args) == 1
 
 
+def test_zero_argument_signatures():
+    """Tests that specific commands that do not take arguments are parsed correctly."""
+    soup = TexSoup(r"$\cap[\cup[\in[\notin[\infty[$")
+    assert len(soup.find("cap").args) == 0
+    assert len(soup.find("cup").args) == 0
+    assert len(soup.find("in").args) == 0
+    assert len(soup.find("notin").args) == 0
+    assert len(soup.find("infty").args) == 0
+
+    soup = TexSoup(r"\begin{equation} \cup [0, \infty) \end{equation}")
+    assert len(soup.find("cup").args) == 0
+
+
 ##############
 # FORMATTING #
 ##############