Раскраска текстов в html и React

    Добавить разметку в текст руками легко. Можно разметить текст прямо здесь, на Хабре, а потом скопировать на сайт. Можно сделать поиск с заменой в Notepad++ или в Atom.

    Если это 1 текст. Если текстов много, хочется иметь инструмент для выделения фрагментов текста html-тегами или формирование исходного кода для React. На Питоне это не сложно (несколько строк кода на цвет).



    Если вы знаете Питон и регулярные выражения, переходите по ссылке.

    Там и примеры, и исходные коды. Под катом подробное описание.

    Разметка текста на примере раскраски исходного кода на Javascript


    Рассмотрим функцию:

    def jsToHtml(s):

    На входе исходный текст, возвращает html.

    Задаем переменные, определяющие атрибуты блоков. В примере для наглядности используются стили, в дальнейшем логичней заменить их на классы.

    comm = 'style="color: red;"' # цвет для комментариев
    blue = 'style="color: blue;"' # синий
    ...

    Разметка.

    Первое, что нужно сделать — это заэкранировать символы '&', '<', '>'

    s = s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')

    '&' экранируется, чтобы правильно отображать буквосочетания '&lt;', '&gt;' и прочие '&...;',

    Символы '<' и '>' экранируются, чтобы не конфликтовать с тэгами.

    Можно много чего заэкранировать, но, на мой взгляд, в utf-8 этого достаточно.

    Алгоритм разметки:

    • Берем re-шаблон и ищем все фрагменты текста, которые ему удовлетворяют.
    • Вырезаем каждый фрагмент, добавляем к нему разметку и сохраняем размеченный фрагмент в массиве (заодно сохраняем оригинальный текст: пригодится).
    • На его место вставляем заглушку с номером.
    • И так для каждого цвета.
    • Когда все раскрашено, заменяем заглушки на раскрашенные фрагменты из массива.

    Заглушка должна быть уникальной, но у нас же в тексте нет ни одного символа '<' и ни одного '>'.

    Делаем заглушку:

    f'<{i}>'

    где i – номер заглушки. Такого в тексте точно нет.

    Что делать, если внутри фрагмента уже есть заглушка.

    Например, в исходном тексте были строки:

    ` пример вложенности: строка  /* а в ней комментарий */ `
    /* комментарий, ` а в нем строка ` */

    Вариантов 2:

    1. Проигнорировать. В этом случае строка будет иметь двойной окрас.
    2. Найти вложенные заглушки и заменить их на исходный (неразмеченный) текст.

    Делаем функцию, которая все это реализует (10 строк кода, 10 строк комментария):

    def oneRe(reStr, s, attr, ls, multiColor=False):
        '''
        Ищет в исходной строке s нужные блоки, добавляет в них разметку и сохраняет результат в массиве ls,
        на их место вставляет заглушки с номерами (<0>, <1> ...<1528>...).
        Возвращает результат в виде строки с заглушками.
        
        reStr - re
        s - строка
        attr - атрибуты style/class/etc
        ls - массив с разметкой
        multiColor=False, если надо убрать вложенную разметку
        '''
        for block in set(re.findall(reStr, s)):
            toArr = block
            if not multiColor: # если multiColor==False, убрать вложенную разметку
                for prev in set(re.findall(r'<[\d]+>', block)): # ищем вложенность: <0> ... <21>
                    iPrev = int(prev[1:-1], 10)                # выделяем номер строки в массиве
                    toArr = toArr.replace(prev, ls[iPrev][1])  # в строке '<0> qwe <21>' заменяем ссылки(<0>,<21>) на первонач. значения
            ls.append([f'<span {attr}>{toArr}</span>', toArr]) # в каждом элеменете массива ls 2 элемента: размеченный текст и оригинал 
            s = s.replace(block, f'<{len(ls)-1}>')
        return s

    Это демонстрационный пример: разметка регулярных выражений может быть некорректной, экранированные символы (\', \") не обрабатываются.

    Для справки: выражение:

    s = s.replace(/A + B/g, 'A - B');

    в редакторах Notepad++ и в Atom раскрашено по разному.

    Теперь, когда есть oneRe, раскраска фрагментов делается просто. Пример, раскрашивающий строки в апострофах и кавычках:

    s = oneRe(r"'[\s\S]*?'", s, green, ls, multiColor=False) # убираем из кода 'строки' в массив ls
    s = oneRe(r'"[\s\S]*?"', s, green, ls, multiColor=False) # убираем из кода "строки" в массив ls
    

    Пример боле сложной раскраски. Нужно в js раскрасить выражения в многостроках
    `многострочная строка с выражениями ${A + B} и ${A - B}`


    for mStr in set(re.findall(r'`[\s\S]+?`', s)): # раскраска переменных внутри многострок
        newFstr = mStr
        for val in set(re.findall(r"\$\{[\s\S]+?\}", mStr)):
            ls.append([f'<span {darkRed}>{val}</span>', val])
            newFstr = newFstr.replace(val, f'<{i}>')
            i += 1
        s = s.replace(mStr, newFstr)
        
    s = oneRe(r'`[\s\S]+?`', s, green, ls, multiColor=True) # убираем из кода `многостроки` в массив ls
    

    сначала находим многосторки:

    re.findall(r'`[\s\S]+?`', s) возвращает список блоков текста между символами обратных кавычек.

    Каждый блок содержит либо пробелы, либо не пробелы ([\s\S] т.е. что угодно).

    Длина блока 1 или более (+).

    Без жадности ("?" означает, что символа "`" внутри блока нет).

    Копируем найденный блок(переменная mStr) в переменную newFstr.
    Находим в блоке подблоки с выражениями ${...}.

    re.findall(r"\$\{[\s\S]*?\}", mStr) возвращает список таких подблоков. Сохраняем разметку в массиве ls и заменяем подблок на заглушку в переменной newFstr.
    Когда подблоки кончатся, заменяем в исходной строке s первоначальное значение блока на новое.

    Set не лишний. Если findall вернет несколько одинаковых блоков, при обработке первого блока в исходном тексте заменятся на заглушки сразу все одинаковые. При обработке второго такого же блока его уже не будет в исходном тексте. Set убирает дублирование.

    Файл jsToHtml.py
    # -*- coding: utf-8 -*- 
    '''
    AON 2020
    
    '''
    
    import re
    
    # *** *** ***
    
    def oneRe(reStr, s, attr, ls, multiColor=False):
        '''
        Ищет в исходной строке s нужные блоки, добавляет в них разметку и сохраняет результат в массиве ls,
        на их место вставляет заглушки с номерами (<0>, <1> ...<1528>...).
        Возвращает результат в виде строки с заглушками.
        
        reStr - re
        s - строка
        attr - атрибуты style/class/etc
        ls - массив с разметкой
        multiColor=False, если надо убрать вложенную разметку
        '''
        i = len(ls) # номер очередной заглушки
        for block in set(re.findall(reStr, s)):
            toArr = block
            if not multiColor: # если multiColor==False, убрать вложенную разметку
                for prev in set(re.findall(r'<[\d]+>', block)): # ищем вложенность: <0> ... <21>
                    iPrev = int(prev[1:-1], 10)                 # выделяем номер строки в массиве
                    toArr = toArr.replace(prev, ls[iPrev][1])   # в строке '<0> qwe <21>' заменяем ссылки(<0>,<21>) на первонач. значения
            ls.append([f'<span {attr}>{toArr}</span>', toArr])  # в каждом элеменете массива ls 2 элемента: размеченный текст и оригинал 
            s = s.replace(block, f'<{i}>')              # заменяем блок текста на зауглушку с номером
            i += 1
        return s
    
    # *** *** ***
    
    def operColor(s, ls, color):
        '''
        раскрашивает операторы.
        Должна вызываться последней, когда в тексте не осталось ни строк, ни комментариев
        '''
        i = len(ls)
        for c in ['&lt;=', '&gt;=', '=&lt;', '=&gt;', '&lt;', '&gt;', '&amp;&amp;', '&amp;',
                  '===', '!==', '==', '!=', '+=', '-=', '++', '--', '||']:
            ls.append([f'<span {color}>{c}</span>',0])
            s = s.replace(c, f'<{i}>')
            i += 1
        for c in '!|=+-?:,.[](){}%*/':
            ls.append([f'<span {color}>{c}</span>',0])
            s = s.replace(c, f'<{i}>')
            i += 1
        return s
    
    # *** *** ***
    
    def jsToHtml(s):
        '''
        Демонстрационный раскрасчик кода.
        Заменяет строки, комментарии и ключевые слова на элементы разметки <span>.
        '''
    
        black = '''style="font-family: 'Courier New', monospace;
            background: #fff; 
            color: black;
            font-weight: bold;
            border: 1px solid #ddd;
            padding: 5px;
            text-align: left;
            white-space: pre;"'''
        comm = 'style="color: red;"'
        green = 'style="color: green; font-style: italic;"'
        blue = 'style="color: blue;"'
        red2 = 'style="color: #840;"'
    
        s = s.replace('&', '&amp;').replace('<', &'&lt;').replace('>', '&gt;')   # экранируем символы '&', '<', '>'
    
        ls = []
    
        i = 0
        for mStr in set(re.findall(r'`[\s\S]+?`', s)): # раскраска переменных внутри многострок
            newFstr = mStr
            for val in set(re.findall(r"\$\{[\s\S]+?\}", mStr)):
                ls.append([f'<span {darkRed}>{val}</span>', val])
                newFstr =newFstr.replace(val, f'<{i}>')
                i += 1
            s = s.replace(mStr, newFstr)
            
        s = oneRe(r'`[\s\S]+?`', s, green, ls, multiColor=True) # убираем из кода `многостроки` в массив ls
        s = oneRe(r"'[\s\S]*?'", s, green, ls, multiColor=False) # убираем из кода 'строки' в массив ls
        s = oneRe(r'"[\s\S]*?"', s, green, ls, multiColor=False) # убираем из кода "строки" в массив ls
        s = oneRe(r'/[\s\S].*?/g\b', s, green, ls, multiColor=False) # убираем из кода re-строки в массив ls
        s = oneRe(r'/[\s\S].*?/\.', s, green, ls, multiColor=False) # убираем из кода re-строки в массив ls
        s = oneRe(r'/\*[\s\S]+?\*/', s, comm, ls, multiColor=False) # убираем из кода /* комментарии */ в массив ls (лучший цвет - красный)
        s = oneRe(r'//[\s\S]*?\n', s, comm, ls, multiColor=False) # убираем из кода // комментарии в массив ls (лучший цвет - красный)
    
        i = len(ls)
    
        # теперь можно раскрасить синтаксис
        for c in ['new', 'JSON', 'Promise', 'then', 'catch', 'let', 'const', 'var', 'true', 'false', 'class', 'from', 'import', 'set', 'list', 'for', 'in', 'if', 'else', 'return', 'null']:
            ls.append([f'<span {blue}>{c}</span>',0])
            s = re.sub (r'\b%s\b' % c, f'<{i}>', s)
            i += 1
    
        # теперь можно раскрасить мои переменные
        for c in ['window', 'doc', 'cmd', 'init','init2', 'recalc', 'hide', 'readOnly', 'validate']:
            ls.append([f'<span {darkRed}>{c}</span>',0])
            s = re.sub (r'\b%s\b' % c, f'<{i}>', s)
            i += 1
    
        s = operColor(s, ls, darkBlue) # теперь можно раскрасить операторы
    
        for j in range(len(ls), 0, -1):  # восстанавливаем операторы, переменные, комментарии, строки с новым окрасом
            s = s.replace(f'<{j-1}>', ls[j-1][0])
    
        return f'<div {black}>{s}</div>'
    
    # *** *** ***
    


    Html в React


    Преобразовать html в исходный код React можно на сайте htmltoreact.com. Там же есть ссылка на GitHub.

    Меня это не устроило: во первых он формирует не совсем то, что мне надо, во вторых – как я это чудо затащу к себе на сервер.

    Написал свой.

    Устанавливаем библиотеку lxml (pip install lxml или pip3 install lxml).

    Импортируем:

    from xml.dom.minidom import parseString
    from lxml import html, etree

    Преобразуем html-текст в xhtml-текст. Это почти одно и то же, но все теги закрыты.

    doc = html.fromstring(htmlText)
    ht = etree.tostring(doc, encoding='utf-8').decode()

    Полученный xhtml парсим в дом-дерево с помощью минидома.

    dom = parseString(ht)

    Делаем функцию, которая рекурсивно скачет по нодам и формирует результат в виде исходного кода React.

    Dom-дерево после вызова parseString – это нода-папа, у которай есть ноды-дети, у которых есть еще дети и т.д.

    Каждая нода – это словарь, содержащий ее описание:

    • nodeName — имя ноды, строка
    • childNodes — ноды-дети, список
    • attributes- атрибуты, словарь
    • У ноды с названием #text есть nodeValue(строка)

    Пример:

    <div class="A" style="color: red;">Red of course,<br> Сэр</div>

    После преобразований получим:
    { 'nodeName':'div',
      'attributes': {'style': 'color: red;', 'class': 'A'},
      'childNodes': [
        {'nodeName':'#text', 'nodeValue': 'Red of course,'},
        {'nodeName':'br'},
        {'nodeName':'#text', 'nodeValue': 'Сэр'},
      ],
    }
    

    Преобразовать dom в строку несложно (есть pprint), при формировании React-кода, я заменил class на className и переделал атрибут style.

    В текстовых нодах заэкранированы '{', '}', '<', '>'.

    Файл htmlToReact.py
    # -*- coding: utf-8 -*- 
    # -*- coding: utf-8 -*- 
    
    from xml.dom.minidom import parseString
    from lxml import html, etree
    
    # *** *** ***
    
    _react = ''
    
    def htmlToReact(buf):
        '''
        buf - html-строка
        возвращает ReactJS-строку 
        '''
        global _react
        _react = ''
    
        try:
            r = re.search('<[\\s\\S]+>', buf)
            if r:
                doc = html.fromstring(r.group(0))
                ht = etree.tostring(doc, encoding='utf-8').decode()
                xHtmlToReact(parseString(ht).childNodes[0], '')
                return _react
            else:
                return '<empty/>'
        except Exception as ex:
            s = f'htmlToReact: \n{ex}'
            print(s)
            return s
    
    # *** *** ***
    
    def sU(a, c):
        '''
        xlink:show   ->  xlinkShow
        font-weight  ->  fontWeight
        '''
        l, _, r = a.partition(c)
        return ( (l + r[0].upper() + r[1:]) if r else a).strip()
    
    # *** *** ***
    
    def xHtmlToReact(n, shift='\n    '):
        '''
        Нужен для показа реакт-кода
        на входе нода, рекурсия по нодам с заменой минусов на upperCase
        результат формирует в global переменной _react
        '''
        global _react
    
        if n.nodeName.lower() in ['head', 'script']:
            return
        
        _react += shift + '<' + n.nodeName.lower()
        if n.attributes:
            for k, v in n.attributes.items():
                if k == 'style':
                    style = ''
                    for s in v.split(';'):
                        if s.strip():
                            l, _, r = s.partition(':')
                            style += f'''{sU(l, '-')}: "{r.strip()}", '''
                    if style:
                        _react += ' style={{' + style + '}}'
                elif k == 'class':
                    _react += f' className="{v}"'
                else:
                    kk = k.replace('xlink:href', 'href') # deprcated
                    _react += f''' {sU( sU(kk, ':'), '-' )}="{v}"'''
            
        _react += '>'
        if n.childNodes:
            for child in n.childNodes:
                if  child.nodeName == '#text':
                    tx = child.nodeValue
                    for x in ['{', '}', '<', '>']:
                        tx = tx.replace(x, '{"' + x + '"_{_')
                    tx = tx.replace('_{_', '}')
                    if tx[-1] == ' ':
                        tx = tx[:-1] + '\xa0'
                    _react += tx.replace('\n', '<br/>')
                else:
                    xHtmlToReact(child)
                    
        _react += f'{shift}</{n.nodeName.lower()}>'
    
    # *** *** ***
    
    


    Примеры и исходные тексты здесь.

    P.S. На Хабре раскраска питоновского кода (а может и других) не идеальна. Если в питоновской строке есть буквосочетание &amp;, оно отображается как &. В своих кодах я подправил, чтобы выглядело правильно. Если хабр ошибку исправит, мои тексты перекривятся.

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое