Source code for fastpyxl.cell.text

# Copyright (c) 2010-2024 fastpyxl

"""
Richtext definition
"""

from fastpyxl.xml.constants import SHEET_MAIN_NS

from fastpyxl.styles.colors import Color
from fastpyxl.styles.fonts import _no_value
from fastpyxl.typed_serialisable.base import Serialisable
from fastpyxl.typed_serialisable.errors import FieldValidationError
from fastpyxl.typed_serialisable.fields import AliasField, Field


def _color_converter(value):
    if value is None:
        return None
    if isinstance(value, Color):
        return value
    if isinstance(value, str):
        return Color(rgb=value)
    raise FieldValidationError(f"color rejected value {value!r}")


def _underline_converter(value):
    if value is None or value == "none":
        return None
    allowed = {
        "single",
        "double",
        "singleAccounting",
        "doubleAccounting",
    }
    if value not in allowed:
        raise FieldValidationError(f"u rejected value {value!r}")
    return value


def _vert_align_converter(value):
    if value is None or value == "none":
        return None
    allowed = {"superscript", "subscript", "baseline"}
    if value not in allowed:
        raise FieldValidationError(f"vertAlign rejected value {value!r}")
    return value


def _scheme_converter(value):
    if value is None or value == "none":
        return None
    allowed = {"major", "minor"}
    if value not in allowed:
        raise FieldValidationError(f"scheme rejected value {value!r}")
    return value


def _family_converter(value):
    if value is None:
        return None
    try:
        numeric = float(value)
    except Exception as exc:  # pragma: no cover
        raise FieldValidationError(f"family rejected value {value!r}") from exc
    if numeric < 0 or numeric > 14:
        raise FieldValidationError(f"family rejected value {value!r}")
    return numeric


_PHONETIC_TYPES = frozenset(
    {
        "halfwidthKatakana",
        "fullwidthKatakana",
        "Hiragana",
        "noConversion",
    }
)


def _phonetic_type_converter(value):
    if value is None:
        return None
    if value not in _PHONETIC_TYPES:
        raise FieldValidationError(f"type rejected value {value!r}")
    return value


_PHONETIC_ALIGN = frozenset(
    {"noControl", "left", "center", "distributed"}
)


def _phonetic_alignment_converter(value):
    if value is None:
        return None
    if value not in _PHONETIC_ALIGN:
        raise FieldValidationError(f"alignment rejected value {value!r}")
    return value


[docs] class PhoneticProperties(Serialisable): tagname = "phoneticPr" fontId: int | None = Field.attribute(expected_type=int, allow_none=True, default=None) type: str | None = Field.attribute( expected_type=str, allow_none=True, converter=_phonetic_type_converter, default=None, ) alignment: str | None = Field.attribute( expected_type=str, allow_none=True, converter=_phonetic_alignment_converter, default=None, ) def __init__( self, fontId=None, type=None, alignment=None, ): self.fontId = fontId self.type = type self.alignment = alignment
[docs] class PhoneticText(Serialisable): tagname = "rPh" sb: int | None = Field.attribute(expected_type=int, allow_none=True, default=None) eb: int | None = Field.attribute(expected_type=int, allow_none=True, default=None) t: str | None = Field.nested_text(expected_type=str, allow_none=True, default=None) text: str | None = AliasField("t", default=None) def __init__( self, sb=None, eb=None, t=None, ): self.sb = sb self.eb = eb self.t = t
[docs] class InlineFont(Serialisable): """ Font for inline text because, yes what you need are different objects with the same elements but different constraints. """ tagname = "RPrElt" xml_order = ( "rFont", "charset", "family", "b", "i", "strike", "outline", "shadow", "condense", "extend", "color", "sz", "u", "vertAlign", "scheme", ) rFont: str | None = Field.nested_value(expected_type=str, allow_none=True, default=None) charset: int | None = Field.nested_value(expected_type=int, allow_none=True, default=None) family: float | None = Field.nested_value( expected_type=float, allow_none=True, converter=_family_converter, default=None, ) b: bool | None = Field.nested_bool(renderer=_no_value, default=None) i: bool | None = Field.nested_bool(renderer=_no_value, default=None) strike: bool | None = Field.nested_bool(allow_none=True, default=None) outline: bool | None = Field.nested_bool(allow_none=True, default=None) shadow: bool | None = Field.nested_bool(allow_none=True, default=None) condense: bool | None = Field.nested_bool(allow_none=True, default=None) extend: bool | None = Field.nested_bool(allow_none=True, default=None) color: Color | None = Field.element( expected_type=Color, allow_none=True, converter=_color_converter, default=None, ) sz: float | None = Field.nested_value(expected_type=float, allow_none=True, default=None) u: str | None = Field.nested_value( expected_type=str, allow_none=True, converter=_underline_converter, default=None, ) vertAlign: str | None = Field.nested_value( expected_type=str, allow_none=True, converter=_vert_align_converter, default=None, ) scheme: str | None = Field.nested_value( expected_type=str, allow_none=True, converter=_scheme_converter, default=None, ) def __init__( self, rFont=None, charset=None, family=None, b=None, i=None, strike=None, outline=None, shadow=None, condense=None, extend=None, color=None, sz=None, u=None, vertAlign=None, scheme=None, ): self.rFont = rFont self.charset = charset self.family = family self.b = b self.i = i self.strike = strike self.outline = outline self.shadow = shadow self.condense = condense self.extend = extend self.color = color self.sz = sz self.u = u self.vertAlign = vertAlign self.scheme = scheme
[docs] @classmethod def from_tree(cls, node): underline = node.find("{%s}u" % SHEET_MAIN_NS) if underline is not None and underline.get("val") is None: underline.set("val", "single") return super().from_tree(node)
[docs] class RichText(Serialisable): tagname = "RElt" rPr: InlineFont | None = Field.element(expected_type=InlineFont, allow_none=True, default=None) font: InlineFont | None = AliasField("rPr", default=None) t: str | None = Field.nested_text(expected_type=str, allow_none=True, default=None) text: str | None = AliasField("t", default=None) xml_order = ("rPr", "t") def __init__( self, rPr=None, t=None, ): self.rPr = rPr self.t = t
[docs] class Text(Serialisable): tagname = "text" xml_order = ("t", "r", "rPh", "phoneticPr") t: str | None = Field.nested_text(expected_type=str, allow_none=True, default=None) plain: str | None = AliasField("t", default=None) r: list[RichText] = Field.sequence(expected_type=RichText, default=list) formatted = AliasField("r", default=None) rPh: list[PhoneticText] = Field.sequence(expected_type=PhoneticText, default=list) phonetic = AliasField("rPh", default=None) phoneticPr: PhoneticProperties | None = Field.element( expected_type=PhoneticProperties, allow_none=True, default=None ) PhoneticProperties = AliasField("phoneticPr", default=None) def __init__( self, t=None, r=(), rPh=(), phoneticPr=None, ): self.t = t self.r = list(r) self.rPh = list(rPh) self.phoneticPr = phoneticPr
[docs] @classmethod def from_tree(cls, node): underline = node.find("{%s}u" % SHEET_MAIN_NS) if underline is not None and underline.get("val") is None: underline.set("val", "single") return super().from_tree(node)
@property def content(self): """ Text stripped of all formatting """ snippets = [] if self.plain is not None: snippets.append(self.plain) for block in self.formatted: if block.t is not None: snippets.append(block.t) return "".join(snippets)