# -*- coding: utf-8 -*- # Copyright 2014-2016 Mir Calculate. http://www.calculate-linux.org # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .output import BaseOutput from .palette import (TextState, BaseColorMapping, ConsoleCodesInfo, LightColorMapping, ConsoleColor256, XmlFormat) from ..tools import SaveableIterator, ignore from html.parser import HTMLParser # from HTMLParser import HTMLParser import re class BaseConverter(): """ Базовый класс обработки (ничего не конвертирует - возвращает как есть) """ def __init__(self, output=BaseOutput()): self.output = output def transform(self, s): return self.output.outputText(s) def detect(self, s): return True class ConsoleCodesConverter(BaseConverter): """Преобразователь текста из цветного консольного вывода через объект форматирования. Объект форматирования должен реализовывать BaseOutput >>> cct = ConsoleCodesConverter(BaseOutput()) >>> outtext = "\033[32;1mHello\033[0;39m" >>> cct.transform(outtext) 'Hello' >>> from output import SpanCssOutput >>> cct = ConsoleCodesConverter(SpanCssOutput()) >>> outtext = "\033[32;1mHello\033[0;39m" >>> cct.transform(outtext) 'Hello' """ class CodeElement(): """Элемент кода в ESC последовательности""" def __init__(self, condition=lambda code: False, action=None): self.action = action self.condition = condition def tryParse(self, code): """Обрабатывает ли экземпляр код""" return self.condition(code) def parse(self, code, codes): """Обработать код, вызвать действие""" if callable(self.action): return self.action() return None def _next_code(self, other): """ Получить следующий код """ try: return int(next(other)) except StopIteration: return None class ColorElement(): """Элемент кода для указания стандартного цвета Проверка кода в интервале, запуск действия с передачей цвета """ # соответствие консольных цветов внутренним цветам mapColors = BaseColorMapping.mapConsole_TS def __init__(self, action=None, begin=None, end=None): self.action = action self.begin = begin self.end = end def tryParse(self, code): try: code = ord(code) except Exception as e: return False return self.begin <= code <= self.end def parse(self, code, codes): if callable(self.action): return self.action(self.mapColors.get(code - self.begin, TextState.Colors.DEFAULT)) else: return None def __init__(self, output=BaseOutput(), escSymb="\033"): super().__init__(output) self.escSymb = escSymb self.escBlock = (r"{esc}(?:\[(\d*(?:;\d+)*)(m)|" "\]\d+;.*?\x07|" "\([B0UK]|" "\[\d*[A-D])".format(esc=escSymb)) self.otherSymb = "(?:\r*\n|\t)" self.reEscBlock = re.compile(self.escBlock) self.reParse = re.compile( "(?:{0}|({1}))?(.*?)(?=$|{0}|{1})".format(self.escBlock, self.otherSymb), re.DOTALL) resetBoldHalfbright = lambda: ( (self.output.resetBold() or "") + (self.output.resetHalfbright() or "")) cci = ConsoleCodesInfo element = self.CodeElement # набор правил обработки кодов reset = element(lambda code: code == cci.RESET, self.output.reset) bold = element(lambda code: code == cci.BOLD, self.output.setBold) halfbright = element(lambda code: code == cci.HALFBRIGHT, self.output.setHalfbright) underline = element(lambda code: code == cci.UNDERLINE, self.output.setUnderline) nounderline = element(lambda code: code == cci.NOUNDERLINE, self.output.resetUnderline) invert = element(lambda code: code == cci.INVERT, self.output.setInvert) noinvert = element(lambda code: code == cci.NOINVERT, self.output.resetInvert) normal = element(lambda code: code == cci.NORMAL, resetBoldHalfbright) reset_foreground = element(lambda code: code == cci.FOREGROUND_DEFAULT, self.output.resetForeground) reset_background = element(lambda code: code == cci.BACKGROUND_DEFAULT, self.output.resetBackground) foreground = self.ColorElement(begin=cci.FOREGROUND, end=cci.FOREGROUND_END, action=self.output.setForeground) background = self.ColorElement(begin=cci.BACKGROUND, end=cci.BACKGROUND_END, action=self.output.setBackground) newline = element( lambda code: type(code) != int and ("\r" in code or "\n" in code), self.output.newLine) tab = element(lambda code: type(code) != int and "\t" in code, self.output.tab) self.grams = [reset, bold, halfbright, underline, nounderline, normal, invert, noinvert, reset_foreground, reset_background, foreground, background, tab, newline] def evaluteGram(self, code, codes=None): """Выполнить грамматику""" if codes is None: codes = SaveableIterator([]) for gram in (x for x in self.grams if x.tryParse(code)): return gram.parse(code, codes) def transform(self, s): """ Запустить преобразование текста """ def generator(): for ctrl, m, other, txt, _s, _m in self.reParse.findall(s): if m: codes = SaveableIterator(ctrl.split(';')) for code in codes: code = int(code or '0') res = self.evaluteGram(code, codes) if res: yield res elif other: res = self.evaluteGram(other) if res: yield res if txt: yield self.output.outputText(txt) yield self.output.endText() return "".join((x for x in generator() if x)) def detect(self, s): """ Определить есть ли в тексте управляющие последовательности """ return bool(self.reEscBlock.search(s)) class ConsoleCodes256Converter(ConsoleCodesConverter): """Расширяет возможность обработки 256 цветного терминала""" class Color256Element(ConsoleCodesConverter.CodeElement): def __init__(self, action=None, begin=None): super().__init__(condition=lambda code: code == begin, action=action) def parse(self, code, codes): """ Тон: 38;5;0-255 Фон: 48;5;0-255 """ colorMap = LightColorMapping(BaseColorMapping).mapConsole_TS codes.save() if self._next_code(codes) == ConsoleCodesInfo.COLOR256: code = self._next_code(codes) if code is not None: if code in colorMap: var = colorMap[code] else: var = ConsoleColor256.consoleToRgb(code) if callable(self.action): self.action(var) else: # если после 38 не 5 - не обрабатываем этот код codes.restore() def __init__(self, *args, **kwargs): ConsoleCodesConverter.__init__(self, *args, **kwargs) cci = ConsoleCodesInfo # обработчики кодов для вывода в 256 foreground256 = self.Color256Element(begin=cci.FOREGROUND256, action=self.output.setForeground) background256 = self.Color256Element(begin=cci.BACKGROUND256, action=self.output.setBackground) self.grams.insert(0, foreground256) self.grams.insert(0, background256) class XmlConverter(BaseConverter): """ Преобразователь текста из внутреннего xml формата """ unescaper = XmlFormat.unescaper def __init__(self, output=BaseOutput()): super().__init__(output) Tags = XmlFormat.Tags FontAttr = XmlFormat.FontAttributes self.tagMap = { Tags.BOLD: self.output.setBold, Tags.HALFBRIGHT: self.output.setHalfbright, Tags.INVERT: self.output.setInvert, Tags.UNDERLINE: self.output.setUnderline, Tags.FONT: self.parseFont } self.singletagMap = { Tags.NEWLINE: self.output.newLine, Tags.TAB: self.output.tab } self.colorMap = {FontAttr.FOREGROUND.lower(): self.output.setForeground, FontAttr.BACKGROUND.lower(): self.output.setBackground} self.reMatch = re.compile("<(?:%s)" % "|".join(self.tagMap.keys()), re.I) self.parser = self.createParser() def createParser(self): """ Создать парсер HTML кода """ parser = HTMLParser() parser.handle_starttag = self.startElementHandler parser.handle_endtag = self.endElementHandler parser.handle_data = self.characterDataHandler parser.handle_startendtag = self.startendElementHandler parser.handle_entityref = self.entityrefElementHandler return parser def parseFont(self, *attrs): for k, v in attrs: k = str(k).lower() if k in self.colorMap: self.colorMap[k](str(v)) def transform(self, s): self.__outdata = [] self.__tagStack = [] self.parser.feed(s) self.__outdata.append(self.output.endText()) return "".join((x for x in self.__outdata if x)) def addResultToOutdata(f): """Добавить возвращаемый результат в список self.__outdata""" def wrapper(self, *args): res = f(self, *args) if res: self.__outdata.append(res) return res return wrapper def _buildTaq(self, name, attrs=(), startendTag=False, endTag=False): """ Создать тэг по параметрам """ lslash, rslash = '', '' if startendTag: rslash = '/' elif endTag: lslash = '/' if attrs: return "<{name} {attrs}{rslash}>".format( name=name, attrs=" ".join(['%s="%s"' % (k, v) for k, v in attrs]), rslash=rslash) else: return "<{lslash}{name}{rslash}>".format(lslash=lslash, name=name, rslash=rslash) @addResultToOutdata def startElementHandler(self, name, attrs): """Обработчик начального тега""" if name in self.tagMap: self.output.pushState() self.__tagStack.append(name) with ignore(TypeError): return self.tagMap[name](*attrs) else: return self.output.outputText(self._buildTaq(name, attrs)) @addResultToOutdata def startendElementHandler(self, name, attrs): """Обработчик одиночного тега""" if name in self.singletagMap: with ignore(TypeError): return self.singletagMap[name](*attrs) else: return self.output.outputText( self._buildTaq(name, attrs, startendTag=True)) @addResultToOutdata def endElementHandler(self, name): """Обработчик завершающего тега""" if name in self.tagMap: if name in self.__tagStack: while self.__tagStack and self.__tagStack.pop() != name: self.output.popState() self.output.popState() else: return self.output.outputText(self._buildTaq(name, endTag=True)) @addResultToOutdata def characterDataHandler(self, data): """Обработчик текста в тэгах""" return self.output.outputText(self.unescaper(data)) @addResultToOutdata def entityrefElementHandler(self, data): return self.output.outputText(self.unescaper("&%s;" % data)) def detect(self, s): return bool(self.reMatch.search(s)) addResultToOutdata = staticmethod(addResultToOutdata)