@abstractmethod
def normalize(self, text):
    """Normalize the text.

    :param string text: The text to normalize.
    :returns: Normalized text.
    :rtype: string
    """
    return text class Chain(object):
    """Apply a series of processors in turn. Stops if a processors returns None."""

    def __init__(self, *callables):
        self.callables = callables

    def __call__(self, value):
        for func in self.callables:
            if value is None:
                break
            value = func(value)
        return value class LStrip(object):
    """Remove a substring from the start of a value."""

    def __init__(self, *substrings):
        self.substrings = substrings

    def __call__(self, value):
        for substring in self.substrings:
            if value.startswith(substring):
                return value[len(substring):]
        return value class RStrip(object):
    """Remove a substring from the end of a value."""

    def __init__(self, *substrings):
        self.substrings = substrings

    def __call__(self, value):
        for substring in self.substrings:
            if value.endswith(substring):
                return value[:-len(substring)]
        return value class Discard(object):
    """Return None if value matches a string."""

    def __init__(self, *match):
        self.match = match

    def __call__(self, value):
        if value in self.match:
            return None
        return value class Cleaner(object):
    """Clean HTML or XML by removing tags completely or replacing with their contents.

    A Cleaner instance provides a ``clean_markup`` method::

        cleaner = Cleaner()
        htmlstring = '<html><body><script>alert("test")</script><p>Some text</p></body></html>'
        print(cleaner.clean_markup(htmlstring))

    A Cleaner instance is also a callable that can be applied to lxml document trees::

        tree = lxml.etree.fromstring(htmlstring)
        cleaner(tree)
        print(lxml.etree.tostring(tree))

    Elements that are matched by ``kill_xpath`` are removed entirely, along with their contents. By default,
    ``kill_xpath`` matches all script and style tags, as well as comments and processing instructions.

    Elements that are matched by ``strip_xpath`` are replaced with their contents. By default, no elements are stripped.
    A common use-case is to set ``strip_xpath`` to ``.//*``, which specifies that all elements should be stripped.

    Elements that are matched by ``allow_xpath`` are excepted from stripping, even if they are also matched by
    ``strip_xpath``. This is useful when setting ``strip_xpath`` to strip all tags, allowing a few expections to be
    specified by ``allow_xpath``.
    """

    kill_xpath = './/script | .//style | .//comment() | .//processing-instruction() | .//*[@style="display:none;"]'
    strip_xpath = None
    allow_xpath = None
    fix_whitespace = True

    namespaces = {
        're': 'http://exslt.org/regular-expressions',
        'set': 'http://exslt.org/sets',
        'dc': 'http://purl.org/dc/elements/1.1/',
        'prism': 'http://prismstandard.org/namespaces/basic/2.0/',
        'xml': 'http://www.w3.org/XML/1998/namespace',
        'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns',
    }

    def __init__(self, **kwargs):
        """Behaviour can be customized by overriding attributes in a subclass or setting them in the constructor.

        :param string kill_xpath: XPath expression for tags to remove along with their contents.
        :param string strip_xpath: XPath expression for tags to replace with their contents.
        :param string allow_xpath: XPath expression for tags to except from strip_xpath.
        :param bool fix_whitespace: Normalize whitespace to a single space and ensure newlines around block elements.
        :param dict namespaces: Namespace prefixes to register for the XPaths.
        """
        # TODO: This is weird. Why don't we change to proper individual keyword arguments with class attribs as default
        for name, value in kwargs.items():
            if not hasattr(self, name):
                raise TypeError('Unknown parameter: %s=%r' % (name, value))
            setattr(self, name, value)

    def __call__(self, doc):
        """Clean the document."""
        if hasattr(doc, 'getroot'):
            doc = doc.getroot()

        if self.fix_whitespace:
            # Ensure newlines around block elements
            for el in doc.iterdescendants():
                if el.tag in BLOCK_ELEMENTS:
                    el.tail = (el.tail or '') + '\n'
                    previous = el.getprevious()
                    parent = el.getparent()
                    if previous is None:
                        parent.text = (parent.text or '') + '\n'
                    else:
                        previous.tail = (previous.tail or '') + '\n'

        # Remove elements that match kill_xpath
        if self.kill_xpath:
            for el in doc.xpath(self.kill_xpath, namespaces=self.namespaces):
                #log.debug('Killing: %s' % tostring(el))
                parent = el.getparent()
                # We can't kill the root element!
                if parent is None:
                    continue
                if el.tail:
                    previous = el.getprevious()
                    if previous is None:
                        parent.text = (parent.text or '') + el.tail
                    else:
                        previous.tail = (previous.tail or '') + el.tail
                parent.remove(el)

        # Collect all the allowed elements
        to_keep = [el for el in doc.xpath(self.allow_xpath, namespaces=self.namespaces)] if self.allow_xpath else []

        # Replace elements that match strip_xpath with their contents
        if self.strip_xpath:
            for el in doc.xpath(self.strip_xpath, namespaces=self.namespaces):
                # Skip if allowed by allow_xpath
                if el in to_keep:
                    continue
                parent = el.getparent()
                previous = el.getprevious()
                # We can't strip the root element!
                if parent is None:
                    continue
                # Append the text to previous tail (or parent text if no previous), ensuring newline if block level
                if el.text and isinstance(el.tag, six.string_types):
                    if previous is None:
                        parent.text = (parent.text or '') + el.text
                    else:
                        previous.tail = (previous.tail or '') + el.text
                # Append the tail to last child tail, or previous tail, or parent text, ensuring newline if block level
                if el.tail:
                    if len(el):
                        last = el[-1]
                        last.tail = (last.tail or '') + el.tail
                    elif previous is None:
                        parent.text = (parent.text or '') + el.tail
                    else:
                        previous.tail = (previous.tail or '') + el.tail
                index = parent.index(el)
                parent[index:index+1] = el[:]

        # Collapse whitespace down to a single space or a single newline
        if self.fix_whitespace:
            for el in doc.iter():
                if el.text is not None:
                    el.text = re.sub(r'\s*\n\s*', '\n', el.text)
                    el.text = re.sub(r'[ \t]+', ' ', el.text)
                    # el.text = re.sub(r'\s+', ' ', el.text)
                if el.tail is not None:
                    el.tail = re.sub(r'\s*\n\s*', '\n', el.tail)
                    el.tail = re.sub(r'[ \t]+', ' ', el.tail)
                    # el.tail = re.sub(r'\s+', ' ', el.tail)

    def clean_html(self, html):
        """Apply ``Cleaner`` to HTML string or document and return a cleaned string or document."""
        result_type = type(html)
        if isinstance(html, six.string_types):
            doc = html_fromstring(html)
        else:
            doc = copy.deepcopy(html)
        self(doc)
        if issubclass(result_type, six.binary_type):
            return tostring(doc, encoding='utf-8')
        elif issubclass(result_type, six.text_type):
            return tostring(doc, encoding='unicode')
        else:
            return doc

    def clean_markup(self, markup, parser=None):
        """Apply ``Cleaner`` to markup string or document and return a cleaned string or document."""
        result_type = type(markup)
        if isinstance(markup, six.string_types):
            doc = fromstring(markup, parser=parser)
        else:
            doc = copy.deepcopy(markup)
        self(doc)
        if issubclass(result_type, six.binary_type):
            return tostring(doc, encoding='utf-8')
        elif issubclass(result_type, six.text_type):
            return tostring(doc, encoding='unicode')
        else:
            return doc class Entity(six.with_metaclass(EntityMeta, BaseEntity)):

    fields = {}

    def __init__(self, selector):
        """

        :param Selector selector: The selector to scrape.
        """
        self._values = {}
        # Iterate all defined fields
        for field_name, field in six.iteritems(self.fields):
            # Scrape field values from selector
            cleaner = getattr(self, 'clean_%s' % field_name, None)
            processor = getattr(self, 'process_%s' % field_name, None)
            value = field.scrape(selector, cleaner=cleaner, processor=processor)
            # Finalize value using finalize_* method on scrape, if it exists
            if hasattr(self, 'finalize_%s' % field_name):
                value = getattr(self, 'finalize_%s' % field_name)(value)
            log.debug('Assigning %s: %s' % (field_name, value))
            setattr(self, field_name, value)

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self._values == other._values
        return False

    def __len__(self):
        return len(self._values)

    def __iter__(self):
        return iter(self._values)

    def __delattr__(self, attr):
        """Handle deletion of field values by setting to default if specified."""
        # Set to default value
        if attr in self.fields:
            setattr(self, attr, self.fields[attr].default)
        else:
            super(Entity, self).__delattr__(attr)

    def __getitem__(self, key):
        """Redirect dictionary-style field access to attribute-style."""
        try:
            if key in self.fields:
                return getattr(self, key)
        except AttributeError:
            pass
        raise KeyError(key)

    def __setitem__(self, key, value):
        """Redirect dictionary-style field setting to attribute-style."""
        if key not in self.fields:
            raise KeyError(key)
        return setattr(self, key, value)

    def __contains__(self, name):
        try:
            val = getattr(self, name)
            return val is not None
        except AttributeError:
            return False

    def __repr__(self):
        return '%s()' % self.__class__.__name__

    @classmethod
    def scrape(cls, selector, root, xpath=False):
        """Return EntityList for the given selector."""
        log.debug('Called scrape classmethod with root: %s' % root)
        roots = selector.xpath(root) if xpath else selector.css(root)
        results = [cls(r) for r in roots]
        return EntityList(*results)

    def serialize(self):
        """Convert Entity to python dictionary."""
        # Serialize fields to a dict
        data = {}
        for field_name in self:
            value = self._values.get(field_name)
            field = self.fields.get(field_name)
            if value is not None:
                if field.all:
                    value = [field.serialize(v) for v in value]
                else:
                    value = field.serialize(value)
            # Skip empty fields unless field.null
            if not field.null and ((field.all and value == []) or (not field.all and value in {None, ''})):
                continue
            data[field.name] = value
        return data

    def to_json(self, *args, **kwargs):
        """Convert Entity to JSON."""
        return json.dumps(self.serialize(), *args, **kwargs) class StringField(BaseField):
    """A string field."""

    def __init__(self, selection, lower=False, upper=False, strip=False, **kwargs):
        """

        :param bool lower: (Optional) Whether to lowercase the string. Default False.
        :param bool upper: (Optional) Whether to uppercase the string. Default False.
        :param bool strip: (Optional) Whether to strip whitespace from start/end. Default False.
        """
        super(StringField, self).__init__(selection, **kwargs)
        self.lower = lower
        self.upper = upper
        self.strip = strip

    def process(self, value):
        value = super(StringField, self).process(value)
        if value is not None:
            if self.strip:
                value = value.strip()
            if self.lower:
                value = value.lower()
            if self.upper:
                value = value.upper()
        return value class EntityField(BaseField):
    """A field that contains another Entity."""

    def __init__(self, entity, selection, **kwargs):
        """

        :param entity: The embedded entity.
        """
        self.entity = entity
        super(EntityField, self).__init__(selection, **kwargs)

    def scrape(self, selector, cleaner=None, processor=None):
        """Scrape the value for this field from the selector."""
        value = self.entity.scrape(selector, root=self.selection, xpath=self.xpath)
        return self._post_scrape(value, processor=processor) class UrlField(StringField):
    """A field with optional URL processing."""

    def __init__(self, selection, strip_querystring=False, **kwargs):
        """

        :param strip_querystring: (Optional) Whether to remove the querystring. Default False.
        """
        self.strip_querystring = strip_querystring
        super(UrlField, self).__init__(selection, **kwargs)

    def process(self, value):
        value = super(UrlField, self).process(value)
        if value is not None and self.strip_querystring:
            value = strip_querystring(value)
        return value class IntField(BaseField):
    """An integer number field."""

    def process(self, value):
        """Convert value to an int."""
        try:
            return int(value)
        except (ValueError, TypeError):
            return None
import logging
from ...text.normalize import normalize
from ...text.processors import Chain, LStrip, RStrip, Discard
from ..clean import Cleaner
from ..entity import Entity
from ..fields import StringField, EntityField, UrlField, IntField
# -*- coding: utf-8 -*-
"""
chemdataextractor.scrape.springer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Tools for scraping documents from Springer, Biomed Central and Chemistry Central XML files.

"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals


log = logging.getLogger(__name__)


#: XML stripper that also kills equations/formulas.
strip_springer_xml = Cleaner(strip_xpath='.//*', kill_xpath='.//display-formula|.//inline-formula|.//m:math|.//abbrgrp', namespaces={'m': 'http://www.w3.org/1998/Math/MathML'})
#: XML stripper that also kills headings
strip_springer_abstract_xml = Cleaner(strip_xpath='.//*', kill_xpath='.//display-formula|.//inline-formula|.//m:math|.//abbrgrp|.//st', namespaces={'m': 'http://www.w3.org/1998/Math/MathML'})


def tidy_springer_references(document):
    """Remove punctuation around references like brackets, commas, hyphens."""

    def strip_preceding(text):
        stext = text.rstrip()
        if stext.endswith('[') or stext.endswith('('):
            #log.debug('%s -> %s' % (text, stext[:-1]))
            return stext[:-1]
        return text

    def strip_between(text):
        stext = text.strip()
        if stext in {',', '-', '\u2013'}:
            #log.debug('%s -> %s' % (text, ''))
            return ''
        return text

    def strip_following(text):
        stext = text.lstrip()
        if stext.startswith(']') or stext.startswith(')'):
            #log.debug('%s -> %s' % (text, stext[1:]))
            return stext[1:]
        return text

    for ref in document.xpath('.//abbrgrp'):
        parent = ref.getparent()
        previous = ref.getprevious()
        next = ref.getnext()
        if previous is None:
            parent.text = strip_preceding(parent.text or '')
        else:
            previous.tail = strip_preceding(previous.tail or '')
        if next is not None and next.tag == 'abbrgrp':
            ref.tail = strip_between(ref.tail or '')
        ref.tail = strip_following(ref.tail or '')
    return document



class SpringerXmlAuthor(Entity):
    """Author information from a Springer XML file."""
    firstname = StringField('./fnm', xpath=True, strip=True)
    middlename = StringField('./mnm|./mi', xpath=True, strip=True)

    lastname = StringField('./snm', xpath=True, strip=True)