#-*- coding: utf-8 -*- # Copyright 2014 Calculate Ltd. 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. import re from xml.etree import ElementTree as ET from calculate.lib.utils.text import MultiReplace from palette import (TextState, BaseColorMapping, ConsoleCodeMapping, LightColorMapping, ConsoleColor256, ConsoleCodesInfo, SpanPalette, XmlFormat) class BaseOutput(object): """ Базовый вывод текста. Вывод просто текста без изменения шрифта """ def __init__(self, state=None): pass def setBold(self): """ Выводимый текст будет жирным """ pass def resetBold(self): """ Выводимый текст будет нежирным """ pass def setUnderline(self): """ Выводимый текст будет подчеркнутым """ pass def resetUnderline(self): """ Выводимый текст не будет подчеркнутым """ pass def setHalfbright(self): """ Цвет выводимого текста использует полутона """ pass def resetHalfbright(self): """ Цвет выводимого текста не использует полутона """ pass def reset(self): """ Использовать шрифт по умолчанию """ pass def endText(self): """ Обработка текста завершена """ return "" def outputText(self, text): """ Вывести текст с установленными настройками """ return text def setForeground(self, color): """ Установить цвет шрифта """ pass def setBackground(self, color): """ Установить цвет фона """ pass def resetForeground(self): """ Использовать цвет шрифта по умолчанию """ pass def resetBackground(self): """ Использовать цвет фона по умолчанию """ pass def setInvert(self): """ Включить инверсию """ def resetInvert(self): """ Выключить инверсию """ def pushState(self): """ Сохранить состояние текста """ def popState(self): """ Восстановить состояние текста из стека """ def newLine(self): """ Вывести текст на новой строке """ return "\n" def tab(self): """ Вывести символ табуляции """ return "\t" def clone(self): """ Создать копию объекта """ return self.__class__() class SaveAttrOutput(BaseOutput): """ Базовый класс с сохранением атрибутов """ def __init__(self, state=None): self.prev_state = state.clone() if state else TextState() self.current_state = self.prev_state.clone() self.state_stack = [] def clone(self): obj = self.__class__() obj.current_state = self.current_state.clone() obj.state_stack = list(self.state_stack) return obj def setBold(self): self.current_state.bold = True def resetBold(self): self.current_state.bold = False def setUnderline(self): self.current_state.underline = True def resetUnderline(self): self.current_state.underline = False def setHalfbright(self): self.current_state.halfbright = True def resetHalfbright(self): self.current_state.halfbright = False def reset(self): self.resetBold() self.resetHalfbright() self.resetUnderline() self.resetForeground() self.resetBackground() self.resetInvert() def setForeground(self, color): self.current_state.foreground = color def resetForeground(self): self.current_state.foreground = TextState.Colors.DEFAULT def setBackground(self, color): self.current_state.background = color def resetBackground(self): self.current_state.background = TextState.Colors.DEFAULT def resetInvert(self): self.current_state.invert = False def setInvert(self): self.current_state.invert = True def pushState(self): self.state_stack.append(self.current_state.clone()) def popState(self): self.current_state = self.state_stack.pop() class ColorTerminalOutput(SaveAttrOutput): """ Форматирует текст для вывода в консоль """ mapColors = ConsoleCodeMapping(ConsoleCodesInfo.FOREGROUND, BaseColorMapping).mapTS_Console mapLightColors = ConsoleCodeMapping(ConsoleCodesInfo.FOREGROUND - LightColorMapping.offset, LightColorMapping).mapTS_Console mapBackgroundColors = ConsoleCodeMapping(ConsoleCodesInfo.BACKGROUND, BaseColorMapping).mapTS_Console def setBold(self): self.resetHalfbright() super(ColorTerminalOutput, self).setBold() def setHalfbright(self): self.resetBold() super(ColorTerminalOutput, self).setHalfbright() def handleForeground(self, prevstate, curstate, attrs, tail_attrs): """Обработать изменение тона""" cci = ConsoleCodesInfo color = curstate.foreground if color in self.mapColors: attrs.append(self.mapColors[color]) elif color in self.mapLightColors: if prevstate.bold == curstate.bold and not curstate.bold: tail_attrs.append(cci.NORMAL) attrs.append(cci.BOLD) attrs.append(self.mapLightColors[color]) else: attrs.append(cci.FOREGROUND_DEFAULT) self.resetForeground() def handleBackground(self, prevstate, curstate, attrs, tail_attrs): """Обработать изменение фона""" color = curstate.background if color in self.mapBackgroundColors: attrs.append(self.mapBackgroundColors[color]) else: attrs.append(ConsoleCodesInfo.BACKGROUND_DEFAULT) def handleIntensity(self, prevstate, curstate, attrs, tail_attrs): """Обработать интенсивность""" if curstate.bold and curstate.bold != prevstate.bold: attrs.append(ConsoleCodesInfo.BOLD) elif (curstate.halfbright and prevstate.halfbright != curstate.halfbright): attrs.append(ConsoleCodesInfo.HALFBRIGHT) else: attrs.append(ConsoleCodesInfo.NORMAL) def handleUnderline(self, prevstate, curstate, attrs, tail_attrs): """Обработать подчеркивание""" if curstate.underline: attrs.append(ConsoleCodesInfo.UNDERLINE) else: attrs.append(ConsoleCodesInfo.NOUNDERLINE) def handleInvert(self, prevstate, curstate, attrs, tail_attrs): """Обработать инверсию""" if curstate.invert: attrs.append(ConsoleCodesInfo.INVERT) else: attrs.append(ConsoleCodesInfo.NOINVERT) def handleColor(self, prevstate, curstate, attrs, tail_attrs): """Обработать изменение цветов (фон и/или тон)""" if prevstate.foreground != curstate.foreground: self.handleForeground(prevstate, curstate, attrs, tail_attrs) if prevstate.background != curstate.background: self.handleBackground(prevstate, curstate, attrs, tail_attrs) def _createAttrs(self, prevstate, curstate): """ Создать ESC последовательность для установки параметров текста """ attrs, tail_attrs = [], [] # получить интенсивность (полутон и жирность относятся к интенсивности) intensity = lambda x: x & (TextState.Attributes.HALFBRIGHT | TextState.Attributes.BOLD) if (prevstate.attr != curstate.attr and curstate.attr == TextState.Attributes.NONE and curstate.foreground is TextState.Colors.DEFAULT and curstate.background is TextState.Colors.DEFAULT): attrs.append(ConsoleCodesInfo.RESET) else: if intensity(prevstate.attr) != intensity(curstate.attr): self.handleIntensity(prevstate, curstate, attrs, tail_attrs) if prevstate.underline != curstate.underline: self.handleUnderline(prevstate, curstate, attrs, tail_attrs) if prevstate.invert != curstate.invert: self.handleInvert(prevstate, curstate, attrs, tail_attrs) if (prevstate.foreground != curstate.foreground or prevstate.background != curstate.background): self.handleColor(prevstate, curstate, attrs, tail_attrs) return attrs, tail_attrs def _createEscCode(self, attrs): """ Создать ESC строку """ attrs = map(str, ['\033['] + attrs + ['m']) return "%s%s%s" % (attrs[0], ";".join(attrs[1:-1]), attrs[-1]) def outputText(self, s): """ Задание параметров текста и вывод его """ if self.prev_state != self.current_state: attr, tail_attrs = \ self._createAttrs(self.prev_state, self.current_state) attr = self._createEscCode(attr) if tail_attrs: postattr = self._createEscCode(tail_attrs) else: postattr = "" self.prev_state = self.current_state.clone() else: attr = "" postattr = "" return attr + s + postattr def endText(self): self.reset() return self.outputText("") def newLine(self): return "\n" def tab(self): return "\t" class ColorTerminal256Output(ColorTerminalOutput): """ Вывод на 256 цветный терминал """ mapLightColors = LightColorMapping.mapTS_Console def handleForeground(self, prevstate, curstate, attrs, tail_attrs): color = curstate.foreground color256 = ConsoleColor256.rgbToConsole(color) if not color256 and color in self.mapLightColors: color256 = self.mapLightColors[color] if color256: attrs.extend([ConsoleCodesInfo.FOREGROUND256, ConsoleCodesInfo.COLOR256, color256]) else: super(ColorTerminal256Output, self).handleForeground(prevstate, curstate, attrs, tail_attrs) def handleBackground(self, prevstate, curstate, attrs, tail_attrs): color = curstate.background color256 = ConsoleColor256.rgbToConsole(color) if not color256 and color in self.mapLightColors: color256 = self.mapLightColors[color] if color256: attrs.extend([ConsoleCodesInfo.BACKGROUND256, ConsoleCodesInfo.COLOR256, color256]) else: super(ColorTerminal256Output, self).handleBackground(prevstate, curstate, attrs, tail_attrs) class ColorTerminal16Output(ColorTerminalOutput): """ Вывод на 16 цветный терминал с преобразованием RGB к ближайшему базовому Bugs: После преобразования текст и фон могут одинакового цвета """ def __init__(self, state=None, palette=None): SaveAttrOutput.__init__(self, state=state) self.palette = palette def _handleNearestColors(self, color): """ Обработка преобразования к ближайшему цвету """ standardColors = TextState.normalColors + TextState.lightColors if self.palette and color not in standardColors: return self.palette.getBaseColorByRGB(color) return color def handleForeground(self, prevstate, curstate, attrs, tail_attrs): """ Добавить преобразование RGB к ближайшему базовому """ _curstate = curstate.clone() _curstate.foreground = \ self._handleNearestColors(curstate.foreground) super(ColorTerminal16Output, self).handleForeground( prevstate, _curstate, attrs, tail_attrs) def handleBackground(self, prevstate, curstate, attrs, tail_attrs): """ Добавить преобразование RGB к ближайшему базовому """ mapHighNormal = dict(zip(TextState.lightColors, TextState.normalColors)) _curstate = curstate.clone() _curstate.background = \ self._handleNearestColors(curstate.background) # преобразовать яркий цвет к обычному _curstate.background = \ mapHighNormal.get(_curstate.background, _curstate.background) super(ColorTerminal16Output, self).handleBackground( prevstate, _curstate, attrs, tail_attrs) class SpanCssOutput(SaveAttrOutput): """ Форматирует текст для вывода в консоль """ def __init__(self, state=None, palette=SpanPalette()): SaveAttrOutput.__init__(self, state=state) self.palette = palette def getStringColor(self, color, bold=False, halfbright=False, background=False): """ Получить название цвета по номеру и состоянию текста """ if halfbright: bright = SpanPalette.LOW_BRIGHT elif bold: bright = SpanPalette.HIGH_BRIGHT else: bright = SpanPalette.NORMAL_BRIGHT if background: return self.palette.getBackgroundColor(color) else: return self.palette.getTextColor(color, bright) def getTags(self, prevstate, curstate): """ Создать tag span для указания параметров текста """ style = [] colorAttr = ["color", "background"] if curstate.invert: colorAttr = colorAttr[1], colorAttr[0] if (prevstate.foreground != curstate.foreground or prevstate.bold != curstate.bold or curstate.invert or prevstate.halfbright != curstate.halfbright): sColor = self.getStringColor(curstate.foreground, curstate.bold, curstate.halfbright, background=False) style.append("%s:%s;" % (colorAttr[0], sColor)) if prevstate.background != curstate.background or curstate.invert: sColor = self.getStringColor(curstate.background, background=True) style.append("%s:%s;" % (colorAttr[1], sColor)) if prevstate.underline != curstate.underline: if curstate.underline: style.append("text-decoration:underline;") else: style.append("text-decoration:none;") if prevstate.bold != curstate.bold: if curstate.bold: style.append("font-weight:bold;") else: style.append("font-weight:normal;") return '' % "".join(style), '' def outputText(self, s): if self.prev_state != self.current_state: lattr, rattr = self.getTags(self.prev_state, self.current_state) else: lattr = rattr = "" return lattr + XmlFormat.escaper(s) + rattr def endText(self): self.reset() return "" def newLine(self): return "
" def tab(self): return " " class XmlOutput(SaveAttrOutput): """ Форматирует текст c описанием формата в XML для внутренней передачи Bugs: игнорирует первоначальное состояние (state) не экономное использование тэгов (например при выводе нескольких строк """ escaper = XmlFormat.escaper def __init__(self, state=None): super(XmlOutput, self).__init__(state=state) self.clear_state = TextState() def getXML(self, curstate, text): """ Создать управляющие тэги :type curstate: TextState :type text: str :type rtype: ET.Element """ Tags, FontAttributes = XmlFormat.Tags, XmlFormat.FontAttributes root = ET.Element("root") tail = root if (curstate.foreground != TextState.Colors.DEFAULT or curstate.background != TextState.Colors.DEFAULT): tail = ET.SubElement(tail, Tags.FONT) sColor = str(curstate.foreground or "") if sColor: tail.attrib[FontAttributes.FOREGROUND] = sColor sColor = str(curstate.background or "") if sColor: tail.attrib[FontAttributes.BACKGROUND] = sColor if curstate.halfbright: tail = ET.SubElement(tail, Tags.HALFBRIGHT) if curstate.invert: tail = ET.SubElement(tail, Tags.INVERT) if curstate.underline: tail = ET.SubElement(tail, Tags.UNDERLINE) if curstate.bold: tail = ET.SubElement(tail, Tags.BOLD) tail.text = text return root[0] def xmlToString(self, xml): """ Получить строку xml не преобразовывая &, <, > """ def generator(root): if root.attrib: yield "<%s %s>" % (root.tag, " ".join(['%s="%s"' % (k, v) for k, v in root.attrib.items()])) else: yield "<%s>" % root.tag if len(root): for element in root: for text in generator(element): yield text if root.text: yield self.escaper(root.text) yield "" % root.tag return "".join(filter(None, list(generator(xml)))) def outputText(self, s): if self.clear_state != self.current_state: return self.xmlToString(self.getXML(self.current_state, s)) else: return self.escaper(s) def endText(self): self.reset() return "" def newLine(self): return "
" def tab(self): return "" class BasePositionOutput(object): """ Объект составляющий ESC последовательности для управлением местом вывода """ def moveCursorUp(self, count=1): """ Переместить курсор вверх """ return "" def moveCursorDown(self, count=1): """ Переместить курсор вниз """ return "" def moveCursorRight(self, count=1): """ Переместить курсор вправо """ return "" def moveCursorLeft(self, count=1): """ Переместить курсор влево """ return "" def clearLine(self, whole_line=False): """ Очистить строку от курсора до конца или всю строку """ return "" def savePosition(self): """ Сохранить положение курсора """ return "" def restorePosition(self): """ Восстановить положение курсора """ return "" class TerminalPositionOutput(BasePositionOutput): """ Управление позицией вывода текста в терминале """ class Codes: UP = 'A' DOWN = 'B' RIGHT = 'C' LEFT = 'D' CLEAR_LINE = 'K' CLEAR_FROM_CURSOR = '1' CLEAR_WHOLE_LINE = '2' SAVE_POSITION = 's' RESTORE_POSITION = 'u' def _createEscCode(self, attrs): """ Создать ESC строку """ return '\033[%s' % attrs def _moveCursor(self, direct, count): if int(count) > 1: count = str(count) else: count = "" return self._createEscCode("%s%s" % (count, direct)) def moveCursorDown(self, count=1): return self._moveCursor(self.Codes.DOWN, count) def moveCursorUp(self, count=1): return self._moveCursor(self.Codes.UP, count) def moveCursorRight(self, count=1): return self._moveCursor(self.Codes.RIGHT, count) def moveCursorLeft(self, count=1): return self._moveCursor(self.Codes.LEFT, count) def clearLine(self, whole_line=False): if whole_line: mode_code = self.Codes.CLEAR_WHOLE_LINE else: mode_code = self.Codes.CLEAR_FROM_CURSOR return self._createEscCode("%s%s"%(mode_code, self.Codes.CLEAR_LINE)) def savePosition(self): return self._createEscCode(self.Codes.SAVE_POSITION) def restorePosition(self): return self._createEscCode(self.Codes.RESTORE_POSITION)