# Source code for bibtexparser.bibtexexpression

    import pyparsing as pp

    from .bibdatabase import BibDataStringExpression

    # General helpers

    def _strip_after_new_lines(s):
        """Removes leading and trailing whitespaces in all but first line."""
        lines = s.splitlines()
        if len(lines) > 1:
            lines = [lines[0]] + [l.lstrip() for l in lines[1:]]
        return '\n'.join(lines)

    def strip_after_new_lines(s):
        """Removes leading and trailing whitespaces in all but first line.

        :param s: string or BibDataStringExpression
        """
        if isinstance(s, BibDataStringExpression):
            s.apply_on_strings(_strip_after_new_lines)
            return s
        else:
            return _strip_after_new_lines(s)

    def add_logger_parse_action(expr, log_func):
        """Register a callback on expression parsing with the adequate message."""
        def action(s, l, t):
            log_func("Found {}: {}".format(expr.resultsName, t))
        expr.add_parse_action(action)

    # Parse action helpers
    # Helpers for returning values from the parsed tokens. Shaped as pyparsing's
    # parse actions. See pyparsing documentation for the arguments.

    def first_token(string_, location, token):
        # TODO Handle this case correctly!
        assert(len(token) == 1)
        return token[0]

    def remove_trailing_newlines(string_, location, token):
        if token[0]:
            return token[0].rstrip('\n')

    def remove_braces(string_, location, token):
        if len(token[0]) < 1:
            return ''
        else:
            start = 1 if token[0][0] == '{' else 0
            end = -1 if token[0][-1] == '}' else None
            return token[0][start:end]

    def field_to_pair(string_, location, token):
        """
        Looks for parsed element named 'Field'.

        :returns: (name, value).
        """
        field = token.get('Field')
        value = field.get('Value')
        if isinstance(value, pp.ParseResults):
            # For pyparsing >= 2.3.1 (see #225 and API change note in pyparsing's
            # Changelog).
            value = value[0]
        return (field.get('FieldName'),
                strip_after_new_lines(value))

    # Expressions helpers

    def in_braces_or_pars(exp):
        """
        exp -> (exp)|{exp}
        """
        return ((pp.Suppress('{') + exp + pp.Suppress('}')) |
                (pp.Suppress('(') + exp + pp.Suppress(')')))

    class BibtexExpression(object):
        """Gives access to pyparsing expressions.

        Attributes are pyparsing expressions for the following elements:

        * main_expression: the bibtex file
        * string_def: a string definition
        * preamble_decl: a preamble declaration
        * explicit_comment: an explicit comment
        * entry: an entry definition
        * implicit_comment: an implicit comment

        """

        ParseException = pp.ParseException

        def __init__(self):
            # Init parse action functions
            self.set_string_name_parse_action(lambda s, l, t: None)

            # Bibtex keywords

            string_def_start = pp.CaselessKeyword("@string")
            preamble_start = pp.CaselessKeyword("@preamble")
            comment_line_start = pp.CaselessKeyword('@comment')

            # String names
            string_name = pp.Word(pp.alphanums + '_-:')('StringName')
            self.set_string_name_parse_action(lambda s, l, t: None)
            string_name.add_parse_action(self._string_name_parse_action)

            # Values inside bibtex fields
            # Values can be integer or string expressions. The latter may use
            # quoted or braced values.

            # Integer values
            integer = pp.Word(pp.nums)('Integer')

            # Braced values: braced values can contain nested (but balanced) braces
            braced_value_content = pp.CharsNotIn('{}')
            braced_value = pp.Forward()  # Recursive definition for nested braces
            braced_value <<= pp.original_text_for(
                '{' + pp.ZeroOrMore(braced_value | braced_value_content) + '}'
                )('BracedValue')
            braced_value.set_parse_action(remove_braces)
            # TODO add ignore for "\}" and "\{" ?
            # TODO @ are not parsed by bibtex in braces

            # Quoted values: may contain braced content with balanced braces
            brace_in_quoted = pp.nested_expr('{', '}', ignore_expr=None)
            text_in_quoted = pp.CharsNotIn('"{}')
            # (quotes should be escaped by braces in quoted value)
            quoted_value = pp.original_text_for(
                '"' + pp.ZeroOrMore(text_in_quoted | brace_in_quoted) + '"'
                )('QuotedValue')
            quoted_value.add_parse_action(pp.remove_quotes)

            # String expressions
            string_expr = pp.DelimitedList(
                (quoted_value | braced_value | string_name), delim='#'
                )('StringExpression')
            string_expr.add_parse_action(self._string_expr_parse_action)

            value = (integer | string_expr)('Value')

            # Entries

            # @EntryType { ...
            entry_type = (pp.Suppress('@') + pp.Word(pp.alphas))('EntryType')
            entry_type.set_parse_action(first_token)

            # Entry key: any character up to a ',' without leading and trailing
            # spaces. Also exclude spaces and prevent it from being empty.
            key = pp.SkipTo(',')('Key')  # TODO Maybe also exclude @',\#}{~%

            def citekeyParseAction(string_, location, token):
                """Parse action for validating citekeys.

                It ensures citekey is not empty and has no space.

                :args: see pyparsing documentation.
                """
                key = first_token(string_, location, token).strip()
                if len(key) < 1:
                    raise self.ParseException(
                        string_, loc=location, msg="Empty citekeys are not allowed.")
                for i, c in enumerate(key):
                    if c.isspace():
                        raise self.ParseException(
                            string_, loc=(location + i),
                            msg="Whitespace not allowed in citekeys.")
                return key

            key.set_parse_action(citekeyParseAction)

            # Field name: word of letters, digits, dashes and underscores
            field_name = pp.Word(pp.alphanums + '_-().+')('FieldName')
            field_name.set_parse_action(first_token)

            # Field: field_name = value
            field = pp.Group(field_name + pp.Suppress('=') + value)('Field')
            field.set_parse_action(field_to_pair)

            # List of fields: comma separeted fields
            field_list = (pp.DelimitedList(field) + pp.Suppress(pp.Optional(','))
                          )('Fields')
            field_list.set_parse_action(
                lambda s, l, t: {k: v for (k, v) in reversed(t.get('Fields'))})

            # Entry: type, key, and fields
            self.entry = (entry_type +
                          in_braces_or_pars(key + pp.Suppress(',') + field_list)
                          )('Entry')

            # Other stuff: comments, string definitions, and preamble declarations

            # Explicit comments: @comment + everything up to next valid declaration
            # starting on new line.
            not_an_implicit_comment = (pp.LineEnd() + pp.Literal('@')
                                       ) | pp.StringEnd()
            self.explicit_comment = (
                pp.Suppress(comment_line_start) +
                pp.original_text_for(pp.SkipTo(not_an_implicit_comment),
                                   as_string=True))('ExplicitComment')
            self.explicit_comment.add_parse_action(remove_trailing_newlines)
            self.explicit_comment.add_parse_action(remove_braces)
            # Previous implementation included comment until next '}'.
            # This is however not inline with bibtex behavior that is to only
            # ignore until EOL. Brace stipping is arbitrary here but avoids
            # duplication on bibtex write.

            # Empty implicit_comments lead to infinite loop of zeroOrMore
            def mustNotBeEmpty(t):
                if not t[0]:
                    raise pp.ParseException("Match must not be empty.")

            # Implicit comments: not anything else
            self.implicit_comment = pp.original_text_for(
                pp.SkipTo(not_an_implicit_comment).set_parse_action(mustNotBeEmpty),
                as_string=True)('ImplicitComment')
            self.implicit_comment.add_parse_action(remove_trailing_newlines)

            # String definition
            self.string_def = (pp.Suppress(string_def_start) + in_braces_or_pars(
                string_name +
                pp.Suppress('=') +
                string_expr('StringValue')
                ))('StringDefinition')

            # Preamble declaration
            self.preamble_decl = (pp.Suppress(preamble_start) +
                                  in_braces_or_pars(value))('PreambleDeclaration')

            # Main bibtex expression

            self.main_expression = pp.ZeroOrMore(
                    self.string_def |
                    self.preamble_decl |
                    self.explicit_comment |
                    self.entry |
                    self.implicit_comment)

        def add_log_function(self, log_fun):
            """Add notice to logger on entry, comment, preamble, string definitions.

            :param log_fun: logger function
            """
            for e in [self.entry,
                      self.implicit_comment,
                      self.explicit_comment,
                      self.preamble_decl,
                      self.string_def]:
                add_logger_parse_action(e, log_fun)

        def set_string_name_parse_action(self, fun):
            """Set the parseAction for string name expression.

            .. Note::

                For some reason pyparsing duplicates the string_name
                expression so setting its parseAction a posteriori has no effect
                in the context of a string expression. This is why this function
                should be used instead.
            """
            self._string_name_parse_action_fun = fun

        def _string_name_parse_action(self, s, l, t):
            return self._string_name_parse_action_fun(s, l, t)

        def _string_expr_parse_action(self, s, l, t):
            return BibDataStringExpression.expression_if_needed(t)

        def parse_file(self, file_obj):
            """Execute parse expression on a file object"""
            return self.main_expression.parse_file(file_obj, parse_all=True)

        def parseFile(self, file_obj):
            """Execute parse expression on a file object

            Alias for parse_file()
            """
            return self.parse_file(file_obj)