Repository: astamminger/zotero-bibtize

Issue:
Assumed BibTex termination sequence for entry fields is incomplete
Currently every entry field of a BibTex entry is assumed to be terminated by the sequence `,\n` and the entries are split accordingly. This leads to issues, especially for abstract fields which likely may contain this sequence, leading to a erroneous splitting of entries.

The termination sequence should thus be extended to check if `,\n` is preceded by `}` which closes the BibText entry field, i.e. the origin regex  `,\n` should be replaced with `(?<=\}),\n`.

Discussion:

Patch:
diff --git a/zotero_bibtize/zotero_bibtize.py b/zotero_bibtize/zotero_bibtize.py
index 2acf9e5..7c98628 100644
--- a/zotero_bibtize/zotero_bibtize.py
+++ b/zotero_bibtize/zotero_bibtize.py
@@ -57,11 +57,24 @@ class BibEntry(object):
         unescaped = self.unescape_bibtex_entry_string(raw_entry_string)
         unescaped = re.sub(r'^(\s*)|(\s*)$', '', unescaped)
         entry_match = re.match(r'^\@([\s\S]*?)\{([\s\S]*?)\}$', unescaped)
-        entry_type = entry_match.group(1)
-        entry_content =  re.split(',\n', entry_match.group(2))
-        # if the last field entry is followed by a comma the last entry
-        # in the list will be '' which will lead to issued further
-        # upstream, thus we check here and remove an emtpy entry
+        entry_type, entry_content = entry_match.group(1, 2)
+        # check if the unescaped bibtex entry is valid
+        if not self._is_balanced(entry_content):
+            raise Exception("Found braces unbalanced after unescaping of "
+                            "BibTeX entry. The offending entry was\n\n"
+                            "{}".format(raw_entry_string))
+        entry_content = []
+        tmp_entry = ''
+        for part in re.split(r",", entry_match.group(2)):
+            tmp_entry += re.sub(r'\n', '', part)
+            # since _is_balanced also returns True for strings containing
+            # no braces at this also works for the initial bibentry key
+            if self._is_balanced(tmp_entry):
+                entry_content.append(re.sub(r'\n', '', tmp_entry))
+                tmp_entry = ''
+            else:  # re-introduce comma if unbalanced
+                tmp_entry += ','
+        # remove possible emtpy entry at the end of the array
         if not entry_content[-1]:
             entry_content = entry_content[:-1]
         # return type, original zotero key and the actual content list 
@@ -114,6 +127,16 @@ class BibEntry(object):
             entry = entry.replace(word, word.lstrip("{").rstrip("}"))
         return entry
 
+    def _is_balanced(self, string):
+        """
+        Check if opening and closing curly braces are balanced in string.
+
+        :param str string: string to be checked for balanced braces
+        """
+        n_open = len(re.findall(r"\{", string))
+        n_close = len(re.findall(r"\}", string))
+        return n_open == n_close
+
     def __str__(self):
         # return bibtex entry as string
         content = ['@{}{{{}'.format(self.type, self.key)]


Test patch:
diff --git a/tests/zotero_bibtize/test_bibtex_entry.py b/tests/zotero_bibtize/test_bibtex_entry.py
index b9e4ecc..d67b9ee 100644
--- a/tests/zotero_bibtize/test_bibtex_entry.py
+++ b/tests/zotero_bibtize/test_bibtex_entry.py
@@ -81,9 +81,7 @@ bibtex_entry_types = [
 @pytest.mark.parametrize(('bibtex_entry_type'), bibtex_entry_types)
 def test_entry_type_is_recognized(bibtex_entry_type):
     from zotero_bibtize.zotero_bibtize import BibEntry
-    #entry_string = (r"@{:}{{key,{{field = {{}}}}"
-    #                .format(bibtex_entry_type))
-    entry_string = r"@{:}{{key,{{}}".format(bibtex_entry_type)
+    entry_string = r"@{:}{{key,}}".format(bibtex_entry_type)
     bibentry = BibEntry(entry_string)
     # check parsed type matches wanted type
     assert bibentry.type == bibtex_entry_type
@@ -238,8 +236,8 @@ def test_multiple_replacement_of_capitalized():
     # words (i.e. embraced by curly braces)
     input_entry = (
         "@bibtextype{bibkey,",
-        "    field1 = {Has \\ce{{Command}}},",
-        "    field2 = {Has same word {Command}}",
+        "   field1 = {Has \\ce{{Command}}},",
+        "   field2 = {Has same word {Command}}",
         "}",
         "",
     )
@@ -247,3 +245,29 @@ def test_multiple_replacement_of_capitalized():
     bibentry = BibEntry(input_entry)
     assert bibentry.fields['field1'] == "Has \\ce{Command}"
     assert bibentry.fields['field2'] == "Has same word Command"
+
+
+def test_splitting_for_containing_termination_sequence():
+    """
+    Regression test for Issue #18
+    https://github.com/astamminger/zotero-bibtize/issues/18
+    """
+    from zotero_bibtize.zotero_bibtize import BibEntry
+    input_entry = (
+        "@bibtextype{bibkey,",
+        "   field1 = {Regular Field Without Newlines},",
+        "   field2 = {Field containing,\n termination sequence}",
+        "}",
+        "",
+    )
+    input_entry = "\n".join(input_entry)
+    bibentry = BibEntry(input_entry)
+    input_entry = (
+        "@bibtextype{bibkey,",
+        "   field1 = {Regular Field Without Newlines},",
+        "   field2 = {Field {containing},\n termination sequence}",
+        "}",
+        "",
+    )
+    input_entry = "\n".join(input_entry)
+    bibentry = BibEntry(input_entry)