Repository: alvinwan/TexSoup

Issue:
Ignoring Latex in certain cases
In the LaTex files I try to parse are some blocks that TexSoup shouldn't try to parse as LaTex.  They are, in fact, similar to a `\begin{code}
...\end{code}` block, what is between the begin and end should not be read as LaTex.  In fact, doing so would lead to a lot of errors in a book about programming.  I have some special code block here (not using `\begin{code}`, but `\begin{console}` or `\begin{special}`.  I could replace these in the original LaTex before using TexSoup, but that sounds like an unnecessary step.  Although I'm not that familiar with laTex, I expect these blocks are described as 'escaped' (perhaps as 'math' blocks) in the `tty` file.  But that's just a guess.  At any rate, when TexSoup tries to parse such files, it fails (with a `EOFError` or `TypeError`, indicating mismatching punctuation, which is kind of normal).

A fix in the API might not be that easy.  A keyword argument in the converter might not be ideal but it would be the easiest fix I guess.  An alternative would be another function (or a method on the parser itself) to ignore such codes.  Perhaps this method already exists, I couldn't find a way to do it in the documentation.

Thank you again for this tool.  I'm using it to try to convert these LaTex books to easy Markdown and that will involve a lot of content and a lot of adjustments, but I think with such a parser it shouldn't be too hard.  So I hope, anyway!

Comments:

Patch:
diff --git a/TexSoup/__init__.py b/TexSoup/__init__.py
index cd8782a..9e622c0 100644
--- a/TexSoup/__init__.py
+++ b/TexSoup/__init__.py
@@ -9,7 +9,7 @@ from TexSoup.tex import *
 
 
 # noinspection PyPep8Naming
-def TexSoup(tex_code):
+def TexSoup(tex_code, skip_envs=()):
     r"""
     At a high-level, parses provided Tex into a navigable, searchable structure.
     This is accomplished in two steps:
@@ -18,6 +18,7 @@ def TexSoup(tex_code):
     2. Structure fed to TexNodes for a searchable, coder-friendly interface.
 
     :param Union[str,iterable] tex_code: the Tex source
+    :param Union[str] skip_envs: names of environments to skip parsing
     :return: :class:`TexSoup.data.TexNode` object representing tex document
 
     >>> from TexSoup import TexSoup
@@ -80,5 +81,5 @@ def TexSoup(tex_code):
     >>> soup
     SOUP
     """
-    parsed, src = read(tex_code)
+    parsed, src = read(tex_code, skip_envs=skip_envs)
     return TexNode(parsed, src=src)
diff --git a/TexSoup/reader.py b/TexSoup/reader.py
index 6dc4a99..0b9c182 100644
--- a/TexSoup/reader.py
+++ b/TexSoup/reader.py
@@ -224,7 +224,7 @@ def tokenize_string(text, delimiters=None):
 ##########
 
 
-def read_tex(src):
+def read_tex(src, skip_envs=()):
     r"""Read next expression from buffer
 
     :param Buffer src: a buffer of tokens
@@ -262,7 +262,7 @@ def read_tex(src):
         expr.args = read_args(src, expr.args)
 
         if mode == 'begin':
-            read_env(src, expr)
+            read_env(src, expr, skip_envs=skip_envs)
         return expr
     if c in ARG_START_TOKENS:
         return read_arg(src, c)
@@ -340,7 +340,7 @@ def read_math_env(src, expr):
     return expr
 
 
-def read_env(src, expr):
+def read_env(src, expr, skip_envs=()):
     r"""Read the environment from buffer.
 
     Advances the buffer until right after the end of the environment. Adds
@@ -351,7 +351,7 @@ def read_env(src, expr):
     :rtype: TexExpr
     """
     contents = []
-    if expr.name in SKIP_ENVS:
+    if expr.name in SKIP_ENVS + skip_envs:
         contents = [src.forward_until(lambda s: s == '\\end')]
     while src.hasNext() and not src.startswith('\\end{%s}' % expr.name):
         contents.append(read_tex(src))
diff --git a/TexSoup/tex.py b/TexSoup/tex.py
index ad8ce44..1b2ec63 100644
--- a/TexSoup/tex.py
+++ b/TexSoup/tex.py
@@ -4,7 +4,7 @@ from TexSoup.utils import *
 import itertools
 
 
-def read(tex):
+def read(tex, skip_envs=()):
     """Read and parse all LaTeX source
 
     :param Union[str,iterable] tex: LaTeX source
@@ -16,7 +16,7 @@ def read(tex):
         tex = ''.join(itertools.chain(*tex))
     buf, children = Buffer(tokenize(tex)), []
     while buf.hasNext():
-        content = read_tex(buf)
+        content = read_tex(buf, skip_envs=skip_envs)
         if content is not None:
             children.append(content)
     return TexEnv('[tex]', children), tex


Test patch:
diff --git a/tests/test_api.py b/tests/test_api.py
index a3bd66b..b2a3825 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1,6 +1,7 @@
 from TexSoup import TexSoup
 from TexSoup.utils import TokenWithPosition
 from tests.config import chikin
+import pytest
 import re
 
 ##############
@@ -169,3 +170,20 @@ def test_search_regex_precompiled_pattern(chikin):
     assert len(matches) == 1
     assert matches[0] == "unless ordered to squat"
     assert matches[0].position == 341
+
+
+###########
+# TEXSOUP #
+###########
+
+
+def test_skip_envs():
+    """Test envs with invalid latex are not parsed."""
+    with pytest.raises(TypeError):
+        soup = TexSoup(r"""will raise error \textbf{aaaaa""")
+
+    # no error, ignores verbatim
+    TexSoup(r"""\begin{verbatim} \textbf{aaaaa \end{verbatim}""")
+
+    # no error, customized to ignore foobar
+    TexSoup(r"""\begin{foobar} \textbf{aaaaa \end{foobar}""", skip_envs=('foobar',))