# -*- coding: utf-8 -*-
# Author: Francois Boulogne
# License:
import logging
from enum import Enum, auto
from typing import Dict, Callable, Iterable
from bibtexparser.bibdatabase import (BibDatabase, COMMON_STRINGS,
BibDataString,
BibDataStringExpression)
logger = logging.getLogger(__name__)
__all__ = ['BibTexWriter']
class SortingStrategy(Enum):
"""
Defines different strategies for sorting the entries not defined in :py:attr:`~.BibTexWriter.display_order` and that are added at the end.
"""
ALPHABETICAL_ASC = auto()
"""
Alphabetical sorting in ascending order.
"""
ALPHABETICAL_DESC = auto()
"""
Alphabetical sorting in descending order.
"""
PRESERVE = auto()
"""
Preserves the order of the entries. Entries are not sorted.
"""
def _apply_sorting_strategy(strategy: SortingStrategy, items: Iterable[str]) -> Iterable[str]:
"""
Sorts the items based on the given sorting strategy.
"""
if strategy == SortingStrategy.ALPHABETICAL_ASC:
return sorted(items)
elif strategy == SortingStrategy.ALPHABETICAL_DESC:
return reversed(sorted(items))
elif strategy == SortingStrategy.PRESERVE:
return items
else:
raise NotImplementedError(f"The strategy {strategy.name} is not implemented.")
def to_bibtex(parsed):
"""
Convenience function for backwards compatibility.
"""
return BibTexWriter().write(parsed)
def _str_or_expr_to_bibtex(e):
if isinstance(e, BibDataStringExpression):
return ' # '.join([_str_or_expr_to_bibtex(s) for s in e.expr])
elif isinstance(e, BibDataString):
return e.name
else:
return '{' + e + '}'
[docs]class BibTexWriter(object):
"""
Writer to convert a :class:`BibDatabase` object to a string or file formatted as a BibTeX file.
Example::
from bibtexparser.bwriter import BibTexWriter
bib_database = ...
writer = BibTexWriter()
writer.contents = ['comments', 'entries']
writer.indent = ' '
writer.order_entries_by = ('ENTRYTYPE', 'author', 'year')
bibtex_str = bibtexparser.dumps(bib_database, writer)
"""
_valid_contents = ['entries', 'comments', 'preambles', 'strings']
def __init__(self, write_common_strings=False):
#: List of BibTeX elements to write, valid values are `entries`, `comments`, `preambles`, `strings`.
self.contents = ['comments', 'preambles', 'strings', 'entries']
#: Character(s) for indenting BibTeX field-value pairs. Default: single space.
self.indent = ' '
#: 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', )`.
self.order_entries_by = ('ID', )
#: Tuple of fields for display order in a single BibTeX entry. Fields not listed here will be displayed at the
# end in the order defined by display_order_sorting. Default: '[]'
self.display_order = []
# Sorting strategy for entries not contained in display_order. Entries not defined in display_order are added
# at the end in the order defined by this strategy. Default: SortingStrategy.ALPHABETICAL_ASC
self.display_order_sorting: SortingStrategy = SortingStrategy.ALPHABETICAL_ASC
#: BibTeX syntax allows comma first syntax
#: (common in functional languages), use this to enable
#: comma first syntax as the bwriter output
self.comma_first = False
#: BibTeX syntax allows the comma to be optional at the end of the last field in an entry.
#: Use this to enable writing this last comma in the bwriter output. Defaults: False.
self.add_trailing_comma = False
#: internal variable used if self.align_values = True
self._max_field_width = 0
#: Whether common strings are written
self.common_strings = write_common_strings
[docs] def write(self, bib_database):
"""
Converts a bibliographic database to a BibTeX-formatted string.
:param bib_database: bibliographic database to be converted to a BibTeX string
:type bib_database: BibDatabase
:return: BibTeX-formatted string
:rtype: str or unicode
"""
bibtex = ''
for content in self.contents:
try:
# Add each element set (entries, comments)
bibtex += getattr(self, '_' + content + '_to_bibtex')(bib_database)
except AttributeError:
logger.warning("BibTeX item '{}' does not exist and will not be written. Valid items are {}."
.format(content, self._valid_contents))
return bibtex
def _entries_to_bibtex(self, bib_database):
if self.order_entries_by:
# TODO: allow sort field does not exist for entry
entries = sorted(bib_database.entries, key=lambda x: BibDatabase.entry_sort_key(x, self.order_entries_by))
else:
entries = bib_database.entries
if self.align_values:
# determine maximum field width to be used
widths = [max(map(len, entry.keys())) for entry in entries]
self._max_field_width = max(widths)
return self.entry_separator.join(self._entry_to_bibtex(entry) for entry in entries)
def _entry_to_bibtex(self, entry):
bibtex = ''
# Write BibTeX key
bibtex += '@' + entry['ENTRYTYPE'] + '{' + entry['ID']
# create display_order of fields for this entry
# first those keys which are both in self.display_order and in entry.keys
display_order = [i for i in self.display_order if i in entry]
# then all the other fields sorted alphabetically
display_order += [i for i in _apply_sorting_strategy(self.display_order_sorting, entry) if i not in self.display_order]
if self.comma_first:
field_fmt = u"\n{indent}, {field:<{field_max_w}} = {value}"
else:
field_fmt = u",\n{indent}{field:<{field_max_w}} = {value}"
# 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=value)
except TypeError:
raise TypeError(u"The field %s in entry %s must be a string"
% (field, entry['ID']))
if self.add_trailing_comma:
if self.comma_first:
bibtex += '\n'+self.indent+','
else:
bibtex += ','
bibtex += "\n}\n"
return bibtex
def _comments_to_bibtex(self, bib_database):
return ''.join(['@comment{{{0}}}\n{1}'.format(comment, self.entry_separator)
for comment in bib_database.comments])
def _preambles_to_bibtex(self, bib_database):
return ''.join(['@preamble{{"{0}"}}\n{1}'.format(preamble, self.entry_separator)
for preamble in bib_database.preambles])
def _strings_to_bibtex(self, bib_database):
return ''.join([
u'@string{{{name} = {value}}}\n{sep}'.format(
name=name,
value=_str_or_expr_to_bibtex(value),
sep=self.entry_separator)
for name, value in bib_database.strings.items()
if (self.common_strings or
name not in COMMON_STRINGS or # user defined string
value != COMMON_STRINGS[name] # string has been updated
)])