Repository: google-research/arxiv-latex-cleaner

Issue:
Nested \iffalse \fi block comments.
I used \iffalse ... \fi to block comment in my latex document, and used this modification of the  _remove_environment command:

```
def _remove_iffalse(text):
  """Removes '\\iffalse *\\fi' from 'text'."""
  """This has problems with nested \\iffalse \\fi statements"""
  return re.sub(
      r'\\iffalse[\s\S]*?\\fi',
      '', text)

```
However, this runs incorrectly on:
```
\iffalse
A
\iffalse
B
\fi
C
\fi
```

Which in latex outputs nothing, but with the _remove_iffalse code above outputs:

```
C
\fi
```

(I had one such nested comment in my document, because of commenting out a subsection of a section that was later commented out in its entirety.)

A similar problem does not exist for \begin{comment} \end{comment}, because 

```
\begin{comment}
A
\begin{comment}
B
\end{comment}
C
\end{comment}
```

Does not compile in Latex.

Discussion:

Patch:
diff --git a/arxiv_latex_cleaner/arxiv_latex_cleaner.py b/arxiv_latex_cleaner/arxiv_latex_cleaner.py
index 96dcd0c..bc12ddc 100644
--- a/arxiv_latex_cleaner/arxiv_latex_cleaner.py
+++ b/arxiv_latex_cleaner/arxiv_latex_cleaner.py
@@ -111,6 +111,36 @@ def _remove_environment(text, environment):
       text)
 
 
+def _remove_iffalse_block(text):
+  """Removes possibly nested r'\iffalse*\fi' blocks from 'text'."""
+  p = re.compile(r'\\if(\w+)|\\fi')
+  level = -1
+  positions_to_del = []
+  start, end = 0, 0
+  for m in p.finditer(text):
+    if m.group() == r'\iffalse' and level == -1:
+      level += 1
+      start = m.start()
+    elif m.group().startswith(r'\if') and level >= 0:
+      level += 1
+    elif m.group() == r'\fi' and level >= 0:
+      if level == 0:
+          end = m.end()
+          positions_to_del.append((start, end))
+      level -= 1
+    else:
+      pass
+    
+  for (start, end) in reversed(positions_to_del):
+    if end < len(text) and text[end].isspace():
+      end_to_del = end + 1
+    else:
+      end_to_del = end
+    text = text[:start] + text[end_to_del:]
+
+  return text
+
+
 def _remove_comments_inline(text):
   """Removes the comments from the string 'text'."""
   if 'auto-ignore' in text:
@@ -147,6 +177,7 @@ def _remove_comments(content, parameters):
   """Erases all LaTeX comments in the content, and writes it."""
   content = [_remove_comments_inline(line) for line in content]
   content = _remove_environment(''.join(content), 'comment')
+  content = _remove_iffalse_block(content)
   for command in parameters['commands_to_delete']:
     content = _remove_command(content, command)
   return content
diff --git a/tex/main.tex b/tex/main.tex
index 2e434d5..0a9abac 100644
--- a/tex/main.tex
+++ b/tex/main.tex
@@ -15,6 +15,16 @@ This is a todo command\mytodo{Do this later}
 \mytodo{This is a todo command with a nested \textit{command}.
 Please remember that up to \texttt{2 levels} of \textit{nesting} are supported.}
 
+\newif\ifvar
+
+\ifvar
+\iffalse
+\ifvar
+Text
+\fi
+\fi
+\fi
+
 \input{figures/figure_included.tex}
 % \input{figures/figure_not_included.tex}
 
diff --git a/tex_arXiv_true/main.tex b/tex_arXiv_true/main.tex
index c3b203f..2df5b95 100644
--- a/tex_arXiv_true/main.tex
+++ b/tex_arXiv_true/main.tex
@@ -9,6 +9,11 @@ This is a percent \%.
 This is a todo command
 
 
+\newif\ifvar
+
+\ifvar
+\fi
+
 \input{figures/figure_included.tex}
 
 \includegraphics{ext_tikz/test1.pdf}


Test patch:
diff --git a/arxiv_latex_cleaner/tests/arxiv_latex_cleaner_test.py b/arxiv_latex_cleaner/tests/arxiv_latex_cleaner_test.py
index 0a34693..60258f1 100644
--- a/arxiv_latex_cleaner/tests/arxiv_latex_cleaner_test.py
+++ b/arxiv_latex_cleaner/tests/arxiv_latex_cleaner_test.py
@@ -88,6 +88,41 @@ class UnitTests(parameterized.TestCase):
         arxiv_latex_cleaner._remove_environment(text_in, 'comment'),
         true_output)
 
+  @parameterized.named_parameters(
+      {
+          'testcase_name': 'no_iffalse',
+          'text_in': 'Foo\n',
+          'true_output': 'Foo\n'
+      }, {
+          'testcase_name': 'if_not_removed',
+          'text_in': '\\ifvar\n\\ifvar\nFoo\n\\fi\n\\fi\n',
+          'true_output': '\\ifvar\n\\ifvar\nFoo\n\\fi\n\\fi\n'
+      }, {
+          'testcase_name': 'if_removed_with_nested_ifvar',
+          'text_in': '\\ifvar\n\\iffalse\n\\ifvar\nFoo\n\\fi\n\\fi\n\\fi\n',
+          'true_output': '\\ifvar\n\\fi\n'
+      }, {
+          'testcase_name': 'if_removed_with_nested_iffalse',
+          'text_in': '\\ifvar\n\\iffalse\n\\iffalse\nFoo\n\\fi\n\\fi\n\\fi\n',
+          'true_output': '\\ifvar\n\\fi\n'
+      }, {
+          'testcase_name': 'if_removed_eof',
+          'text_in': '\\iffalse\nFoo\n\\fi',
+          'true_output': ''
+      }, {
+          'testcase_name': 'if_removed_space',
+          'text_in': '\\iffalse\nFoo\n\\fi ',
+          'true_output': ''
+      }, {
+          'testcase_name': 'if_removed_backslash',
+          'text_in': '\\iffalse\nFoo\n\\fi\\end{document}',
+          'true_output': '\\end{document}'
+      })
+  def test_remove_iffalse_block(self, text_in, true_output):
+    self.assertEqual(
+        arxiv_latex_cleaner._remove_iffalse_block(text_in),
+        true_output)
+
   @parameterized.named_parameters(
       {
           'testcase_name': 'all_pass',