Repository: sciunto-org/python-bibtexparser
Pull request #290 (2022-01-03 15:41:22)

✨ Add option to adjust alignment of text of multi-line values.
A new option `align_multiline_values` is added to BibTexWriter to enable whether text of multiline values should be aligned on top of each other. Indentation also considered `_max_field_width`.

Current behaviour:
BibTeX entry:
```
@article{test,
  title     = {Hello World. I'm a very very very
  very very very very long title. Hello Bibtex Parser!
  This is still a long title!},
}
```

Parsing with BibtexParser will result in:
```
@article{test,
 title = {Hello World. I'm a very very very
very very very very long title. Hello Bibtex Parser!
This is still a long title!}
}
```

When `align_multiline_values` is set to true, the output will be the
following:
```
@article{test,
 title = {Hello World. I'm a very very very
          very very very very long title. Hello Bibtex Parser!
          This is still a long title!}
}
```

MiWeiss: Hi @michaelfruth 

I just joined the project - sorry for the long wait. 

Before I start the full review - would you mind adding some unit tests to cover this functionality? Please also make sure the already existing indentation functionality works well with this.

Thanks for opening the PR!


michaelfruth: Thanks! I'll probably add some unit tests next week or two.
michaelfruth: I'm lacking behind my schedule. I didn't forget to write some tests, try to do this until the end of this week.
MiWeiss: No worries! Thanks for the heads up, and for your contribution.
michaelfruth: I've added some tests to test:
1. the basic functionality
2. the new functionality with `align_values` and `indent`

I have also encountered this behavior #315 , I'm not quite sure if this is expected.

Patch:
diff --git a/bibtexparser/bwriter.py b/bibtexparser/bwriter.py
index 454a977..7e4758a 100644
--- a/bibtexparser/bwriter.py
+++ b/bibtexparser/bwriter.py
@@ -58,6 +58,9 @@ class BibTexWriter(object):
         #: Align values. Determines the maximal number of characters used in any fieldname and aligns all values
         #    according to that by filling up with single spaces. Default: False
         self.align_values = False
+        #: Align multi-line values. Formats a multi-line value such that the text is aligned exactly
+        #    on top of each other. Default: False
+        self.align_multiline_values = False
         #: Characters(s) for separating BibTeX entries. Default: new line.
         self.entry_separator = '\n'
         #: Tuple of fields for ordering BibTeX entries. Set to `None` to disable sorting. Default: BibTeX key `('ID', )`.
@@ -127,11 +130,31 @@ class BibTexWriter(object):
         # Write field = value lines
         for field in [i for i in display_order if i not in ['ENTRYTYPE', 'ID']]:
             try:
+                value = _str_or_expr_to_bibtex(entry[field])
+
+                if self.align_multiline_values:
+                    # Calculate indent of multi-line values. Text from a multiline string
+                    # should be aligned, i.e., be exactly on top of each other.
+                    # E.g.:       title = {Hello
+                    #                      World}
+                    # Calculate the indent of "World":
+                    # Left of field (whitespaces before e.g. 'title')
+                    value_indent = len(self.indent)
+                    # Field itself (e.g. len('title'))
+                    if self._max_field_width > 0:
+                        value_indent += self._max_field_width
+                    else:
+                        value_indent += len(field)
+                    # Right of field ' = ' (<- 3 chars) + '{' (<- 1 char)
+                    value_indent += 3 + 1
+
+                    value = value.replace('\n', '\n' + value_indent * ' ')
+
                 bibtex += field_fmt.format(
                     indent=self.indent,
                     field=field,
                     field_max_w=self._max_field_width,
-                    value=_str_or_expr_to_bibtex(entry[field]))
+                    value=value)
             except TypeError:
                 raise TypeError(u"The field %s in entry %s must be a string"
                                 % (field, entry['ID']))


Test patch:
diff --git a/bibtexparser/tests/data/article_multilines.bib b/bibtexparser/tests/data/article_multilines.bib
new file mode 100644
index 0000000..7a6bbe3
--- /dev/null
+++ b/bibtexparser/tests/data/article_multilines.bib
@@ -0,0 +1,13 @@
+@ARTICLE{Cesar2013,
+  author = {Jean César},
+  title = {A mutline line title is very amazing. It should be
+long enough to test multilines... with two lines or should we
+even test three lines... What an amazing title.},
+  year = {2013},
+  journal = {Nice Journal},
+  abstract = {This is an abstract. This line should be long enough to test
+multilines... and with a french érudit word},
+  comments = {A comment},
+  keyword = {keyword1, keyword2,
+multiline-keyword1, multiline-keyword2},
+}
diff --git a/bibtexparser/tests/test_bibtexwriter.py b/bibtexparser/tests/test_bibtexwriter.py
index b7a83c6..0d8fafb 100644
--- a/bibtexparser/tests/test_bibtexwriter.py
+++ b/bibtexparser/tests/test_bibtexwriter.py
@@ -169,6 +169,106 @@ class TestBibTexWriter(unittest.TestCase):
  title = {Optical fiber fusion slicing},
  author = {Yablon, A.D.}
 }
+"""
+        self.assertEqual(result, expected)
+
+    def test_align_multiline_values(self):
+        with open('bibtexparser/tests/data/article_multilines.bib') as bibtex_file:
+            bib_database = bibtexparser.load(bibtex_file)
+        writer = BibTexWriter()
+        writer.align_multiline_values = True
+        writer.display_order = ["author", "title", "year", "journal", "abstract", "comments", "keyword"]
+        result = bibtexparser.dumps(bib_database, writer)
+        expected = \
+"""@article{Cesar2013,
+ author = {Jean César},
+ title = {A mutline line title is very amazing. It should be
+          long enough to test multilines... with two lines or should we
+          even test three lines... What an amazing title.},
+ year = {2013},
+ journal = {Nice Journal},
+ abstract = {This is an abstract. This line should be long enough to test
+             multilines... and with a french érudit word},
+ comments = {A comment},
+ keyword = {keyword1, keyword2,
+            multiline-keyword1, multiline-keyword2}
+}
+"""
+        self.assertEqual(result, expected)
+
+    def test_align_multiline_values_with_align(self):
+        with open('bibtexparser/tests/data/article_multilines.bib') as bibtex_file:
+            bib_database = bibtexparser.load(bibtex_file)
+        writer = BibTexWriter()
+        writer.align_multiline_values = True
+        writer.align_values = True
+        writer.display_order = ["author", "title", "year", "journal", "abstract", "comments", "keyword"]
+        result = bibtexparser.dumps(bib_database, writer)
+        expected = \
+"""@article{Cesar2013,
+ author    = {Jean César},
+ title     = {A mutline line title is very amazing. It should be
+              long enough to test multilines... with two lines or should we
+              even test three lines... What an amazing title.},
+ year      = {2013},
+ journal   = {Nice Journal},
+ abstract  = {This is an abstract. This line should be long enough to test
+              multilines... and with a french érudit word},
+ comments  = {A comment},
+ keyword   = {keyword1, keyword2,
+              multiline-keyword1, multiline-keyword2}
+}
+"""
+        self.assertEqual(result, expected)
+
+    def test_align_multiline_values_with_indent(self):
+        with open('bibtexparser/tests/data/article_multilines.bib') as bibtex_file:
+            bib_database = bibtexparser.load(bibtex_file)
+        writer = BibTexWriter()
+        writer.align_multiline_values = True
+        writer.indent = ' ' * 3
+        writer.display_order = ["author", "title", "year", "journal", "abstract", "comments", "keyword"]
+        result = bibtexparser.dumps(bib_database, writer)
+        expected = \
+"""@article{Cesar2013,
+   author = {Jean César},
+   title = {A mutline line title is very amazing. It should be
+            long enough to test multilines... with two lines or should we
+            even test three lines... What an amazing title.},
+   year = {2013},
+   journal = {Nice Journal},
+   abstract = {This is an abstract. This line should be long enough to test
+               multilines... and with a french érudit word},
+   comments = {A comment},
+   keyword = {keyword1, keyword2,
+              multiline-keyword1, multiline-keyword2}
+}
+"""
+        self.assertEqual(result, expected)
+
+    def test_align_multiline_values_with_align_with_indent(self):
+        with open('bibtexparser/tests/data/article_multilines.bib') as bibtex_file:
+            bib_database = bibtexparser.load(bibtex_file)
+        writer = BibTexWriter()
+        writer.align_multiline_values = True
+        writer.indent = ' ' * 3
+        writer.align_values = True
+        writer.display_order = ["author", "title", "year", "journal", "abstract", "comments", "keyword"]
+        result = bibtexparser.dumps(bib_database, writer)
+        expected = \
+"""@article{Cesar2013,
+   author    = {Jean César},
+   title     = {A mutline line title is very amazing. It should be
+                long enough to test multilines... with two lines or should we
+                even test three lines... What an amazing title.},
+   year      = {2013},
+   journal   = {Nice Journal},
+   abstract  = {This is an abstract. This line should be long enough to test
+                multilines... and with a french érudit word},
+   comments  = {A comment},
+   keyword   = {keyword1, keyword2,
+                multiline-keyword1, multiline-keyword2}
+}
 """
         self.assertEqual(result, expected)