# Source code for bibtexparser.bparser

    # -*- coding: utf-8 -*-

    # Original source: github.com/okfn/bibserver
    # Authors:
    # markmacgillivray
    # Etienne Posthumus (epoz)
    # Francois Boulogne <fboulogne at april dot org>

    import io
    import logging
    import warnings

    from bibtexparser.bibdatabase import (BibDatabase, BibDataString, as_text,
                                          BibDataStringExpression, STANDARD_TYPES)
    from bibtexparser.bibtexexpression import BibtexExpression

    logger = logging.getLogger(__name__)

    __all__ = ['BibTexParser']

    def parse(data, *args, **kwargs):
        parser = BibTexParser(*args, **kwargs)
        return parser.parse(data)

    class BibTexParser(object):
        """
        A parser for reading BibTeX bibliographic data files.

        Example::

            from bibtexparser.bparser import BibTexParser

            bibtex_str = ...

            parser = BibTexParser()
            parser.ignore_nonstandard_types = False
            parser.homogenize_fields = False
            parser.common_strings = False
            bib_database = bibtexparser.loads(bibtex_str, parser)

        :param customization: function or None (default)
            Customization to apply to parsed entries.
        :param ignore_nonstandard_types: bool (default True)
            If True ignores non-standard bibtex entry types.
        :param homogenize_fields: bool (default False)
            Common field name replacements (as set in alt_dict attribute).
        :param interpolate_strings: bool (default True)
            If True, replace bibtex string by their value, else uses
            BibDataString objects.
        :param common_strings: bool (default True)
            Include common string definitions (e.g. month abbreviations) to
            the bibtex file.
        :param add_missing_from_crossref: bool (default False)
            Resolve BibTeX references set in the crossref field for BibTeX entries
            and add the fields from the referenced entry to the referencing entry.
        """

        def __new__(cls, data=None, **args):
            """
            To catch the old API structure in which creating the parser would
            immediately parse and return data.
            """

            if data is None:
                return super(BibTexParser, cls).__new__(cls)
            else:
                # For backwards compatibility: if data is given, parse
                # and return the `BibDatabase` object instead of the parser.
                return parse(data, **args)

        def __init__(self, data=None,
                     customization=None,
                     ignore_nonstandard_types=True,
                     homogenize_fields=False,
                     interpolate_strings=True,
                     common_strings=True,
                     add_missing_from_crossref=False):
            """
            Creates a parser for reading BibTeX files

            :return: parser
            :rtype: `BibTexParser`
            """

            self._parse_call_count = 0
            #: Parsers can be called multiple times, merging databases, but raising a warning.
            #      Set this to `True` to disable the warning. Default: `False`
            self.expect_multiple_parse = False

            self.bib_database = BibDatabase()

            #: Load common strings such as months abbreviation
            #: Default: `True` (new in 1.4.0 - was previously `False`)
            self.common_strings = common_strings
            if self.common_strings:
                self.bib_database.load_common_strings()

            #: Callback function to process BibTeX entries after parsing,
            #: for example to create a list from a string with multiple values.
            #: By default all BibTeX values are treated as simple strings.
            #: Default: `None`.
            self.customization = customization

            #: Ignore non-standard BibTeX types (`book`, `article`, etc).
            #: Default: `True`.
            self.ignore_nonstandard_types = ignore_nonstandard_types

            #: Sanitize BibTeX field names, for example change `url` to `link` etc.
            #: Field names are always converted to lowercase names.
            #: Default: `False`.
            self.homogenize_fields = homogenize_fields

            #: Interpolate Bibtex Strings or keep the structure
            self.interpolate_strings = interpolate_strings

            # On some sample data files, the character encoding detection simply
            # hangs We are going to default to utf8, and mandate it.
            self.encoding = 'utf8'

            # Add missing field from cross-ref
            self.add_missing_from_crossref = add_missing_from_crossref

            # pre-defined set of key changes
            self.alt_dict = {
                'keyw': u'keyword',
                'keywords': u'keyword',
                'authors': u'author',
                'editors': u'editor',
                'urls': u'url',
                'link': u'url',
                'links': u'url',
                'subjects': u'subject',
                'xref': u'crossref'
            }

            # Setup the parser expression
            self._init_expressions()

        def parse(self, bibtex_str, partial=False):
            """Parse a BibTeX string into an object

            :param bibtex_str: BibTeX string
            :type: str or unicode
            :param partial: If True, print errors only on parsing failures.
                If False, an exception is raised.
            :type: boolean
            :return: bibliographic database
            :rtype: BibDatabase
            """

            self._parse_call_count += 1
            if self._parse_call_count == 2 and not self.expect_multiple_parse:
                warnings.warn("The parser has been called more than once. "
                              "Subsequent parse calls lead to a combined BibTeX library. \n"
                              "To avoid the warning, set property `parser.expect_multiple_parse` to `True`.",
                              category=UserWarning, stacklevel=2)

            bibtex_file_obj = self._bibtex_file_obj(bibtex_str)
            try:
                self._expr.parse_file(bibtex_file_obj)
            except self._expr.ParseException as exc:
                logger.error("Could not parse properly, starting at %s", exc.line)
                if not partial:
                    raise exc

            if self.add_missing_from_crossref:
                self.bib_database.add_missing_from_crossref()

            return self.bib_database

        def parse_file(self, file, partial=False):
            """Parse a BibTeX file into an object

            :param file: BibTeX file or file-like object
            :type: file
            :param partial: If True, print errors only on parsing failures.
                If False, an exception is raised.
            :type: boolean
            :return: bibliographic database
            :rtype: BibDatabase
            """
            return self.parse(file.read(), partial=partial)

        def _init_expressions(self):
            """
            Defines all parser expressions used internally.
            """
            self._expr = BibtexExpression()

            # Handle string as BibDataString object
            self._expr.set_string_name_parse_action(
                lambda s, l, t:
                    BibDataString(self.bib_database, t[0]))

            # Add notice to logger
            self._expr.add_log_function(logger.debug)

            # Set actions
            self._expr.entry.add_parse_action(
                lambda s, l, t: self._add_entry(
                    t.get('EntryType'), t.get('Key'), t.get('Fields'))
                )
            self._expr.implicit_comment.add_parse_action(
                lambda s, l, t: self._add_comment(t[0])
                )
            self._expr.explicit_comment.add_parse_action(
                lambda s, l, t: self._add_comment(t[0])
                )
            self._expr.preamble_decl.add_parse_action(
                lambda s, l, t: self._add_preamble(t[0])
                )
            self._expr.string_def.add_parse_action(
                lambda s, l, t: self._add_string(t['StringName'].name,
                                                 t['StringValue'])
                )

        def _bibtex_file_obj(self, bibtex_str):
            # Some files have Byte-order marks inserted at the start
            byte = b'\xef\xbb\xbf'

            if isinstance(bibtex_str, str):
                byte = str(byte, self.encoding, 'ignore')
                if len(bibtex_str) >= 1 and bibtex_str[0] == byte:
                    bibtex_str = bibtex_str[1:]
            else:
                if len(bibtex_str) >= 3 and bibtex_str[:3] == byte:
                    bibtex_str = bibtex_str[3:]
                bibtex_str = bibtex_str.decode(encoding=self.encoding)
            return io.StringIO(bibtex_str)

        def _clean_val(self, val):
            """ Clean instring before adding to dictionary

            :param val: a value
            :type val: string
            :returns: string -- value
            """
            if not val or val == "{}":
                return ''
            elif self.interpolate_strings:
                return as_text(val)
            else:
                return val

        def _clean_key(self, key):
            """ Lowercase a key and return as unicode.

            :param key: a key
            :type key: string
            :returns: (unicode) string -- value
            """
            key = key.lower()
            if not isinstance(key, str):
                return str(key, 'utf-8')
            else:
                return key

        def _clean_field_key(self, key):
            """ Clean a bibtex field key and homogenize alternative forms.

            :param key: a key
            :type key: string
            :returns: string -- value
            """
            key = self._clean_key(key)
            if self.homogenize_fields:
                if key in list(self.alt_dict.keys()):
                    key = self.alt_dict[key]
            return key

        def _add_entry(self, entry_type, entry_id, fields):
            """ Adds a parsed entry.
            Includes checking type and fields, cleaning, applying customizations.

            :param entry_type: the entry type
            :type entry_type: string
            :param entry_id: the entry bibid
            :type entry_id: string
            :param fields: the fields and values
            :type fields: dictionary
            :returns: string -- value
            """
            d = {}
            entry_type = self._clean_key(entry_type)
            if self.ignore_nonstandard_types and entry_type not in STANDARD_TYPES:
                logger.warning('Entry type %s not standard. Not considered.',
                               entry_type)
                return
            for key in fields:
                d[self._clean_field_key(key)] = self._clean_val(fields[key])
            d['ENTRYTYPE'] = entry_type
            d['ID'] = entry_id

            crossref = d.get('crossref', None)
            if self.add_missing_from_crossref and crossref is not None:
                d['_crossref'] = crossref

            if self.customization is not None:
                logger.debug('Apply customizations and return dict')
                d = self.customization(d)

            self.bib_database.entries.append(d)

        def _add_comment(self, comment):
            """
            Stores a comment in the list of comment.

            :param comment: the parsed comment
            :type comment: string
            """
            logger.debug('Store comment in list of comments: ' +
                         comment.__repr__())
            self.bib_database.comments.append(comment)

        def _add_string(self, string_key, string):
            """
            Stores a new string in the string dictionary.

            :param string_key: the string key
            :type string_key: string
            :param string: the string value
            :type string: string
            """
            if string_key in self.bib_database.strings:
                logger.warning('Overwriting existing string for key: %s.',
                               string_key)
            logger.debug(u'Store string: {} -> {}'.format(string_key, string))
            self.bib_database.strings[string_key] = self._clean_val(string)

        def _add_preamble(self, preamble):
            """
            Stores a preamble.

            :param preamble: the parsed preamble
            :type preamble: string
            """
            logger.debug('Store preamble in list of preambles')
            self.bib_database.preambles.append(preamble)