import docx.text.font as Font from typing import Optional, Union, Dict, Any from enum import Enum class ThemeFont(Enum): MAJOR_ASCII = "majorAscii" MAJOR_H_ANSI = "majorHAnsi" MAJOR_EAST_ASIAN = "majorEastAsia" MAJOR_BIDI = "majorBidi" MINOR_ASCII = "minorAscii" MINOR_H_ANSI = "minorHAnsi" MINOR_EAST_ASIAN = "minorEastAsia" MINOR_BIDI = "minorBidi" class Font: """ Класс, представляющий форматирование шрифта для пробега текста (run) в Word. Поддерживает три-state значения (True/False/None) и множественные шрифты. """ def __init__(self): # Основные шрифты self._ascii: Optional[str] = None self._h_ansi: Optional[str] = None self._east_asia: Optional[str] = None self._cs: Optional[str] = None # Тематические шрифты (меньший приоритет) self._ascii_theme: Optional[ThemeFont] = None self._h_ansi_theme: Optional[ThemeFont] = None self._east_asia_theme: Optional[ThemeFont] = None self._cs_theme: Optional[ThemeFont] = None # Три-state булевы свойства (None = унаследовано) self._bold: Optional[bool] = None self._italic: Optional[bool] = None self._small_caps: Optional[bool] = None self._strike: Optional[bool] = None self._double_strike: Optional[bool] = None self._outline: Optional[bool] = None self._shadow: Optional[bool] = None self._emboss: Optional[bool] = None self._imprint: Optional[bool] = None self._hidden: Optional[bool] = None self._all_caps: Optional[bool] = None self._complex_script: Optional[bool] = None self._rtl: Optional[bool] = None self._no_proof: Optional[bool] = None # --- Свойство: name (основное имя шрифта) --- @property def name(self) -> Optional[str]: """Возвращает имя основного шрифта (w:ascii).""" return self._ascii @name.setter def name(self, value: Optional[str]) -> None: """ Устанавливает w:ascii и w:hAnsi одновременно. Если значение None — оба атрибута удаляются. """ if value is None: self._ascii = None self._h_ansi = None else: self._ascii = str(value) self._h_ansi = str(value) # --- Другие шрифты --- @property def east_asia(self) -> Optional[str]: return self._east_asia @east_asia.setter def east_asia(self, value: Optional[str]) -> None: self._east_asia = value @property def complex_script(self) -> Optional[str]: return self._cs @complex_script.setter def complex_script(self, value: Optional[str]) -> None: self._cs = value @property def ascii_theme(self) -> Optional[ThemeFont]: return self._ascii_theme @ascii_theme.setter def ascii_theme(self, theme_font: Optional[ThemeFont]) -> None: if isinstance(theme_font, ThemeFont) or theme_font is None: self._ascii_theme = theme_font else: raise ValueError("ascii_theme must be a ThemeFont or None") @property def h_ansi_theme(self) -> Optional[ThemeFont]: return self._h_ansi_theme @h_ansi_theme.setter def h_ansi_theme(self, theme_font: Optional[ThemeFont]) -> None: self._h_ansi_theme = theme_font # --- Три-state Boolean Properties (Toggle properties marked) --- @property def bold(self) -> Optional[bool]: return self._bold @bold.setter def bold(self, value: Optional[bool]) -> None: if value in (True, False, None): self._bold = value else: raise ValueError("bold must be True, False, or None") @property def italic(self) -> Optional[bool]: return self._italic @italic.setter def italic(self, value: Optional[bool]) -> None: if value in (True, False, None): self._italic = value else: raise ValueError("italic must be True, False, or None") @property def small_caps(self) -> Optional[bool]: return self._small_caps @small_caps.setter def small_caps(self, value: Optional[bool]) -> None: if value in (True, False, None): self._small_caps = value else: raise ValueError("small_caps must be True, False, or None") @property def strike(self) -> Optional[bool]: return self._strike @strike.setter def strike(self, value: Optional[bool]) -> None: if value in (True, False, None): self._strike = value else: raise ValueError("strike must be True, False, or None") @property def double_strike(self) -> Optional[bool]: return self._double_strike @double_strike.setter def double_strike(self, value: Optional[bool]) -> None: if value in (True, False, None): self._double_strike = value else: raise ValueError("double_strike must be True, False, or None") @property def outline(self) -> Optional[bool]: return self._outline @outline.setter def outline(self, value: Optional[bool]) -> None: if value in (True, False, None): self._outline = value else: raise ValueError("outline must be True, False, or None") @property def shadow(self) -> Optional[bool]: return self._shadow @shadow.setter def shadow(self, value: Optional[bool]) -> None: if value in (True, False, None): self._shadow = value else: raise ValueError("shadow must be True, False, or None") @property def emboss(self) -> Optional[bool]: return self._emboss @emboss.setter def emboss(self, value: Optional[bool]) -> None: if value in (True, False, None): self._emboss = value else: raise ValueError("emboss must be True, False, or None") @property def imprint(self) -> Optional[bool]: return self._imprint @imprint.setter def imprint(self, value: Optional[bool]) -> None: if value in (True, False, None): self._imprint = value else: raise ValueError("imprint must be True, False, or None") @property def hidden(self) -> Optional[bool]: return self._hidden @hidden.setter def hidden(self, value: Optional[bool]) -> None: if value in (True, False, None): self._hidden = value else: raise ValueError("hidden must be True, False, or None") @property def all_caps(self) -> Optional[bool]: return self._all_caps @all_caps.setter def all_caps(self, value: Optional[bool]) -> None: if value in (True, False, None): self._all_caps = value else: raise ValueError("all_caps must be True, False, or None") @property def cs(self) -> Optional[bool]: return self._complex_script @cs.setter def cs(self, value: Optional[bool]) -> None: if value in (True, False, None): self._complex_script = value else: raise ValueError("cs must be True, False, or None") @property def rtl(self) -> Optional[bool]: return self._rtl @rtl.setter def rtl(self, value: Optional[bool]) -> None: if value in (True, False, None): self._rtl = value else: raise ValueError("rtl must be True, False, or None") @property def no_proof(self) -> Optional[bool]: return self._no_proof @no_proof.setter def no_proof(self, value: Optional[bool]) -> None: if value in (True, False, None): self._no_proof = value else: raise ValueError("no_proof must be True, False, or None") # --- Методы сериализации в XML / словарь --- def to_xml_dict(self) -> Dict[str, Any]: """Преобразует текущее состояние шрифта в словарь, имитирующий XML.""" r_pr = {} fonts = {} if self._ascii: fonts['ascii'] = self._ascii if self._h_ansi: fonts['hAnsi'] = self._h_ansi if self._east_asia: fonts['eastAsia'] = self._east_asia if self._cs: fonts['cs'] = self._cs if self._ascii_theme: fonts['asciiTheme'] = self._ascii_theme.value if self._h_ansi_theme: fonts['hAnsiTheme'] = self._h_ansi_theme.value if self._east_asia_theme: fonts['eastAsiaTheme'] = self._east_asia_theme.value if self._cs_theme: fonts['csTheme'] = self._cs_theme.value if fonts: r_pr['rFonts'] = fonts # Boolean properties (если не None) bool_map = { 'b': self._bold, 'i': self._italic, 'caps': self._all_caps, 'smallCaps': self._small_caps, 'strike': self._strike, 'dstrike': self._double_strike, 'outline': self._outline, 'shadow': self._shadow, 'emboss': self._emboss, 'imprint': self._imprint, 'vanish': self._hidden, 'cs': self._complex_script, 'rtl': self._rtl, 'noProof': self._no_proof } for tag, val in bool_map.items(): if val is not None: r_pr[tag] = 'on' if val else 'off' return r_pr def __repr__(self) -> str: return f"" class FontManager: """ Централизованный менеджер шрифтов, использующий темы. Может устанавливать глобальные шрифты (major/minor) для разных языковых групп. """ def __init__(self): self._theme_fonts: Dict[ThemeFont, str] = {} def set_theme_font(self, theme_type: ThemeFont, font_name: str) -> None: """Устанавливает шрифт для определённой темы.""" self._theme_fonts[theme_type] = font_name def get_theme_font(self, theme_type: ThemeFont) -> Optional[str]: """Получает шрифт по типу темы.""" return self._theme_fonts.get(theme_type) def apply_theme_to_font(self, font: Font) -> None: """Применяет тему к объекту Font, если явно не задан основной шрифт.""" # Только если w:ascii не задан, используем w:asciiTheme if font._ascii is None and font._ascii_theme: font._ascii = self.get_theme_font(font._ascii_theme) if font._h_ansi is None and font._h_ansi_theme: font._h_ansi = self.get_theme_font(font._h_ansi_theme) if font._east_asia is None and font._east_asia_theme: font._east_asia = self.get_theme_font(font._east_asia_theme) if font._cs is None and font._cs_theme: font._cs = self.get_theme_font(font._cs_theme) def __repr__(self) -> str: return f"" from enum import Enum from typing import Optional, Dict, Any, List class StyleType(Enum): PARAGRAPH = "paragraph" CHARACTER = "character" TABLE = "table" NUMBERING = "numbering" class Style: """ Представление одного стиля Word (). Поддерживает атрибуты: имя, ID, тип, базовый стиль, приоритет, поведение и форматирование. """ def __init__( self, style_id: str, type_: StyleType, name: Optional[str] = None, default: bool = False, custom_style: bool = False, ): self.style_id = style_id self.type = type_ self.default = default self.custom_style = custom_style # Основные свойства self._name: Optional[str] = name or style_id # fallback на styleId self._based_on: Optional[str] = None # ссылка на другой styleId self._next: Optional[str] = None self._link: Optional[str] = None # Поведенческие флаги self._ui_priority: Optional[int] = None self._semi_hidden: bool = False self._unhide_when_used: bool = False self._q_format: bool = False self._locked: bool = False # Форматирование self._paragraph_format: Optional[Dict[str, Any]] = None self._font: Optional[Dict[str, Any]] = None self._table_properties: Optional[Dict[str, Any]] = None self._row_properties: Optional[Dict[str, Any]] = None self._cell_properties: Optional[Dict[str, Any]] = None self._table_style_properties: List[Dict[str, Any]] = [] # Служебное self._rsid: Optional[str] = None # --- Свойства --- @property def name(self) -> str: return self._name or self.style_id @name.setter def name(self, value: str) -> None: self._name = value @property def based_on(self) -> Optional[str]: return self._based_on @based_on.setter def based_on(self, style_id: Optional[str]) -> None: self._based_on = style_id @property def next_style(self) -> Optional[str]: return self._next @next_style.setter def next_style(self, style_id: Optional[str]) -> None: self._next = style_id @property def linked_style(self) -> Optional[str]: return self._link @linked_style.setter def linked_style(self, style_id: Optional[str]) -> None: self._link = style_id @property def priority(self) -> Optional[int]: return self._ui_priority @priority.setter def priority(self, value: Optional[int]) -> None: if value is not None and not isinstance(value, int): raise ValueError("priority must be an integer or None") self._ui_priority = value @property def semi_hidden(self) -> bool: return self._semi_hidden @semi_hidden.setter def semi_hidden(self, value: bool) -> None: self._semi_hidden = bool(value) @property def unhide_when_used(self) -> bool: return self._unhide_when_used @unhide_when_used.setter def unhide_when_used(self, value: bool) -> None: self._unhide_when_used = bool(value) @property def quick_style(self) -> bool: return self._q_format @quick_style.setter def quick_style(self, value: bool) -> None: self._q_format = bool(value) @property def locked(self) -> bool: return self._locked @locked.setter def locked(self, value: bool) -> None: self._locked = bool(value) # --- Форматирование --- def set_paragraph_format(self, ppr: Dict[str, Any]) -> None: self._paragraph_format = ppr def set_font(self, rpr: Dict[str, Any]) -> None: self._font = rpr def set_table_properties(self, tbl_pr: Dict[str, Any]) -> None: self._table_properties = tbl_pr def set_row_properties(self, tr_pr: Dict[str, Any]) -> None: self._row_properties = tr_pr def set_cell_properties(self, tc_pr: Dict[str, Any]) -> None: self._cell_properties = tc_pr def add_table_style_property(self, prop: Dict[str, Any]) -> None: self._table_style_properties.append(prop) # --- Вспомогательные методы --- def is_builtin(self) -> bool: """Возвращает True, если стиль встроенный (не пользовательский).""" return not self.custom_style def is_visible_in_gallery(self) -> bool: """Появляется ли стиль в галерее стилей?""" return not self.semi_hidden and self.quick_style def to_xml_dict(self) -> Dict[str, Any]: """Сериализация в словарь, близкий к XML.""" result = { "w:type": self.type.value, "w:styleId": self.style_id, } if self.default: result["w:default"] = "1" if self.custom_style: result["w:customStyle"] = "1" children = [] if self._name: children.append({"w:name": {"w:val": self._name}}) if self._based_on: children.append({"w:basedOn": {"w:val": self._based_on}}) if self._next: children.append({"w:next": {"w:val": self._next}}) if self._link: children.append({"w:link": {"w:val": self._link}}) if self._ui_priority is not None: children.append({"w:uiPriority": {"w:val": str(self._ui_priority)}}) if self._semi_hidden: children.append({"w:semiHidden": {}}) if self._unhide_when_used: children.append({"w:unhideWhenUsed": {}}) if self._q_format: children.append({"w:qFormat": {}}) if self._locked: children.append({"w:locked": {}}) if self._rsid: children.append({"w:rsid": {"w:val": self._rsid}}) if self._paragraph_format: children.append({"w:pPr": self._paragraph_format}) if self._font: children.append({"w:rPr": self._font}) if self._table_properties: children.append({"w:tblPr": self._table_properties}) if self._row_properties: children.append({"w:trPr": self._row_properties}) if self._cell_properties: children.append({"w:tcPr": self._cell_properties}) for tsp in self._table_style_properties: children.append({"w:tblStylePr": tsp}) if children: result["_children"] = children return result def __repr__(self) -> str: return f"