# Cython директива для обеспечения использования синтаксиса строк Python 3
# cython: language_level=3str

"""Инструмент для очистки HTML.

Удаляет нежелательные теги и содержимое. Подробнее см. в классе `Cleaner`.
"""

import copy
import re
from urllib.parse import urlsplit, unquote_plus

from lxml import etree
from lxml.html import defs
from lxml.html import fromstring as lxml_fromstring, XHTML_NAMESPACE
from lxml.html import xhtml_to_html, _transform_result

__all__ = ['clean_html', 'clean', 'Cleaner', 'autolink', 'autolink_html',
           'word_break', 'word_break_html']

# Look at http://code.sixapart.com/trac/livejournal/browser/trunk/cgi-bin/cleanhtml.pl
#   Particularly the CSS cleaning; most of the tag cleaning is integrated now
# I have multiple kinds of schemes searched; but should schemes be
#   whitelisted instead?
# max height?
# remove images?  Also in CSS?  background attribute?
# Some way to whitelist object, iframe, etc (e.g., if you want to
#   allow *just* embedded YouTube movies)
# Log what was deleted and why?
# style="behavior: ..." might be bad in IE?
# Should we have something for just <meta http-equiv>?  That's the worst of the
#   metas.
# UTF-7 detections?  Example:
#     <HEAD><META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=UTF-7"> </HEAD>+ADw-SCRIPT+AD4-alert('XSS');+ADw-/SCRIPT+AD4-
#   you don't always have to have the charset set, if the page has no charset
#   and there's UTF7-like code in it.
# Look at these tests: http://htmlpurifier.org/live/smoketests/xssAttacks.php


# This is an IE-specific construct you can have in a stylesheet to
# run some Javascript:
_replace_css_javascript = re.compile(
    r'expression\s*\(.*?\)', re.S|re.I).sub

# Регулярное выражение для удаления CSS-директив `@import`
_replace_css_import = re.compile(
    r'@\s*import', re.I).sub

# Регулярное выражение для определения содержания, которое может являться HTML-тегом
_looks_like_tag_content = re.compile(
    r'</?[a-zA-Z]+|\son[a-zA-Z]+\s*=',
    (re.ASCII)).search

# Регулярное выражение для поиска data URLs для изображений
_find_image_dataurls = re.compile(
    r'data:image/(.+?);base64,', re.I).findall

# Регулярное выражение для поиска потенциально опасных URI-схем
_possibly_malicious_schemes = re.compile(
    r'(javascript|jscript|livescript|vbscript|data|about|mocha):',
    re.I).findall
# SVG images can contain script content
_is_unsafe_image_type = re.compile(r"(xml|svg)", re.I).search

def _has_javascript_scheme(s):
    """
    Проверяет, содержит ли строка схемы JavaScript или небезопасные изображения.
    """
    safe_image_urls = 0
    for image_type in _find_image_dataurls(s):
        if _is_unsafe_image_type(image_type):
            return True
        safe_image_urls += 1
    return len(_possibly_malicious_schemes(s)) > safe_image_urls

# Регулярное выражение для замены пробельных символов и управляющих символов ASCII
_substitute_whitespace = re.compile(r'[\s\x00-\x08\x0B\x0C\x0E-\x19]+').sub

# FIXME: check against: http://msdn2.microsoft.com/en-us/library/ms537512.aspx
_conditional_comment_re = re.compile(
    r'\[if[\s\n\r]+.*?][\s\n\r]*>', re.I|re.S)

# XPath выражение для поиска элементов с атрибутом style
_find_styled_elements = etree.XPath(
    "descendant-or-self::*[@style]")

# XPath выражение для поиска внешних ссылок
_find_external_links = etree.XPath(
    ("descendant-or-self::a  [normalize-space(@href) and substring(normalize-space(@href),1,1) != '#'] |"
     "descendant-or-self::x:a[normalize-space(@href) and substring(normalize-space(@href),1,1) != '#']"),
    namespaces={'x':XHTML_NAMESPACE})

# Регулярное выражение для удаления всех управляющих символов ASCII (00-1F,7F), за исключением:
# - 09 - горизонтальная табуляция
# - 0A - перевод строки
# - 0B - вертикальная табуляция
# - 0D - возврат каретки
_ascii_control_characters = re.compile(r"[\x00-\x08\x0C\x0E-\x1F\x7F]")

def fromstring(string):
    """
    Расширенная функция fromstring, которая удаляет управляющие символы ASCII 
    перед передачей входных данных в оригинальную функцию lxml.html.fromstring.
    """
    from lxml import html
    parser = html.HTMLParser(huge_tree=True, recover=True)
    return lxml_fromstring(_ascii_control_characters.sub("", string), parser=parser)

class Cleaner:
    """
    Экземпляры этого класса очищают документ от возможных опасных элементов.
    Очистка управляется атрибутами; можно переопределить атрибуты в подклассе 
    или задать их в конструкторе.

    ``scripts``:
        Удаляет любые теги ``<script>``.

    ``javascript``:
        Удаляет любые элементы, содержащие JavaScript, такие как атрибут ``onclick``. Также удаляет стили,
        поскольку они могут содержать JavaScript.

    ``comments``:
        Удаляет любые комментарии.

    ``style``:
        Удаляет любые теги ``<style>``.

    ``inline_style``:
        Удаляет любые атрибуты стилей. По умолчанию принимает значение опции ``style``.

    ``links``:
        Удаляет любые теги ``<link>``.

    ``meta``:
        Удаляет любые теги ``<meta>``.

    ``page_structure``:
        Удаляет структурные части страницы: ``<head>``, ``<html>``, ``<title>``.

    ``processing_instructions``:
        Удаляет любые инструкции процессора.

    ``embedded``:
        Удаляет любые встроенные объекты (flash, iframes).

    ``frames``:
        Удаляет любые теги, связанные с фреймами.

    ``forms``:
        Удаляет любые теги формы.

    ``annoying_tags``:
        Удаляет теги, которые не являются неправильными, но раздражают. Например, ``<blink>`` и ``<marquee>``.

    ``remove_tags``:
        Список тегов для удаления. Только теги будут удалены,
        их содержимое будет перемещено в родительский тег.

    ``kill_tags``:
        Список тегов для полного удаления. Удаляются также их содержимое,
        т.е. вся поддерево, а не только сам тег.

    ``allow_tags``:
        Список тегов, которые следует оставить (по умолчанию все теги включены).

    ``remove_unknown_tags``:
        Удаляет любые теги, которые не являются стандартной частью HTML.

    ``safe_attrs_only``:
        Если True, включает только "безопасные" атрибуты (список берется с сайта 
        sanitisation web site от feedparser).

    ``safe_attrs``:
        Набор имен атрибутов, который переопределяет список атрибутов, считаемых "безопасными"
        (когда safe_attrs_only=True).

    ``add_nofollow``:
        Если True, к любым тегам <a> будет добавлен ``rel="nofollow"``.

    ``host_whitelist``:
        Список или набор хостов, которые можно использовать для встроенного содержимого
        (например, для содержимого типа ``<object>``, ``<link rel="stylesheet">`` и т. д.).
        Также можно реализовать/переопределить метод
        ``allow_embedded_url(el, url)`` или ``allow_element(el)``, чтобы реализовать 
        более сложные правила для встроенного содержимого.
        Любое содержимое, которое пройдет этот тест, будет отображаться независимо от
        значения (например) ``embedded``.

        Обратите внимание, что этот параметр может не работать так, как ожидалось, если вы не
        преобразуете ссылки в абсолютные перед очисткой.

        Обратите внимание, что возможно, вам также придется установить ``whitelist_tags``.

    ``whitelist_tags``:
        Набор тегов, которые можно включить с ``host_whitelist``.
        По умолчанию это ``iframe`` и ``embed``; вы можете захотеть
        включить другие теги, такие как ``script``, или вы можете захотеть
        реализовать ``allow_embedded_url`` для большего контроля. Установите None для 
        включения всех тегов.

    Этот метод модифицирует документ на месте.
    """

    scripts = True
    javascript = True
    comments = True
    style = False
    inline_style = None
    links = True
    meta = True
    page_structure = True
    processing_instructions = True
    embedded = True
    frames = True
    forms = True
    annoying_tags = True
    remove_tags = ()
    allow_tags = ()
    kill_tags = ()
    remove_unknown_tags = True
    safe_attrs_only = True
    safe_attrs = None
    add_nofollow = False
    host_whitelist = ()
    whitelist_tags = {'iframe', 'embed'}

    def __init__(self, **kw):
        # Инициализация и настройка параметров очистки
        self.safe_attrs = {'*': defs.safe_attrs}
        not_an_attribute = object()
        for name, value in kw.items():
            default = getattr(self, name, not_an_attribute)
            if default is None or default is True or default is False:
                pass
            elif isinstance(default, (frozenset, set, tuple, list, dict)):
                if isinstance(value, str):
                    raise TypeError(
                        f"Ожидалась коллекция, но получена строка: {name}={value!r}")
            else:
                raise TypeError(
                    f"Неизвестный параметр: {name}={value!r}")
            setattr(self, name, value)

        if self.inline_style is None and 'inline_style' not in kw:
            self.inline_style = self.style

        if kw.get("allow_tags"):
            if kw.get("remove_unknown_tags"):
                raise ValueError("Нелогично передавать оба параметра "
                                 "allow_tags и remove_unknown_tags")
            self.remove_unknown_tags = False

        self.host_whitelist = frozenset(self.host_whitelist) if self.host_whitelist else ()

    # Используется для поиска основного URL для заданного тега, который будет удален:
    _tag_link_attrs = dict(
        script='src',
        link='href',
        applet=['code', 'object'],
        iframe='src',
        embed='src',
        layer='src',
        # FIXME: there doesn't really seem like a general way to figure out what
        # links an <object> tag uses; links often go in <param> tags with values
        # that we don't really know.  You'd have to have knowledge about specific
        # kinds of plugins (probably keyed off classid), and match against those.
        ##object=?,
        # FIXME: not looking at the action currently, because it is more complex
        # than than -- if you keep the form, you should keep the form controls.
        ##form='action',
        a='href',
    )
    def _ensure_root_element(self, doc):
        """
        Проверяет, является ли `doc` элементом или деревом, и возвращает корневой элемент.
        Если передан экземпляр ElementTree, возвращается его корневой элемент.
        """
        try:
            getroot = doc.getroot
        except AttributeError:
            pass  # Если это уже элемент, ничего не делаем
        else:
            doc = getroot()  # Если это ElementTree, получаем корневой элемент
        return doc
    
    def _kill_conditional_comments_if_needed(self, doc):
        """
        Удаляет условные комментарии, специфичные для IE, если включено удаление комментариев.
        Условные комментарии могут содержать HTML, который необходимо удалить для защиты.
        """
        if not self.comments:
            self.kill_conditional_comments(doc)

    def _remove_invalid_param_tags(self, doc):
        """
        Удаляет теги <param>, которые не находятся внутри <applet> или <object>.
        Этот метод используется для очистки некорректных вложений <param>.
        """
        if self.embedded:
            for el in list(doc.iter('param')):
                parent = el.getparent()
                while parent is not None and parent.tag not in ('applet', 'object'):
                    parent = parent.getparent()
                if parent is None:
                    el.drop_tree()

    def _initialize_tags(self):
        """
        Инициализирует наборы тегов для удаления, разрешения и уничтожения.
        Определяет, какие теги будут удалены, какие разрешены, и как обрабатываются неизвестные теги.
        """
        kill_tags = set(self.kill_tags or ())
        remove_tags = set(self.remove_tags or ())
        allow_tags = set(self.allow_tags or ())

        if self.scripts:
            kill_tags.add('script')

        if self.comments:
            kill_tags.add(etree.Comment)

        if self.processing_instructions:
            kill_tags.add(etree.ProcessingInstruction)

        if self.style:
            kill_tags.add('style')

        if self.links:
            kill_tags.add('link')

        if self.meta:
            kill_tags.add('meta')

        if self.page_structure:
            # Удаляем структурные части страницы
            remove_tags.update(('head', 'html', 'title'))

        if self.embedded:
            # FIXME: is <layer> really embedded?
            # We should get rid of any <param> tags not inside <applet>;
            # These are not really valid anyway.
            remove_tags.update(('iframe', 'embed', 'layer', 'object', 'param'))
            kill_tags.add('applet')
        
        if self.frames:
            # FIXME: ideally we should look at the frame links, but
            # generally frames don't mix properly with an HTML
            # fragment anyway.
            kill_tags.update(defs.frame_tags)

        if self.forms:
            remove_tags.add('form')
            kill_tags.update(('button', 'input', 'select', 'textarea'))
        if self.annoying_tags:
            remove_tags.update(('blink', 'marquee'))

        if self.remove_unknown_tags:
            if allow_tags:
                raise ValueError(
                    "Нелогично передавать оба параметра allow_tags и remove_unknown_tags")
            allow_tags = set(defs.tags)

        if allow_tags:
            if not self.comments:
                allow_tags.add(etree.Comment)
            if not self.processing_instructions:
                allow_tags.add(etree.ProcessingInstruction)

        return kill_tags, remove_tags, allow_tags

    def _handle_tags(self, allow_tags, doc):
        """
        Обрабатывает сценарии и удаляет теги, которые не разрешены.
        Перебирает все элементы документа и удаляет теги, которые не разрешены.
        """
        bad = []
        for el in doc.iter():
            if el.tag not in allow_tags:
                bad.append(el)
                # if el.tag not in self.allow_tags:
                #     self.allow_tags.append(el.tag)
        # Если список "плохих" тегов не пуст
        if bad:
            # Проверка, является ли первый элемент корневым элементом документа
            if bad[0] is doc:
                # Если корневой элемент "плохой", преобразуем его в <div> и очищаем атрибуты
                el = bad.pop(0)
                el.tag = 'div'
                el.attrib.clear()

            # Удаление всех остальных "плохих" тегов
            for el in bad:
                el.drop_tag()
    def _remove_unsafe_attributes(self, doc):
        """
        Удаляет небезопасные атрибуты из элементов, если включена эта опция.
        Проверяет каждый атрибут на предмет его безопасности и удаляет те, которые не включены в список безопасных.
        """
        bad_attrs = dict()
        for el in doc.iter(etree.Element):
            safe_attrs = set(self.safe_attrs.get(el.tag, []) + self.safe_attrs.get('*', []))
            attrib = el.attrib
            for aname in attrib.keys():
                if aname not in safe_attrs:
                    if el.tag not in bad_attrs:
                        bad_attrs[el.tag] = set()
                    # if el.tag not in self.safe_attrs:
                    #     self.safe_attrs[el.tag] = list()
                    # if aname not in self.safe_attrs[el.tag]:
                    #     self.safe_attrs[el.tag].append(aname)
                    bad_attrs[el.tag].add(aname)
                    del attrib[aname]
        return bad_attrs

    def _handle_javascript(self, doc):
        """
        Обрабатывает удаление или замену JavaScript в документе.
        Удаляет JavaScript из атрибутов и ссылок, а также из стилей, если это необходимо.
        """
        if not (self.safe_attrs_only and self.safe_attrs == defs.safe_attrs):
            for el in doc.iter(etree.Element):
                attrib = el.attrib
                for aname in attrib.keys():
                    if aname.startswith('on'):
                        del attrib[aname]
        doc.rewrite_links(self._remove_javascript_link, resolve_base_href=False)
        
        if not self.inline_style:
            self._remove_javascript_from_styles(doc)

        if not self.links and (self.style or self.javascript):
            # Если ссылки не должны быть удалены полностью, но JavaScript или стили не разрешены,
            # то удаляем только те ссылки, которые подключают таблицы стилей
            for el in list(doc.iter('link')):
                if 'stylesheet' in el.get('rel', '').lower():
                    if not self.allow_element(el):
                        el.drop_tree()
        
        if not self.style:
            self._remove_style_elements(doc)


    def _remove_javascript_from_styles(self, doc):
        """
        Удаляет JavaScript из атрибутов стилей.
        Проверяет стили на наличие опасных JavaScript выражений и удаляет их.
        """
        for el in _find_styled_elements(doc):
            old = el.get('style')
            new = _replace_css_javascript('', old)
            new = _replace_css_import('', new)
            if self._has_sneaky_javascript(new):
                del el.attrib['style']
            elif new != old:
                el.set('style', new)

    def _remove_style_elements(self, doc):
        """
        Удаляет элементы стилей, содержащие JavaScript.
        Проверяет текст стиля на наличие JavaScript и удаляет его при необходимости.
        """
        for el in list(doc.iter('style')):
            if el.get('type', '').lower().strip() == 'text/javascript':
                el.drop_tree()
                continue
            old = el.text or ''
            new = _replace_css_javascript('', old)
            new = _replace_css_import('', new)
            if self._has_sneaky_javascript(new):
                el.text = '/* deleted */'
            elif new != old:
                el.text = new

    def _handle_kill_and_remove_tags(self, doc, kill_tags, remove_tags):
        """
        Обрабатывает удаление или полное уничтожение тегов в документе на основе настроек.
        Определяет, какие теги должны быть удалены или полностью уничтожены.
        """

        self._remove_invalid_param_tags(doc)
        _remove = []
        _kill = []
        for el in doc.iter():
            if el.tag in kill_tags:
                if self.allow_element(el):
                    continue
                _kill.append(el)
            elif el.tag in remove_tags:
                if self.allow_element(el):
                    continue
                _remove.append(el)

        self._process_remove_and_kill_elements(doc, _remove, _kill)

    def _process_remove_and_kill_elements(self, doc, _remove, _kill):
        """
        Фактически удаляет или уничтожает элементы из документа.
        Реализует логику удаления и очистки элементов из документа.
        """
        if _remove and _remove[0] == doc:
            el = _remove.pop(0)
            el.tag = 'div'
            el.attrib.clear()
        elif _kill and _kill[0] == doc:
            el = _kill.pop(0)
            if el.tag != 'html':
                el.tag = 'div'
            el.clear()

        _kill.reverse()  # начинаем с самых вложенных тегов
        for el in _kill:
            el.drop_tree()
        for el in _remove:
            el.drop_tag()

    def _add_nofollow_to_links(self, doc):
        """
        Добавляет атрибут rel="nofollow" к внешним ссылкам, если включена эта опция.
        Защищает от передачи ранжирования через ссылки на ненадежные сайты.
        """
        for el in _find_external_links(doc):
            if not self.allow_follow(el):
                rel = el.get('rel')
                if rel:
                    if ('nofollow' in rel and ' nofollow ' in (' %s ' % rel)):
                        continue
                    rel = '%s nofollow' % rel
                else:
                    rel = 'nofollow'
                el.set('rel', rel)


    def allow_follow(self, anchor):
        """
        Переопределите этот метод, чтобы подавить добавление rel="nofollow" в некоторых анкорах.
        """
        return False

    def allow_element(self, el):
        """
        Определяет, можно ли принять или отклонить элемент в соответствии с настройками.
        """
        if el.tag not in self._tag_link_attrs:
            return False
        attr = self._tag_link_attrs[el.tag]
        if isinstance(attr, (list, tuple)):
            for one_attr in attr:
                url = el.get(one_attr)
                if not url:
                    return False
                if not self.allow_embedded_url(el, url):
                    return False
            return True
        else:
            url = el.get(attr)
            if not url:
                return False
            return self.allow_embedded_url(el, url)

    def allow_embedded_url(self, el, url):
        """
        Определяет, можно ли принять или отклонить URL, найденный в атрибутах элемента или его тексте.
        """
        if self.whitelist_tags is not None and el.tag not in self.whitelist_tags:
            return False
        parts = urlsplit(url)
        if parts.scheme not in ('http', 'https'):
            return False
        if parts.hostname in self.host_whitelist:
            return True
        return False

    def kill_conditional_comments(self, doc):
        """
        Условные комментарии IE фактически внедряют HTML, который парсер
        обычно не видит. Мы не можем позволить этому случиться, поэтому
        мы удалим любые комментарии, которые могут быть условными.
        """
        has_conditional_comment = _conditional_comment_re.search
        self._kill_elements(
            doc, lambda el: has_conditional_comment(el.text),
            etree.Comment)

    def _kill_elements(self, doc, condition, iterate=None):
        bad = []
        for el in doc.iter(iterate):
            if condition(el):
                bad.append(el)
        for el in bad:
            el.drop_tree()

    def _remove_javascript_link(self, link):
        # links like "j a v a s c r i p t:" might be interpreted in IE
        new = _substitute_whitespace('', unquote_plus(link))
        if _has_javascript_scheme(new):
            # FIXME: should this be None to delete?
            return ''
        return link

    _substitute_comments = re.compile(r'/\*.*?\*/', re.S).sub

    def _has_sneaky_javascript(self, style):
        """
        В зависимости от браузера, конструкции вроде ``e x p r e s s i o n(...)``
        могут быть интерпретированы как выражения, или ``expre/* stuff */ssion(...)``. Это
        проверяет попытки использовать такие хитрости.

        Обычно ответом будет удаление всего стиля; если в стиле есть немного
        JavaScript, другое правило удалит только JavaScript из стиля; это правило 
        ловит более скрытые попытки.
        """
        style = self._substitute_comments('', style)
        style = style.replace('\\', '')
        style = _substitute_whitespace('', style)
        style = style.lower()
        if _has_javascript_scheme(style):
            return True
        if 'expression(' in style:
            return True
        if '@import' in style:
            return True
        if '</noscript' in style:
            # e.g. '<noscript><style><a title="</noscript><img src=x onerror=alert(1)>">'
            return True
        if _looks_like_tag_content(style):
            # e.g. '<math><style><img src=x onerror=alert(1)></style></math>'
            return True
        return False

    def clean_html(self, html):
        """
        Очищает HTML от нежелательных элементов и возвращает результат.
        """
        result_type = type(html)
        # добавим div тег на случай если передали просто текст, чтобы всегда удалять потом рут тег
        if isinstance(html, str):
            html = f'<div id="eva-tag">{html}</div>'
        if isinstance(html, (str, bytes)):
            doc = fromstring(html)
        else:
            doc = copy.deepcopy(html)
        doc = self._ensure_root_element(doc)
        xhtml_to_html(doc)
        if not self.comments:
            self.kill_conditional_comments(doc)

        kill_tags, remove_tags, allow_tags = self._initialize_tags()
        if allow_tags:
            self._handle_tags(allow_tags, doc)
        if self.safe_attrs_only:
            bad_attrs = self._remove_unsafe_attributes(doc)

        if self.javascript:
            self._handle_javascript(doc)
        
        self._handle_kill_and_remove_tags(doc, kill_tags, remove_tags)

        if self.add_nofollow:
            self._add_nofollow_to_links(doc)

        result = _transform_result(result_type, doc)
        # Обрежем наш тег в результате
        return result[18:-6]

clean = Cleaner()
clean_html = clean.clean_html

############################################################
## Автоматическая генерация ссылок (Autolinking)
############################################################

_link_regexes = [
    re.compile(r'(?P<body>https?://(?P<host>[a-z0-9._-]+)(?:/[/\-_.,a-z0-9%&?;=~]*)?(?:\([/\-_.,a-z0-9%&?;=~]*\))?)', re.I),
    re.compile(r'mailto:(?P<body>[a-z0-9._-]+@(?P<host>[a-z0-9_.-]+[a-z]))', re.I),
    ]

_avoid_elements = ['textarea', 'pre', 'code', 'head', 'select', 'a']

_avoid_hosts = [
    re.compile(r'^localhost', re.I),
    re.compile(r'\bexample\.(?:com|org|net)$', re.I),
    re.compile(r'^127\.0\.0\.1$'),
    ]

_avoid_classes = ['nolink']

def autolink(el, link_regexes=_link_regexes,
             avoid_elements=_avoid_elements,
             avoid_hosts=_avoid_hosts,
             avoid_classes=_avoid_classes):
    """
    Превращает любые найденные URL в ссылки.

    Поиск ссылок осуществляется с помощью регулярных выражений (по умолчанию для mailto и http(s)).

    Ссылки не будут добавлены в элементы, указанные в avoid_elements, или в элементы с классами из avoid_classes.
    Также ссылки не будут добавлены для хостов, которые соответствуют регулярным выражениям в avoid_hosts
    (по умолчанию localhost и 127.0.0.1).

    Если передается элемент, то его tail не будет заменен, только содержимое элемента.
    """
    if el.tag in avoid_elements:
        return
    class_name = el.get('class')
    if class_name:
        class_name = class_name.split()
        for match_class in avoid_classes:
            if match_class in class_name:
                return
    for child in list(el):
        autolink(child, link_regexes=link_regexes,
                 avoid_elements=avoid_elements,
                 avoid_hosts=avoid_hosts,
                 avoid_classes=avoid_classes)
        if child.tail:
            text, tail_children = _link_text(
                child.tail, link_regexes, avoid_hosts, factory=el.makeelement)
            if tail_children:
                child.tail = text
                index = el.index(child)
                el[index+1:index+1] = tail_children
    if el.text:
        text, pre_children = _link_text(
            el.text, link_regexes, avoid_hosts, factory=el.makeelement)
        if pre_children:
            el.text = text
            el[:0] = pre_children

def _link_text(text, link_regexes, avoid_hosts, factory):
    """
    Находит текстовые ссылки и преобразует их в элементы <a>.
    """
    leading_text = ''
    links = []
    last_pos = 0
    while 1:
        best_match, best_pos = None, None
        for regex in link_regexes:
            regex_pos = last_pos
            while 1:
                match = regex.search(text, pos=regex_pos)
                if match is None:
                    break
                host = match.group('host')
                for host_regex in avoid_hosts:
                    if host_regex.search(host):
                        regex_pos = match.end()
                        break
                else:
                    break
            if match is None:
                continue
            if best_pos is None or match.start() < best_pos:
                best_match = match
                best_pos = match.start()
        if best_match is None:
            # No more matches
            if links:
                assert not links[-1].tail
                links[-1].tail = text
            else:
                assert not leading_text
                leading_text = text
            break
        link = best_match.group(0)
        end = best_match.end()
        if link.endswith('.') or link.endswith(','):
            # These punctuation marks shouldn't end a link
            end -= 1
            link = link[:-1]
        prev_text = text[:best_match.start()]
        if links:
            assert not links[-1].tail
            links[-1].tail = prev_text
        else:
            assert not leading_text
            leading_text = prev_text
        anchor = factory('a')
        anchor.set('href', link)
        body = best_match.group('body')
        if not body:
            body = link
        if body.endswith('.') or body.endswith(','):
            body = body[:-1]
        anchor.text = body
        links.append(anchor)
        text = text[end:]
    return leading_text, links

def autolink_html(html, *args, **kw):
    """
    Применяет функцию autolink к HTML-коду.
    """
    result_type = type(html)
    if isinstance(html, (str, bytes)):
        doc = fromstring(html)
    else:
        doc = copy.deepcopy(html)
    autolink(doc, *args, **kw)
    return _transform_result(result_type, doc)

autolink_html.__doc__ = autolink.__doc__

############################################################
## Перенос слов (Word wrapping)
############################################################

_avoid_word_break_elements = ['pre', 'textarea', 'code']
_avoid_word_break_classes = ['nobreak']

def word_break(el, max_width=40,
               avoid_elements=_avoid_word_break_elements,
               avoid_classes=_avoid_word_break_classes,
               break_character=chr(0x200b)):
    """
    Переносит длинные слова, найденные в тексте (не в атрибутах).

    Не затрагивает теги в avoid_elements, по умолчанию
    ``<textarea>`` и ``<pre>``.

    Перенос слов осуществляется вставкой символа Zero Width Space (&#8203;),
    который обычно не занимает места при отображении, но копируется как пробел, 
    а в моноширинных шрифтах может занимать место.

    См. http://www.cs.tut.fi/~jkorpela/html/nobr.html для обсуждения
    """
    if el.tag in _avoid_word_break_elements:
        return
    class_name = el.get('class')
    if class_name:
        dont_break = False
        class_name = class_name.split()
        for avoid in avoid_classes:
            if avoid in class_name:
                dont_break = True
                break
        if dont_break:
            return
    if el.text:
        el.text = _break_text(el.text, max_width, break_character)
    for child in el:
        word_break(child, max_width=max_width,
                   avoid_elements=avoid_elements,
                   avoid_classes=avoid_classes,
                   break_character=break_character)
        if child.tail:
            child.tail = _break_text(child.tail, max_width, break_character)

def word_break_html(html, *args, **kw):
    """
    Применяет функцию word_break к HTML-коду.
    """
    result_type = type(html)
    doc = fromstring(html)
    word_break(doc, *args, **kw)
    return _transform_result(result_type, doc)

def _break_text(text, max_width, break_character):
    """
    Переносит текст на части, если длина слова превышает max_width.
    """
    words = text.split()
    for word in words:
        if len(word) > max_width:
            replacement = _insert_break(word, max_width, break_character)
            text = text.replace(word, replacement)
    return text

_break_prefer_re = re.compile(r'[^a-z]', re.I)

def _insert_break(word, width, break_character):
    """
    Вставляет символ разрыва в длинное слово.
    """
    orig_word = word
    result = ''
    while len(word) > width:
        start = word[:width]
        breaks = list(_break_prefer_re.finditer(start))
        if breaks:
            last_break = breaks[-1]
            # Only walk back up to 10 characters to find a nice break:
            if last_break.end() > width-10:
                # FIXME: should the break character be at the end of the
                # chunk, or the beginning of the next chunk?
                start = word[:last_break.end()]
        result += start + break_character
        word = word[len(start):]
    result += word
    return result
