Deploy site
This commit is contained in:
@ -0,0 +1,53 @@
|
||||
from dataproperty import Align, Format
|
||||
|
||||
from ._cell import Cell
|
||||
from ._font import FontSize, FontStyle, FontWeight
|
||||
from ._style import DecorationLine, Style, ThousandSeparator, VerticalAlign
|
||||
from ._styler import (
|
||||
GFMarkdownStyler,
|
||||
HtmlStyler,
|
||||
LatexStyler,
|
||||
MarkdownStyler,
|
||||
NullStyler,
|
||||
ReStructuredTextStyler,
|
||||
TextStyler,
|
||||
get_align_char,
|
||||
)
|
||||
from ._styler_interface import StylerInterface
|
||||
from ._theme import (
|
||||
CheckStyleFilterKeywordArgsFunc,
|
||||
ColSeparatorStyleFilterFunc,
|
||||
StyleFilterFunc,
|
||||
Theme,
|
||||
fetch_theme,
|
||||
list_themes,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"Align",
|
||||
"Format",
|
||||
"Cell",
|
||||
"FontSize",
|
||||
"FontStyle",
|
||||
"FontWeight",
|
||||
"Style",
|
||||
"ThousandSeparator",
|
||||
"VerticalAlign",
|
||||
"DecorationLine",
|
||||
"GFMarkdownStyler",
|
||||
"HtmlStyler",
|
||||
"LatexStyler",
|
||||
"MarkdownStyler",
|
||||
"NullStyler",
|
||||
"ReStructuredTextStyler",
|
||||
"StylerInterface",
|
||||
"TextStyler",
|
||||
"CheckStyleFilterKeywordArgsFunc",
|
||||
"ColSeparatorStyleFilterFunc",
|
||||
"StyleFilterFunc",
|
||||
"Theme",
|
||||
"get_align_char",
|
||||
"fetch_theme",
|
||||
"list_themes",
|
||||
)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,30 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from ._style import Style
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Cell:
|
||||
"""
|
||||
A data class representing a cell in a table.
|
||||
"""
|
||||
|
||||
row: int
|
||||
"""row index. ``-1`` means that the table header row."""
|
||||
|
||||
col: int
|
||||
"""column index."""
|
||||
|
||||
value: Any
|
||||
"""data for the cell."""
|
||||
|
||||
default_style: Style
|
||||
"""default |Style| for the cell."""
|
||||
|
||||
def is_header_row(self) -> bool:
|
||||
"""
|
||||
Return |True| if the cell is a header.
|
||||
"""
|
||||
|
||||
return self.row < 0
|
@ -0,0 +1,23 @@
|
||||
from enum import Enum, unique
|
||||
|
||||
|
||||
@unique
|
||||
class FontSize(Enum):
|
||||
NONE = "none"
|
||||
TINY = "tiny"
|
||||
SMALL = "small"
|
||||
MEDIUM = "medium"
|
||||
LARGE = "large"
|
||||
|
||||
|
||||
@unique
|
||||
class FontStyle(Enum):
|
||||
NORMAL = "normal"
|
||||
ITALIC = "italic"
|
||||
TYPEWRITER = "typewriter"
|
||||
|
||||
|
||||
@unique
|
||||
class FontWeight(Enum):
|
||||
NORMAL = "normal"
|
||||
BOLD = "bold"
|
@ -0,0 +1,373 @@
|
||||
import warnings
|
||||
from enum import Enum, unique
|
||||
from typing import Any, Final, Optional, Union
|
||||
|
||||
from dataproperty import Align
|
||||
from tcolorpy import Color
|
||||
|
||||
from .._function import normalize_enum
|
||||
from ._font import FontSize, FontStyle, FontWeight
|
||||
|
||||
|
||||
@unique
|
||||
class DecorationLine(Enum):
|
||||
NONE = "none"
|
||||
LINE_THROUGH = "line_through"
|
||||
STRIKE = "strike"
|
||||
UNDERLINE = "underline"
|
||||
|
||||
|
||||
@unique
|
||||
class ThousandSeparator(Enum):
|
||||
NONE = "none" #: no thousands separator
|
||||
COMMA = "comma" #: ``','`` as thousands separator
|
||||
SPACE = "space" #: ``' '`` as thousands separator
|
||||
UNDERSCORE = "underscore" #: ``'_'`` as thousands separator
|
||||
|
||||
|
||||
@unique
|
||||
class VerticalAlign(Enum):
|
||||
BASELINE = (1 << 0, "baseline")
|
||||
TOP = (1 << 1, "top")
|
||||
MIDDLE = (1 << 2, "middle")
|
||||
BOTTOM = (1 << 3, "bottom")
|
||||
|
||||
@property
|
||||
def align_code(self) -> int:
|
||||
return self.__align_code
|
||||
|
||||
@property
|
||||
def align_str(self) -> str:
|
||||
return self.__align_string
|
||||
|
||||
def __init__(self, code: int, string: str) -> None:
|
||||
self.__align_code = code
|
||||
self.__align_string = string
|
||||
|
||||
|
||||
_s_to_ts: Final[dict[str, ThousandSeparator]] = {
|
||||
"": ThousandSeparator.NONE,
|
||||
",": ThousandSeparator.COMMA,
|
||||
" ": ThousandSeparator.SPACE,
|
||||
"_": ThousandSeparator.UNDERSCORE,
|
||||
}
|
||||
|
||||
|
||||
def _normalize_thousand_separator(value: Union[str, ThousandSeparator]) -> ThousandSeparator:
|
||||
if isinstance(value, ThousandSeparator):
|
||||
return value
|
||||
|
||||
thousand_separator = normalize_enum(
|
||||
value,
|
||||
ThousandSeparator,
|
||||
default=ThousandSeparator.NONE,
|
||||
validate=False,
|
||||
)
|
||||
if isinstance(thousand_separator, ThousandSeparator):
|
||||
return thousand_separator
|
||||
|
||||
norm_value = _s_to_ts.get(value)
|
||||
if norm_value is None:
|
||||
raise ValueError(f"unknown thousand separator: {value}")
|
||||
|
||||
return norm_value
|
||||
|
||||
|
||||
class Style:
|
||||
"""Style specifier class for table elements.
|
||||
|
||||
Args:
|
||||
color (Union[|str|, tcolorpy.Color, |None|]):
|
||||
Text color for cells.
|
||||
When using str, specify a color code (``"#XXXXXX"``) or a color name.
|
||||
|
||||
.. note::
|
||||
In the current version, only applicable for part of text format writer classes.
|
||||
|
||||
fg_color (Union[|str|, tcolorpy.Color, |None|]):
|
||||
Alias to :py:attr:`~.color`.
|
||||
|
||||
bg_color (Union[|str|, tcolorpy.Color, |None|]):
|
||||
Background color for cells.
|
||||
When using str, specify a color code (``"#XXXXXX"``) or a color name.
|
||||
|
||||
.. note::
|
||||
In the current version, only applicable for part of text format writer classes.
|
||||
|
||||
align (|str| / :py:class:`~.style.Align`):
|
||||
Horizontal text alignment for cells.
|
||||
This can be only applied for text format writer classes.
|
||||
Possible string values are:
|
||||
|
||||
- ``"auto"`` (default)
|
||||
- Detect data type for each column and set alignment that appropriate
|
||||
for the type automatically
|
||||
- ``"left"``
|
||||
- ``"right"``
|
||||
- ``"center"``
|
||||
|
||||
vertical_align (|str| / :py:class:`~.style.VerticalAlign`):
|
||||
Vertical text alignment for cells.
|
||||
This can be only applied for HtmlTableWriter class.
|
||||
Possible string values are:
|
||||
|
||||
- ``"baseline"`` (default)
|
||||
- ``"top"``
|
||||
- ``"middle"``
|
||||
- ``"bottom"``
|
||||
|
||||
font_size (|str| / :py:class:`~.style.FontSize`):
|
||||
Font size specification for cells in a column.
|
||||
This can be only applied for HTML/Latex writer classes.
|
||||
Possible string values are:
|
||||
|
||||
- ``"tiny"``
|
||||
- ``"small"``
|
||||
- ``"medium"``
|
||||
- ``"large"``
|
||||
- ``"none"`` (default: no font size specification)
|
||||
|
||||
font_weight (|str| / :py:class:`~.style.FontWeight`):
|
||||
Font weight specification for cells in a column.
|
||||
This can be only applied for HTML/Latex/Markdown writer classes.
|
||||
Possible string values are:
|
||||
|
||||
- ``"normal"`` (default)
|
||||
- ``"bold"``
|
||||
|
||||
font_style (|str| / :py:class:`~.style.FontStyle`):
|
||||
Font style specification for cells in a column.
|
||||
This can be applied only for HTML/Latex/Markdown writer classes.
|
||||
Possible string values are:
|
||||
|
||||
- ``"normal"`` (default)
|
||||
- ``"italic"``
|
||||
- ``"typewriter"`` (only for Latex writer)
|
||||
|
||||
decoration_line (|str| / :py:class:`~.style.DecorationLine`)
|
||||
|
||||
Experiental.
|
||||
Possible string values are:
|
||||
|
||||
- ``"line-through"``
|
||||
- ``"strike"`` (alias for ``"line-through"``)
|
||||
- ``"underline"``
|
||||
- ``"none"`` (default)
|
||||
|
||||
thousand_separator (|str| / :py:class:`~.style.ThousandSeparator`):
|
||||
Thousand separator specification for numbers in a column.
|
||||
This can be only applied for text format writer classes.
|
||||
Possible string values are:
|
||||
|
||||
- ``","``/``"comma"``
|
||||
- ``" "``/``"space"``
|
||||
- ``"_"``/``"underscore"``
|
||||
- ``""``/``"none"`` (default)
|
||||
|
||||
Example:
|
||||
:ref:`example-style`
|
||||
"""
|
||||
|
||||
@property
|
||||
def align(self) -> Align:
|
||||
return self.__align
|
||||
|
||||
@align.setter
|
||||
def align(self, value: Align) -> None:
|
||||
self.__align = value
|
||||
|
||||
@property
|
||||
def vertical_align(self) -> VerticalAlign:
|
||||
return self.__valign
|
||||
|
||||
@property
|
||||
def decoration_line(self) -> DecorationLine:
|
||||
return self.__decoration_line
|
||||
|
||||
@property
|
||||
def font_size(self) -> FontSize:
|
||||
return self.__font_size
|
||||
|
||||
@property
|
||||
def font_style(self) -> FontStyle:
|
||||
return self.__font_style
|
||||
|
||||
@property
|
||||
def font_weight(self) -> FontWeight:
|
||||
return self.__font_weight
|
||||
|
||||
@property
|
||||
def color(self) -> Optional[Color]:
|
||||
return self.__fg_color
|
||||
|
||||
@property
|
||||
def fg_color(self) -> Optional[Color]:
|
||||
return self.__fg_color
|
||||
|
||||
@property
|
||||
def bg_color(self) -> Optional[Color]:
|
||||
return self.__bg_color
|
||||
|
||||
@property
|
||||
def thousand_separator(self) -> ThousandSeparator:
|
||||
return self.__thousand_separator
|
||||
|
||||
@property
|
||||
def padding(self) -> Optional[int]:
|
||||
return self.__padding
|
||||
|
||||
@padding.setter
|
||||
def padding(self, value: Optional[int]) -> None:
|
||||
self.__padding = value
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
self.__kwargs = kwargs
|
||||
self.__update_color(initialize=True)
|
||||
self.__update_align(initialize=True)
|
||||
self.__update_font(initialize=True)
|
||||
self.__update_misc(initialize=True)
|
||||
|
||||
if self.__kwargs:
|
||||
warnings.warn(f"unknown style attributes found: {self.__kwargs.keys()}", UserWarning)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
items = []
|
||||
|
||||
if self.align:
|
||||
items.append(f"align={self.align.align_string}")
|
||||
if self.padding is not None:
|
||||
items.append(f"padding={self.padding}")
|
||||
if self.vertical_align:
|
||||
items.append(f"valign={self.vertical_align.align_str}")
|
||||
if self.color:
|
||||
items.append(f"color={self.color}")
|
||||
if self.bg_color:
|
||||
items.append(f"bg_color={self.bg_color}")
|
||||
if self.decoration_line is not DecorationLine.NONE:
|
||||
items.append(f"decoration_line={self.decoration_line.value}")
|
||||
if self.font_size is not FontSize.NONE:
|
||||
items.append(f"font_size={self.font_size.value}")
|
||||
if self.font_style:
|
||||
items.append(f"font_style={self.font_style.value}")
|
||||
if self.font_weight:
|
||||
items.append(f"font_weight={self.font_weight.value}")
|
||||
if self.thousand_separator is not ThousandSeparator.NONE:
|
||||
items.append(f"thousand_separator={self.thousand_separator.value}")
|
||||
|
||||
return "({})".format(", ".join(items))
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if self.__class__ is not other.__class__:
|
||||
return False
|
||||
|
||||
return all(
|
||||
[
|
||||
self.align == other.align,
|
||||
self.font_size == other.font_size,
|
||||
self.font_style == other.font_style,
|
||||
self.font_weight == other.font_weight,
|
||||
self.thousand_separator == other.thousand_separator,
|
||||
]
|
||||
)
|
||||
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
if self.__class__ is not other.__class__:
|
||||
return True
|
||||
|
||||
return not self.__eq__(other)
|
||||
|
||||
def update(self, **kwargs: Any) -> None:
|
||||
"""Update specified style attributes."""
|
||||
self.__kwargs = kwargs
|
||||
self.__update_color(initialize=False)
|
||||
self.__update_align(initialize=False)
|
||||
self.__update_font(initialize=False)
|
||||
self.__update_misc(initialize=False)
|
||||
|
||||
if self.__kwargs:
|
||||
warnings.warn(f"unknown style attributes found: {self.__kwargs.keys()}", UserWarning)
|
||||
|
||||
def __update_color(self, initialize: bool) -> None:
|
||||
fg_color = self.__kwargs.pop("color", None) or self.__kwargs.pop("fg_color", None)
|
||||
if fg_color:
|
||||
self.__fg_color: Optional[Color] = Color(fg_color)
|
||||
elif initialize:
|
||||
self.__fg_color = None
|
||||
|
||||
bg_color = self.__kwargs.pop("bg_color", None)
|
||||
if bg_color:
|
||||
self.__bg_color: Optional[Color] = Color(bg_color)
|
||||
elif initialize:
|
||||
self.__bg_color = None
|
||||
|
||||
def __update_font(self, initialize: bool) -> None:
|
||||
font_size = self.__kwargs.pop("font_size", None)
|
||||
if font_size:
|
||||
self.__font_size = normalize_enum(
|
||||
font_size,
|
||||
FontSize,
|
||||
validate=False,
|
||||
default=FontSize.NONE,
|
||||
)
|
||||
elif initialize:
|
||||
self.__font_size = FontSize.NONE
|
||||
self.__validate_attr("font_size", (FontSize, str))
|
||||
|
||||
font_style = self.__kwargs.pop("font_style", None)
|
||||
if font_style:
|
||||
self.__font_style = normalize_enum(font_style, FontStyle, default=FontStyle.NORMAL)
|
||||
elif initialize:
|
||||
self.__font_style = FontStyle.NORMAL
|
||||
self.__validate_attr("font_style", (FontStyle,))
|
||||
|
||||
font_weight = self.__kwargs.pop("font_weight", None)
|
||||
if font_weight:
|
||||
self.__font_weight = normalize_enum(font_weight, FontWeight, default=FontWeight.NORMAL)
|
||||
elif initialize:
|
||||
self.__font_weight = FontWeight.NORMAL
|
||||
self.__validate_attr("font_weight", (FontWeight,))
|
||||
|
||||
def __update_align(self, initialize: bool) -> None:
|
||||
align = self.__kwargs.pop("align", None)
|
||||
if align:
|
||||
self.__align = normalize_enum(align, Align, default=Align.AUTO)
|
||||
elif initialize:
|
||||
self.__align = Align.AUTO
|
||||
self.__validate_attr("align", (Align,))
|
||||
|
||||
valign = self.__kwargs.pop("vertical_align", None)
|
||||
if valign:
|
||||
self.__valign = normalize_enum(valign, VerticalAlign, default=VerticalAlign.BASELINE)
|
||||
elif initialize:
|
||||
self.__valign = VerticalAlign.BASELINE
|
||||
self.__validate_attr("vertical_align", (VerticalAlign,))
|
||||
|
||||
def __update_misc(self, initialize: bool) -> None:
|
||||
padding = self.__kwargs.pop("padding", None)
|
||||
if padding is not None:
|
||||
self.__padding = padding
|
||||
elif initialize:
|
||||
self.__padding = None
|
||||
|
||||
decoration_line = self.__kwargs.pop("decoration_line", None)
|
||||
if decoration_line:
|
||||
self.__decoration_line = normalize_enum(
|
||||
decoration_line, DecorationLine, default=DecorationLine.NONE
|
||||
)
|
||||
elif initialize:
|
||||
self.__decoration_line = DecorationLine.NONE
|
||||
self.__validate_attr("decoration_line", (DecorationLine,))
|
||||
|
||||
thousand_separator = self.__kwargs.pop("thousand_separator", None)
|
||||
if thousand_separator:
|
||||
self.__thousand_separator = _normalize_thousand_separator(thousand_separator)
|
||||
elif initialize:
|
||||
self.__thousand_separator = ThousandSeparator.NONE
|
||||
self.__validate_attr("thousand_separator", (ThousandSeparator,))
|
||||
|
||||
def __validate_attr(self, attr_name: str, expected_types: tuple[type, ...]) -> None:
|
||||
value = getattr(self, attr_name)
|
||||
expected = " or ".join(c.__name__ for c in expected_types)
|
||||
|
||||
if not isinstance(value, expected_types):
|
||||
raise TypeError(f"{attr_name} must be instance of {expected}: actual={type(value)}")
|
@ -0,0 +1,331 @@
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, Final, Optional
|
||||
|
||||
from dataproperty import Align
|
||||
from tcolorpy import Color, tcolor
|
||||
|
||||
from ._font import FontSize, FontStyle, FontWeight
|
||||
from ._style import DecorationLine, Style, ThousandSeparator
|
||||
from ._styler_interface import StylerInterface
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..writer._table_writer import AbstractTableWriter
|
||||
|
||||
|
||||
_align_char_mapping: Final[dict[Align, str]] = {
|
||||
Align.AUTO: "<",
|
||||
Align.LEFT: "<",
|
||||
Align.RIGHT: ">",
|
||||
Align.CENTER: "^",
|
||||
}
|
||||
|
||||
|
||||
def get_align_char(align: Align) -> str:
|
||||
return _align_char_mapping[align]
|
||||
|
||||
|
||||
def _to_latex_rgb(color: Color, value: str) -> str:
|
||||
return r"\textcolor{" + color.color_code + "}{" + value + "}"
|
||||
|
||||
|
||||
class AbstractStyler(StylerInterface):
|
||||
def __init__(self, writer: "AbstractTableWriter") -> None:
|
||||
self._writer = writer
|
||||
self._font_size_map = self._get_font_size_map()
|
||||
|
||||
def get_font_size(self, style: Style) -> Optional[str]:
|
||||
return self._font_size_map.get(style.font_size)
|
||||
|
||||
def get_additional_char_width(self, style: Style) -> int:
|
||||
return 0
|
||||
|
||||
def apply(self, value: Any, style: Style) -> str:
|
||||
return value
|
||||
|
||||
def apply_align(self, value: str, style: Style) -> str:
|
||||
return value
|
||||
|
||||
def apply_terminal_style(self, value: str, style: Style) -> str:
|
||||
return value
|
||||
|
||||
def _get_font_size_map(self) -> dict[FontSize, str]:
|
||||
return {}
|
||||
|
||||
|
||||
class NullStyler(AbstractStyler):
|
||||
def get_font_size(self, style: Style) -> Optional[str]:
|
||||
return ""
|
||||
|
||||
|
||||
class TextStyler(AbstractStyler):
|
||||
def apply_terminal_style(self, value: str, style: Style) -> str:
|
||||
if not self._writer.enable_ansi_escape:
|
||||
return value
|
||||
|
||||
ansi_styles = []
|
||||
|
||||
if style.decoration_line in (DecorationLine.STRIKE, DecorationLine.LINE_THROUGH):
|
||||
ansi_styles.append("strike")
|
||||
if style.decoration_line == DecorationLine.UNDERLINE:
|
||||
ansi_styles.append("underline")
|
||||
|
||||
if style.font_weight == FontWeight.BOLD:
|
||||
ansi_styles.append("bold")
|
||||
|
||||
if self._writer.colorize_terminal:
|
||||
return tcolor(value, color=style.color, bg_color=style.bg_color, styles=ansi_styles)
|
||||
|
||||
return tcolor(value, styles=ansi_styles)
|
||||
|
||||
def __get_align_format(self, style: Style) -> str:
|
||||
align_char = get_align_char(style.align)
|
||||
format_items = ["{:" + align_char]
|
||||
if style.padding is not None and style.padding > 0:
|
||||
format_items.append(str(style.padding))
|
||||
format_items.append("s}")
|
||||
|
||||
return "".join(format_items)
|
||||
|
||||
def apply_align(self, value: str, style: Style) -> str:
|
||||
return self.__get_align_format(style).format(value)
|
||||
|
||||
def apply(self, value: str, style: Style) -> str:
|
||||
if value:
|
||||
if style.thousand_separator == ThousandSeparator.SPACE:
|
||||
value = value.replace(",", " ")
|
||||
elif style.thousand_separator == ThousandSeparator.UNDERSCORE:
|
||||
value = value.replace(",", "_")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class HtmlStyler(TextStyler):
|
||||
def _get_font_size_map(self) -> dict[FontSize, str]:
|
||||
return {
|
||||
FontSize.TINY: "font-size:x-small",
|
||||
FontSize.SMALL: "font-size:small",
|
||||
FontSize.MEDIUM: "font-size:medium",
|
||||
FontSize.LARGE: "font-size:large",
|
||||
}
|
||||
|
||||
|
||||
class LatexStyler(TextStyler):
|
||||
class Command:
|
||||
BOLD: Final = r"\bf"
|
||||
ITALIC: Final = r"\it"
|
||||
TYPEWRITER: Final = r"\tt"
|
||||
UNDERLINE: Final = r"\underline"
|
||||
STRIKEOUT: Final = r"\sout"
|
||||
|
||||
def get_additional_char_width(self, style: Style) -> int:
|
||||
dummy_value = "d"
|
||||
applied_value = self.apply(dummy_value, style)
|
||||
|
||||
return len(applied_value) - len(dummy_value)
|
||||
|
||||
def apply(self, value: Any, style: Style) -> str:
|
||||
value = super().apply(value, style)
|
||||
if not value:
|
||||
return value
|
||||
|
||||
font_size = self.get_font_size(style)
|
||||
commands = []
|
||||
|
||||
if font_size:
|
||||
commands.append(font_size)
|
||||
|
||||
if style.font_weight == FontWeight.BOLD:
|
||||
commands.append(self.Command.BOLD)
|
||||
|
||||
if style.font_style == FontStyle.ITALIC:
|
||||
commands.append(self.Command.ITALIC)
|
||||
elif style.font_style == FontStyle.TYPEWRITER:
|
||||
commands.append(self.Command.TYPEWRITER)
|
||||
|
||||
if style.decoration_line in (DecorationLine.STRIKE, DecorationLine.LINE_THROUGH):
|
||||
commands.append(self.Command.STRIKEOUT)
|
||||
elif style.decoration_line == DecorationLine.UNDERLINE:
|
||||
commands.append(self.Command.UNDERLINE)
|
||||
|
||||
for cmd in commands:
|
||||
value = cmd + "{" + value + "}"
|
||||
|
||||
value = self.__apply_color(value, style)
|
||||
|
||||
return value
|
||||
|
||||
def __apply_color(self, value: str, style: Style) -> str:
|
||||
if not style.fg_color:
|
||||
return value
|
||||
|
||||
value = _to_latex_rgb(style.fg_color, value)
|
||||
|
||||
return value
|
||||
|
||||
def _get_font_size_map(self) -> dict[FontSize, str]:
|
||||
return {
|
||||
FontSize.TINY: r"\tiny",
|
||||
FontSize.SMALL: r"\small",
|
||||
FontSize.MEDIUM: r"\normalsize",
|
||||
FontSize.LARGE: r"\large",
|
||||
}
|
||||
|
||||
|
||||
class MarkdownStyler(TextStyler):
|
||||
def get_additional_char_width(self, style: Style) -> int:
|
||||
width = 0
|
||||
|
||||
if style.font_weight == FontWeight.BOLD:
|
||||
width += 4
|
||||
|
||||
if style.font_style == FontStyle.ITALIC:
|
||||
width += 2
|
||||
|
||||
return width
|
||||
|
||||
def apply(self, value: Any, style: Style) -> str:
|
||||
value = super().apply(value, style)
|
||||
if not value:
|
||||
return value
|
||||
|
||||
value = self._apply_font_weight(value, style)
|
||||
value = self._apply_font_style(value, style)
|
||||
|
||||
return value
|
||||
|
||||
def _apply_font_weight(self, value: Any, style: Style) -> str:
|
||||
if style.font_weight == FontWeight.BOLD:
|
||||
value = f"**{value}**"
|
||||
|
||||
return value
|
||||
|
||||
def _apply_font_style(self, value: Any, style: Style) -> str:
|
||||
if style.font_style == FontStyle.ITALIC:
|
||||
value = f"_{value}_"
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class GFMarkdownStyler(MarkdownStyler):
|
||||
"""
|
||||
A styler class for GitHub Flavored Markdown
|
||||
"""
|
||||
|
||||
def get_additional_char_width(self, style: Style) -> int:
|
||||
width = super().get_additional_char_width(style)
|
||||
|
||||
if style.decoration_line in (DecorationLine.STRIKE, DecorationLine.LINE_THROUGH):
|
||||
width += 4
|
||||
|
||||
if self.__use_latex(style):
|
||||
dummy_value = "d"
|
||||
value = self.apply(dummy_value, style)
|
||||
width += len(value) - len(dummy_value)
|
||||
|
||||
return width
|
||||
|
||||
def apply(self, value: Any, style: Style) -> str:
|
||||
value = super().apply(value, style)
|
||||
if not value:
|
||||
return value
|
||||
|
||||
use_latex = self.__use_latex(style)
|
||||
|
||||
if use_latex:
|
||||
value = self.__escape_for_latex(value)
|
||||
value = LatexStyler.Command.TYPEWRITER + "{" + value + "}"
|
||||
|
||||
value = self.__apply_decoration_line(value, style)
|
||||
|
||||
if use_latex:
|
||||
value = r"$$" + self.__apply_color(value, style) + r"$$"
|
||||
|
||||
return value
|
||||
|
||||
def __use_latex(self, style: Style) -> bool:
|
||||
return style.fg_color is not None
|
||||
|
||||
def __escape_for_latex(self, value: str) -> str:
|
||||
value = re.sub(r"[\s_]", r"\\\\\g<0>", value)
|
||||
return value.replace("-", r"\text{-}")
|
||||
|
||||
def __apply_decoration_line(self, value: str, style: Style) -> str:
|
||||
use_latex = self.__use_latex(style)
|
||||
|
||||
if style.decoration_line in (DecorationLine.STRIKE, DecorationLine.LINE_THROUGH):
|
||||
if use_latex:
|
||||
value = r"\enclose{horizontalstrike}{" + value + "}"
|
||||
else:
|
||||
value = f"~~{value}~~"
|
||||
elif style.decoration_line == DecorationLine.UNDERLINE:
|
||||
if use_latex:
|
||||
value = r"\underline{" + value + "}"
|
||||
|
||||
return value
|
||||
|
||||
def __apply_color(self, value: str, style: Style) -> str:
|
||||
if not style.fg_color:
|
||||
return value
|
||||
|
||||
return _to_latex_rgb(style.fg_color, value)
|
||||
|
||||
def _apply_font_weight(self, value: Any, style: Style) -> str:
|
||||
if not self.__use_latex(style):
|
||||
return super()._apply_font_weight(value, style)
|
||||
|
||||
if style.font_weight == FontWeight.BOLD:
|
||||
value = LatexStyler.Command.BOLD + "{" + value + "}"
|
||||
|
||||
return value
|
||||
|
||||
def _apply_font_style(self, value: Any, style: Style) -> str:
|
||||
if not self.__use_latex(style):
|
||||
return super()._apply_font_style(value, style)
|
||||
|
||||
if style.font_style == FontStyle.ITALIC:
|
||||
value = LatexStyler.Command.ITALIC + "{" + value + "}"
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class ReStructuredTextStyler(TextStyler):
|
||||
def get_additional_char_width(self, style: Style) -> int:
|
||||
from ..writer import RstCsvTableWriter
|
||||
|
||||
width = 0
|
||||
|
||||
if style.font_weight == FontWeight.BOLD:
|
||||
width += 4
|
||||
elif style.font_style == FontStyle.ITALIC:
|
||||
width += 2
|
||||
|
||||
if (
|
||||
style.thousand_separator == ThousandSeparator.COMMA
|
||||
and self._writer.format_name == RstCsvTableWriter.FORMAT_NAME
|
||||
):
|
||||
width += 2
|
||||
|
||||
return width
|
||||
|
||||
def apply(self, value: Any, style: Style) -> str:
|
||||
from ..writer import RstCsvTableWriter
|
||||
|
||||
value = super().apply(value, style)
|
||||
if not value:
|
||||
return value
|
||||
|
||||
if style.font_weight == FontWeight.BOLD:
|
||||
value = f"**{value}**"
|
||||
elif style.font_style == FontStyle.ITALIC:
|
||||
# in reStructuredText, some custom style definition will be required to
|
||||
# set for both bold and italic (currently not supported)
|
||||
value = f"*{value}*"
|
||||
|
||||
if (
|
||||
style.thousand_separator == ThousandSeparator.COMMA
|
||||
and self._writer.format_name == RstCsvTableWriter.FORMAT_NAME
|
||||
):
|
||||
value = f'"{value}"'
|
||||
|
||||
return value
|
@ -0,0 +1,26 @@
|
||||
import abc
|
||||
from typing import Any, Optional
|
||||
|
||||
from ._style import Style
|
||||
|
||||
|
||||
class StylerInterface(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def apply(self, value: Any, style: Style) -> str: # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def apply_align(self, value: str, style: Style) -> str: # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def apply_terminal_style(self, value: str, style: Style) -> str: # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_font_size(self, style: Style) -> Optional[str]: # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_additional_char_width(self, style: Style) -> int: # pragma: no cover
|
||||
raise NotImplementedError()
|
@ -0,0 +1,93 @@
|
||||
import importlib
|
||||
import pkgutil
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Final, NamedTuple, Optional, Protocol
|
||||
|
||||
from .._logger import logger
|
||||
from ..style import Cell, Style
|
||||
|
||||
|
||||
PLUGIN_NAME_PEFIX: Final = "pytablewriter"
|
||||
PLUGIN_NAME_SUFFIX: Final = "theme"
|
||||
KNOWN_PLUGINS: Final = (
|
||||
f"{PLUGIN_NAME_PEFIX}_altrow_{PLUGIN_NAME_SUFFIX}",
|
||||
f"{PLUGIN_NAME_PEFIX}_altcol_{PLUGIN_NAME_SUFFIX}",
|
||||
)
|
||||
|
||||
|
||||
class StyleFilterFunc(Protocol):
|
||||
def __call__(self, cell: Cell, **kwargs: Any) -> Optional[Style]: ...
|
||||
|
||||
|
||||
class ColSeparatorStyleFilterFunc(Protocol):
|
||||
def __call__(
|
||||
self, left_cell: Optional[Cell], right_cell: Optional[Cell], **kwargs: Any
|
||||
) -> Optional[Style]: ...
|
||||
|
||||
|
||||
class CheckStyleFilterKeywordArgsFunc(Protocol):
|
||||
def __call__(self, **kwargs: Any) -> None: ...
|
||||
|
||||
|
||||
class Theme(NamedTuple):
|
||||
style_filter: Optional[StyleFilterFunc]
|
||||
col_separator_style_filter: Optional[ColSeparatorStyleFilterFunc]
|
||||
check_style_filter_kwargs: Optional[CheckStyleFilterKeywordArgsFunc]
|
||||
|
||||
|
||||
def list_themes() -> Sequence[str]:
|
||||
return list(load_ptw_plugins())
|
||||
|
||||
|
||||
def load_ptw_plugins() -> dict[str, Theme]:
|
||||
plugin_regexp: Final = re.compile(
|
||||
rf"^{PLUGIN_NAME_PEFIX}[_-].+[_-]{PLUGIN_NAME_SUFFIX}", re.IGNORECASE
|
||||
)
|
||||
|
||||
discovered_plugins: Final = {
|
||||
name: importlib.import_module(name)
|
||||
for _finder, name, _ispkg in pkgutil.iter_modules()
|
||||
if plugin_regexp.search(name) is not None
|
||||
}
|
||||
|
||||
logger.debug(f"discovered_plugins: {list(discovered_plugins)}")
|
||||
|
||||
themes: dict[str, Theme] = {}
|
||||
for theme, plugin in discovered_plugins.items():
|
||||
style_filter = plugin.style_filter if hasattr(plugin, "style_filter") else None
|
||||
col_sep_style_filter = (
|
||||
plugin.col_separator_style_filter
|
||||
if hasattr(plugin, "col_separator_style_filter")
|
||||
else None
|
||||
)
|
||||
check_kwargs_func = (
|
||||
plugin.check_style_filter_kwargs
|
||||
if hasattr(plugin, "check_style_filter_kwargs")
|
||||
else None
|
||||
)
|
||||
themes[theme] = Theme(style_filter, col_sep_style_filter, check_kwargs_func)
|
||||
|
||||
return themes
|
||||
|
||||
|
||||
def fetch_theme(plugin_name: str) -> Theme:
|
||||
loaded_themes: Final = load_ptw_plugins()
|
||||
theme_regexp: Final = re.compile(
|
||||
rf"^{PLUGIN_NAME_PEFIX}[_-]{plugin_name}[_-]{PLUGIN_NAME_SUFFIX}", re.IGNORECASE
|
||||
)
|
||||
matched_theme = None
|
||||
|
||||
for loaded_theme in loaded_themes:
|
||||
if theme_regexp.search(loaded_theme):
|
||||
matched_theme = loaded_theme
|
||||
break
|
||||
else:
|
||||
err_msgs = [f"{plugin_name} theme is not installed."]
|
||||
|
||||
if plugin_name in KNOWN_PLUGINS:
|
||||
err_msgs.append(f"try 'pip install {plugin_name}' to install the theme.")
|
||||
|
||||
raise RuntimeError(" ".join(err_msgs))
|
||||
|
||||
return loaded_themes[matched_theme]
|
Reference in New Issue
Block a user