@@ -0,0 +1,860 @@
|
||||
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"<Font name={self.name!r} bold={self.bold} italic={self.italic}>"
|
||||
|
||||
|
||||
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"<FontManager themes={len(self._theme_fonts)}>"
|
||||
|
||||
|
||||
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 (<w:style>).
|
||||
Поддерживает атрибуты: имя, 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"<Style id='{self.style_id}' type={self.type.name} name='{self.name}'>"
|
||||
|
||||
from typing import Iterator, Optional, Dict, List
|
||||
import re
|
||||
|
||||
|
||||
class Styles:
|
||||
"""
|
||||
Коллекция всех стилей документа: определённых, встроенных и latent.
|
||||
Имитирует поведение Word по сортировке, отображению и разрешению стилей.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._styles: Dict[str, Style] = {} # styleId -> Style
|
||||
self._latent_styles: Dict[str, Dict[str, Any]] = {}
|
||||
self._doc_defaults = {
|
||||
"rPr": {},
|
||||
"pPr": {}
|
||||
}
|
||||
self._latent_count = 0
|
||||
self._def_semi_hidden = True
|
||||
self._def_unhide_when_used = True
|
||||
self._def_q_format = False
|
||||
self._def_locked_state = False
|
||||
self._def_ui_priority = 99
|
||||
|
||||
# --- Управление latent styles ---
|
||||
|
||||
def define_latent_style(
|
||||
self,
|
||||
name: str,
|
||||
ui_priority: Optional[int] = None,
|
||||
semi_hidden: Optional[bool] = None,
|
||||
unhide_when_used: Optional[bool] = None,
|
||||
q_format: Optional[bool] = None,
|
||||
locked: Optional[bool] = None,
|
||||
) -> None:
|
||||
"""Добавляет исключение для latent style."""
|
||||
self._latent_styles[name] = {
|
||||
"uiPriority": ui_priority,
|
||||
"semiHidden": semi_hidden,
|
||||
"unhideWhenUsed": unhide_when_used,
|
||||
"qFormat": q_format,
|
||||
"locked": locked,
|
||||
}
|
||||
|
||||
def set_latent_defaults(
|
||||
self,
|
||||
count: int = 276,
|
||||
def_semi_hidden: bool = True,
|
||||
def_unhide_when_used: bool = True,
|
||||
def_q_format: bool = False,
|
||||
def_locked_state: bool = False,
|
||||
def_ui_priority: int = 99,
|
||||
) -> None:
|
||||
"""Устанавливает параметры по умолчанию для latent styles."""
|
||||
self._latent_count = count
|
||||
self._def_semi_hidden = def_semi_hidden
|
||||
self._def_unhide_when_used = def_unhide_when_used
|
||||
self._def_q_format = def_q_format
|
||||
self._def_locked_state = def_locked_state
|
||||
self._def_ui_priority = def_ui_priority
|
||||
|
||||
def get_latent_behavior(self, name: str) -> Dict[str, Any]:
|
||||
"""Возвращает поведенческие атрибуты для встроенного стиля."""
|
||||
base = {
|
||||
"semiHidden": self._def_semi_hidden,
|
||||
"unhideWhenUsed": self._def_unhide_when_used,
|
||||
"qFormat": self._def_q_format,
|
||||
"locked": self._def_locked_state,
|
||||
"uiPriority": self._def_ui_priority,
|
||||
}
|
||||
override = self._latent_styles.get(name, {})
|
||||
base.update({k: v for k, v in override.items() if v is not None})
|
||||
return base
|
||||
|
||||
# --- Добавление/получение стилей ---
|
||||
|
||||
def add_style(
|
||||
self,
|
||||
style_id: str,
|
||||
type_: StyleType,
|
||||
name: Optional[str] = None,
|
||||
default: bool = False,
|
||||
custom_style: bool = False,
|
||||
) -> Style:
|
||||
"""Создаёт и добавляет новый стиль."""
|
||||
style = Style(style_id, type_, name=name, default=default, custom_style=custom_style)
|
||||
self._styles[style_id] = style
|
||||
return style
|
||||
|
||||
def get_by_id(self, style_id: str) -> Optional[Style]:
|
||||
"""Получить стиль по ID."""
|
||||
return self._styles.get(style_id)
|
||||
|
||||
def get_by_name(self, name: str) -> Optional[Style]:
|
||||
"""Найти стиль по имени (регистронезависимо)."""
|
||||
for style in self._styles.values():
|
||||
if style.name.lower() == name.lower():
|
||||
return style
|
||||
return None
|
||||
|
||||
def __getitem__(self, key: str) -> Style:
|
||||
"""Доступ как по ID, так и по имени."""
|
||||
style = self.get_by_id(key)
|
||||
if style is None:
|
||||
style = self.get_by_name(key)
|
||||
if style is None:
|
||||
raise KeyError(f"Style '{key}' not found.")
|
||||
return style
|
||||
|
||||
def __setitem__(self, key: str, style: Style) -> None:
|
||||
if not isinstance(style, Style):
|
||||
raise TypeError("Only Style objects can be added.")
|
||||
self._styles[style.style_id] = style
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
style = self.get_by_id(key) or self.get_by_name(key)
|
||||
if style is None:
|
||||
raise KeyError(f"Style '{key}' not found.")
|
||||
del self._styles[style.style_id]
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
return (key in self._styles) or (self.get_by_name(key) is not None)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._styles)
|
||||
|
||||
def __iter__(self) -> Iterator[Style]:
|
||||
"""Итерация по всем определённым стилям."""
|
||||
return iter(self._styles.values())
|
||||
|
||||
# --- Document defaults ---
|
||||
|
||||
def set_default_run_properties(self, rpr: Dict[str, Any]) -> None:
|
||||
self._doc_defaults["rPr"] = rpr
|
||||
|
||||
def set_default_paragraph_properties(self, ppr: Dict[str, Any]) -> None:
|
||||
self._doc_defaults["pPr"] = ppr
|
||||
|
||||
def get_default_font(self) -> Dict[str, Any]:
|
||||
return self._doc_defaults["rPr"]
|
||||
|
||||
def get_default_paragraph_format(self) -> Dict[str, Any]:
|
||||
return self._doc_defaults["pPr"]
|
||||
|
||||
# --- Списки стилей (как в интерфейсе Word) ---
|
||||
|
||||
def recommended_list(self) -> List[Style]:
|
||||
"""Стили, отображаемые в категории 'Recommended'."""
|
||||
return [
|
||||
s for s in self._styles.values()
|
||||
if not s.semi_hidden
|
||||
]
|
||||
|
||||
def styles_in_use(self, used_style_ids: List[str]) -> List[Style]:
|
||||
"""Стили, применённые в документе и не semiHidden."""
|
||||
return [
|
||||
s for s in self._styles.values()
|
||||
if s.style_id in used_style_ids and not s.semi_hidden
|
||||
]
|
||||
|
||||
def in_current_document(self) -> List[Style]:
|
||||
"""Все определённые стили, кроме semiHidden."""
|
||||
return [s for s in self._styles.values() if not s.semi_hidden]
|
||||
|
||||
def all_styles(self) -> List[Style]:
|
||||
"""Все определённые стили (включая semiHidden)."""
|
||||
return list(self._styles.values())
|
||||
|
||||
# --- Сортировка ---
|
||||
|
||||
def sorted_by_appearance(self) -> List[Style]:
|
||||
"""
|
||||
Сортировка стилей так, как они отображаются в панели стилей Word:
|
||||
- По uiPriority (возрастание)
|
||||
- При равном приоритете — по имени (алфавитно)
|
||||
- Custom-стили без uiPriority попадают в начало (uiPriority=0)
|
||||
"""
|
||||
def sort_key(style: Style) -> tuple:
|
||||
prio = style.priority if style.priority is not None else 0
|
||||
return (prio, style.name.lower())
|
||||
|
||||
return sorted(self._styles.values(), key=sort_key)
|
||||
|
||||
# --- Генерация XML ---
|
||||
|
||||
def to_xml_dict(self) -> Dict[str, Any]:
|
||||
result = {
|
||||
"w:styles": {
|
||||
"_children": []
|
||||
}
|
||||
}
|
||||
|
||||
# docDefaults
|
||||
if any(self._doc_defaults["rPr"]) or any(self._doc_defaults["pPr"]):
|
||||
defaults = {"_children": []}
|
||||
if self._doc_defaults["rPr"]:
|
||||
defaults["_children"].append({
|
||||
"w:rPrDefault": {
|
||||
"_children": [{"w:rPr": self._doc_defaults["rPr"]}]
|
||||
}
|
||||
})
|
||||
if self._doc_defaults["pPr"]:
|
||||
defaults["_children"].append({
|
||||
"w:pPrDefault": {
|
||||
"_children": [{"w:pPr": self._doc_defaults["pPr"]}]
|
||||
}
|
||||
})
|
||||
result["w:styles"]["_children"].append(defaults)
|
||||
|
||||
# latentStyles
|
||||
latent_children = []
|
||||
for name, props in self._latent_styles.items():
|
||||
exc = {"w:name": name}
|
||||
if props["uiPriority"] is not None:
|
||||
exc["w:uiPriority"] = str(props["uiPriority"])
|
||||
if props["semiHidden"] is not None:
|
||||
exc["w:semiHidden"] = "1" if props["semiHidden"] else "0"
|
||||
if props["unhideWhenUsed"] is not None:
|
||||
exc["w:unhideWhenUsed"] = "1" if props["unhideWhenUsed"] else "0"
|
||||
if props["qFormat"] is not None:
|
||||
exc["w:qFormat"] = "1" if props["qFormat"] else "0"
|
||||
if props["locked"] is not None:
|
||||
exc["w:locked"] = "1" if props["locked"] else "0"
|
||||
latent_children.append({"w:lsdException": exc})
|
||||
|
||||
latent_attrs = {
|
||||
"w:count": str(self._latent_count),
|
||||
"w:defSemiHidden": "1" if self._def_semi_hidden else "0",
|
||||
"w:defUnhideWhenUsed": "1" if self._def_unhide_when_used else "0",
|
||||
"w:defQFormat": "1" if self._def_q_format else "0",
|
||||
"w:defLockedState": "1" if self._def_locked_state else "0",
|
||||
"w:defUIPriority": str(self._def_ui_priority),
|
||||
}
|
||||
result["w:styles"]["_children"].append({
|
||||
"w:latentStyles": {
|
||||
"_children": latent_children,
|
||||
**latent_attrs
|
||||
}
|
||||
})
|
||||
|
||||
# Все стили
|
||||
for style in self._styles.values():
|
||||
result["w:styles"]["_children"].append({"w:style": style.to_xml_dict()})
|
||||
|
||||
return result
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Styles count={len(self)} latent={len(self._latent_styles)}>"
|
||||
|
||||
# Создаём коллекцию стилей
|
||||
styles = Styles()
|
||||
|
||||
# Настройка latent-стилей (встроенные по умолчанию)
|
||||
styles.set_latent_defaults(count=276, def_semi_hidden=True, def_unhide_when_used=True)
|
||||
styles.define_latent_style("Normal", ui_priority=0, semi_hidden=False, unhide_when_used=False, q_format=True)
|
||||
styles.define_latent_style("Heading 1", ui_priority=9, q_format=True)
|
||||
|
||||
# Добавляем стили
|
||||
normal = styles.add_style("Normal", StyleType.PARAGRAPH, name="Normal", default=True)
|
||||
normal.quick_style = True
|
||||
normal.semi_hidden = False
|
||||
|
||||
heading1 = styles.add_style("Heading1", StyleType.PARAGRAPH, name="Heading 1")
|
||||
heading1.based_on = "Normal"
|
||||
heading1.priority = 9
|
||||
heading1.quick_style = True
|
||||
heading1.semi_hidden = False
|
||||
heading1.set_font({"b": {}, "sz": {"w:val": "28"}})
|
||||
|
||||
foobar = styles.add_style("Foobar", StyleType.PARAGRAPH, name="Foobar", custom_style=True)
|
||||
foobar.priority = 50
|
||||
foobar.semi_hidden = False
|
||||
foobar.quick_style = True
|
||||
|
||||
# Установка умолчаний документа
|
||||
styles.set_default_run_properties({
|
||||
"rFonts": {"w:asciiTheme": "minorHAnsi"},
|
||||
"sz": {"w:val": "24"}
|
||||
})
|
||||
styles.set_default_paragraph_properties({"spacing": {"w:after": "120"}})
|
||||
|
||||
# Получение стилей
|
||||
print(styles["Normal"]) # <Style id='Normal' ...>
|
||||
print(styles["Heading 1"]) # найдёт по имени
|
||||
|
||||
# Список рекомендованных
|
||||
for s in styles.recommended_list():
|
||||
print(s.name)
|
||||
|
||||
# Сортировка
|
||||
for s in styles.sorted_by_appearance():
|
||||
print(f"{s.name} (priority={s.priority})")
|
||||
|
||||
# Экспорт
|
||||
xml_data = styles.to_xml_dict()
|
||||
Reference in New Issue
Block a user