860 lines
29 KiB
Python
860 lines
29 KiB
Python
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() |