Return a :class:`SiteConfig` built from a ``config_string``.

    Simple syntax errors are just plainly ignored, and logged as warnings.

    :param config_string: a full site config file, raw-loaded from storage
        with something like
        ``config_string = open('path/to/site/config.txt', 'r').read()``.
    :type config_string: str or unicode

    :returns: a :class:`SiteConfig` instance.
    :raises: :class:`InvalidSiteConfig` in case of an unrecoverable error.

    .. note:: See the source code for supported directives names.

def ftr_string_to_instance(config_string):
    """ Return a :class:`SiteConfig` built from a ``config_string``.

    Simple syntax errors are just plainly ignored, and logged as warnings.

    :param config_string: a full site config file, raw-loaded from storage
        with something like
        ``config_string = open('path/to/site/config.txt', 'r').read()``.
    :type config_string: str or unicode

    :returns: a :class:`SiteConfig` instance.
    :raises: :class:`InvalidSiteConfig` in case of an unrecoverable error.

    .. note:: See the source code for supported directives names.
    """

    config = SiteConfig()

    for line_number, line_content in enumerate(
            config_string.strip().split(u'\n'), start=1):

        line_content = line_content.strip()

        # Skip empty lines & comments.
        if not line_content or line_content.startswith(u'#'):
            continue

        try:
            key, value = [
                x.strip() for x in line_content.strip().split(u':', 1)
            ]

        except:
            LOGGER.warning(u'Unrecognized syntax “%s” on line #%s.',
                           line_content, line_number)
            continue

        # handle some very rare title()d directives.
        key = key.lower()

        if not key or (not value and key != 'replace_string'):
            LOGGER.warning(u'Empty key or value in “%s” on line #%s.',
                           line_content, line_number)
            continue

        # Commands for which we accept multiple statements.
        elif key in (
            'title', 'body', 'author', 'date',
            'strip', 'strip_id_or_class', 'strip_image_src',
            'single_page_link', 'single_page_link_in_feed',
            'next_page_link',
            'http_header',

            'find_string',
            'replace_string',

            'test_url',
            'test_contains',
            'test_title',
            'test_date',
            'test_author',
            'test_language',
        ):

            if key.endswith(u'_string'):
                # Append to list. Duplicites are allowed.
                getattr(config, key).append(value)

            else:
                # Add to set, preserving order but squashing duplicates.
                getattr(config, key).add(value)

        # Single statement commands that evaluate to True or False.
        elif key in ('tidy', 'prune', 'autodetect_on_failure', ):

            if value.lower() in ('no', 'false', '0', ):
                setattr(config, key, False)

            else:
                setattr(config, key, bool(value))

        # Single statement commands stored as strings.
        elif key in ('parser', ):
            setattr(config, key, value)

        # The “replace_string(………): replace_value” one-liner syntax.
        elif key.startswith('replace_string(') and key.endswith(')'):
            # These 2 are lists, not sets.
            config.find_string.append(key[15:-1])
            config.replace_string.append(value)

        else:
            LOGGER.warning(u'Unsupported directive “%s” on line #%s.',
                           line_content, line_number)

    find_count = len(config.find_string)
    replace_count = len(config.replace_string)

    if find_count != replace_count:
        raise InvalidSiteConfig(u'find_string and remplace_string do not '
                                u'correspond ({0} != {1})'.format(
                                    find_count, replace_count))

    return config