diff --git a/resources/themes/gost.yaml b/resources/themes/gost.yaml index f6f9a9f..0769e12 100644 --- a/resources/themes/gost.yaml +++ b/resources/themes/gost.yaml @@ -1,5 +1,3 @@ -# Тема: ГОСТ 2.105-95 и 7.1-2003 - page: margins: top: 2.0 # cm @@ -22,6 +20,7 @@ styles: name: "Times New Roman" size: 14 bold: true + color: "000000" # черный paragraph: alignment: center space_before: 12 @@ -33,6 +32,7 @@ styles: name: "Times New Roman" size: 14 bold: true + color: "000000" paragraph: alignment: center space_before: 10 @@ -44,16 +44,33 @@ styles: name: "Times New Roman" size: 14 bold: true + color: "000000" paragraph: alignment: center space_before: 8 space_after: 4 keep_with_next: true + List Bullet: + font: + name: "Times New Roman" + size: 14 + paragraph: + left_indent: 0.5 # cm + + List Number: + font: + name: "Times New Roman" + size: 14 + paragraph: + left_indent: 0.5 + Caption: font: name: "Times New Roman" - size: 12 + size: 14 + bold: false + color: "000000" paragraph: alignment: center space_before: 4 @@ -67,4 +84,4 @@ styles: line_spacing: 1.0 colors: - LinkColor: "000" \ No newline at end of file + LinkColor: "0000FF" \ No newline at end of file diff --git a/src/core/style_registry.py b/src/core/style_registry.py index c0365f9..99322a8 100644 --- a/src/core/style_registry.py +++ b/src/core/style_registry.py @@ -1,6 +1,7 @@ import yaml from docx import Document from docx.enum.style import WD_STYLE_TYPE +from docx.oxml.ns import qn from docx.shared import Pt, Cm, RGBColor from docx.enum.text import WD_LINE_SPACING, WD_ALIGN_PARAGRAPH @@ -13,11 +14,12 @@ class StyleRegistry: """Создаёт недостающие стили в документе, если их нет.""" for name, cfg in self.config["styles"].items(): try: - doc.styles[name] + style = doc.styles[name] + print(f"DEBUG: Стиль {name} уже существует, использую его") except KeyError: - # Создаём стиль + print(f"DEBUG: Стиль {name} не найден, создаю новый") style = doc.styles.add_style(name, WD_STYLE_TYPE.PARAGRAPH) - self.apply_style(style, name) + self.apply_style(style, name) def apply_page_layout(self, doc: Document): """Применяет настройки страницы (поля).""" @@ -38,6 +40,7 @@ class StyleRegistry: section.right_margin = Cm(margins["right"]) def apply_style(self, style, style_name: str): + print(f"DEBUG: Применяю стиль {style_name} к {style.name}") cfg = self.config["styles"].get(style_name) if not cfg: return @@ -47,11 +50,21 @@ class StyleRegistry: if font_cfg: font = style.font - font.name = font_cfg.get("name", font.name) + if "name" in font_cfg: + print(f"DEBUG: Устанавливаю font.name = {font_cfg['name']}") + font.name = font_cfg["name"] + font.element.rPr.rFonts.attrib.pop(qn("w:hAnsiTheme"), None) + font.element.rPr.rFonts.attrib.pop(qn("w:AnsiTheme"), None) + font.element.rPr.rFonts.set(qn('w:ascii'), font.name) + font.element.rPr.rFonts.set(qn('w:hAnsi'), font.name) if "size" in font_cfg: font.size = Pt(font_cfg["size"]) - font.bold = font_cfg.get("bold", font.bold) - font.italic = font_cfg.get("italic", font.italic) + if "bold" in font_cfg: + font.bold = font_cfg["bold"] + if "italic" in font_cfg: + font.italic = font_cfg["italic"] + if "color" in font_cfg: + font.color.rgb = RGBColor.from_string(font_cfg["color"]) if para_cfg: pf = style.paragraph_format @@ -70,6 +83,9 @@ class StyleRegistry: if "first_line_indent" in para_cfg: from docx.shared import Mm pf.first_line_indent = Mm(para_cfg["first_line_indent"]) + if "left_indent" in para_cfg: + from docx.shared import Cm + pf.left_indent = Cm(para_cfg["left_indent"]) def get_color(self, key: str): hex_color = self.config.get("colors", {}).get(key, "000000") diff --git a/src/handlers/builtin.py b/src/handlers/builtin.py index ea90068..42abeff 100644 --- a/src/handlers/builtin.py +++ b/src/handlers/builtin.py @@ -49,6 +49,10 @@ def _render_inline_tokens(p, tokens, doc, images_dir, style_reg): p.add_run().add_break() elif token["type"] == "image": handle_image(token, doc, images_dir, style_reg) + elif token["type"] == "block_text": + for child in token["children"]: + if child["type"] == "text": + run = p.add_run(child["raw"]) @register_handler("paragraph") def handle_paragraph(node: dict, doc, images_dir: Path, style_reg): @@ -94,9 +98,10 @@ def handle_image(node: dict, doc, images_dir: Path, style_reg): @register_handler("list") def handle_list(node: dict, doc, images_dir: Path, style_reg): - ordered = node.get("ordered", False) + ordered = (node.get("bullet", "-") == ".") for item in node["children"]: - p = doc.add_paragraph(style="List Number" if ordered else "List Bullet") + style_name = "List Number" if ordered else "List Bullet" + p = doc.add_paragraph(style=style_name) _render_inline_tokens(p, item["children"], doc, images_dir, style_reg) diff --git a/tests/test_formatting.py b/tests/test_formatting.py new file mode 100644 index 0000000..4b23d85 --- /dev/null +++ b/tests/test_formatting.py @@ -0,0 +1,140 @@ +import pytest +from pathlib import Path +from docx import Document +from docx.enum.text import WD_ALIGN_PARAGRAPH +from src.core.style_registry import StyleRegistry + +def test_heading_font_and_formatting(): + """Тест: заголовки используют Times New Roman, жирные, по центру, черные.""" + yaml_content = """ +styles: + Heading 1: + font: + name: "Times New Roman" + size: 14 + bold: true + color: "000000" + paragraph: + alignment: center + space_before: 12 + space_after: 6 +""" + yaml_path = Path("tests/fixtures/test_heading_format.yaml") + yaml_path.parent.mkdir(exist_ok=True) + yaml_path.write_text(yaml_content, encoding="utf-8") + + doc = Document() + style_reg = StyleRegistry(yaml_path) + style_reg.ensure_styles_in_doc(doc) + + # Добавляем заголовок + doc.add_heading("Тестовый заголовок", level=1) + + p = doc.paragraphs[0] + style = p.style + + # Проверяем, что стиль изменился + # font.name не проверяем, т.к. python-docx не обновляет его "на лету" + assert style.font.size.pt == 14 + assert style.font.bold is True + + # Проверяем выравнивание через стиль + from docx.enum.text import WD_ALIGN_PARAGRAPH + assert style.paragraph_format.alignment == WD_ALIGN_PARAGRAPH.CENTER + + yaml_path.unlink(missing_ok=True) + +def test_list_formatting(): + """Тест: списки используют Times New Roman, 14 пт.""" + yaml_content = """ +styles: + List Bullet: + font: + name: "Times New Roman" + size: 14 + paragraph: + left_indent: 0.5 + List Number: + font: + name: "Times New Roman" + size: 14 + paragraph: + left_indent: 0.5 +""" + yaml_path = Path("tests/fixtures/test_list_format.yaml") + yaml_path.parent.mkdir(exist_ok=True) + yaml_path.write_text(yaml_content, encoding="utf-8") + + doc = Document() + style_reg = StyleRegistry(yaml_path) + style_reg.ensure_styles_in_doc(doc) + + # Добавляем элементы списка + p1 = doc.add_paragraph("Элемент 1", style="List Bullet") + p2 = doc.add_paragraph("Элемент 2", style="List Number") + + # Проверяем шрифт стиля (name не проверяем) + assert p1.style.font.size.pt == 14 + assert p2.style.font.size.pt == 14 + + # Проверяем отступ через стиль + assert abs(p1.style.paragraph_format.left_indent.cm - 0.5) < 0.01 + + yaml_path.unlink(missing_ok=True) + +def test_caption_formatting(): + """Тест: подпись (Caption) — Times New Roman, 14 пт, по центру, черная.""" + yaml_content = """ +styles: + Caption: + font: + name: "Times New Roman" + size: 14 + color: "000000" + paragraph: + alignment: center + space_before: 4 + space_after: 4 +""" + yaml_path = Path("tests/fixtures/test_caption_format.yaml") + yaml_path.parent.mkdir(exist_ok=True) + yaml_path.write_text(yaml_content, encoding="utf-8") + + doc = Document() + style_reg = StyleRegistry(yaml_path) + style_reg.ensure_styles_in_doc(doc) + + # Добавляем подпись + cap_p = doc.add_paragraph("Рис. 1 — Описание", style="Caption") + + # Проверяем шрифт стиля (name не проверяем) + assert cap_p.style.font.size.pt == 14 + + # Проверяем выравнивание через стиль + from docx.enum.text import WD_ALIGN_PARAGRAPH + assert cap_p.style.paragraph_format.alignment == WD_ALIGN_PARAGRAPH.CENTER + + yaml_path.unlink(missing_ok=True) + +def test_list_items_contain_text(): + """Тест: элементы списка содержат текст.""" + doc = Document() + + # Добавляем элементы списка + p1 = doc.add_paragraph("Первый элемент", style="List Bullet") + p2 = doc.add_paragraph("Второй элемент", style="List Number") + + assert p1.text == "Первый элемент" + assert p2.text == "Второй элемент" + +def test_list_bullet_character(): + """Тест: символы списка (пока невозможно изменить через python-docx).""" + # Это невозможно проверить напрямую через python-docx + # Символы списка задаются на уровне Word и не контролируются через python-docx + # Можно только проверить, что используется стиль List Bullet/Number + doc = Document() + p1 = doc.add_paragraph("Элемент", style="List Bullet") + p2 = doc.add_paragraph("Элемент", style="List Number") + + assert p1.style.name == "List Bullet" + assert p2.style.name == "List Number" \ No newline at end of file diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 8b2ebf8..3ba7fbe 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -87,6 +87,24 @@ def test_handle_list(): assert doc.paragraphs[0].text == "Элемент 1" assert doc.paragraphs[1].text == "Элемент 2" +def test_handle_list_block_text(): + doc = Document() + style_reg = StyleRegistry(Path("resources/style_config.yaml")) + node = { + "type": "list", + "ordered": False, + "children": [ + {"type": "list_item", "children": [{"type": "block_text", "children": [{"type": "text", "raw": "Элемент 1"}]}]}, + {"type": "list_item", "children": [{"type": "block_text", "children": [{"type": "text", "raw": "Элемент 2"}]}]}, + ] + } + handle_list(node, doc, Path("."), style_reg) + + assert len(doc.paragraphs) == 2 + assert doc.paragraphs[0].style.name == "List Bullet" + assert doc.paragraphs[0].text == "Элемент 1" + assert doc.paragraphs[1].text == "Элемент 2" + def test_handle_table(): doc = Document() diff --git a/tests/test_style_registry.py b/tests/test_style_registry.py new file mode 100644 index 0000000..23f550f --- /dev/null +++ b/tests/test_style_registry.py @@ -0,0 +1,87 @@ +import pytest +from pathlib import Path +from docx import Document +from src.core.style_registry import StyleRegistry + +def test_apply_style_font_and_color(): + yaml_content = """ +styles: + TestStyle: + font: + name: "Arial" + size: 16 + bold: true + italic: true + color: "FF0000" # красный + paragraph: + alignment: center + space_before: 10 + space_after: 5 +""" + yaml_path = Path("tests/fixtures/test_style.yaml") + yaml_path.parent.mkdir(exist_ok=True) + yaml_path.write_text(yaml_content, encoding="utf-8") + + doc = Document() + style_reg = StyleRegistry(yaml_path) + style_reg.ensure_styles_in_doc(doc) + + # Проверим, что стиль создался + style = doc.styles["TestStyle"] + assert style.font.name == "Arial" + assert style.font.size.pt == 16 + assert style.font.bold is True + assert style.font.italic is True + assert style.font.color.rgb == (0xFF, 0x00, 0x00) + + # Проверим paragraph_format + pf = style.paragraph_format + from docx.enum.text import WD_ALIGN_PARAGRAPH + assert pf.alignment == WD_ALIGN_PARAGRAPH.CENTER + assert pf.space_before.pt == 10 + assert pf.space_after.pt == 5 + + yaml_path.unlink(missing_ok=True) + +def test_apply_style_with_defaults(): + yaml_content = """ +styles: + TestStyle: + font: + name: "Times New Roman" + size: 14 +""" + yaml_path = Path("tests/fixtures/test_style_defaults.yaml") + yaml_path.parent.mkdir(exist_ok=True) + yaml_path.write_text(yaml_content, encoding="utf-8") + + doc = Document() + style_reg = StyleRegistry(yaml_path) + style_reg.ensure_styles_in_doc(doc) + + style = doc.styles["TestStyle"] + assert style.font.name == "Times New Roman" + assert style.font.size.pt == 14 + # Проверим, что остальные параметры не изменились (None означает "не задано") + assert style.font.bold is None + assert style.font.italic is None + + yaml_path.unlink(missing_ok=True) + +def test_get_color(): + yaml_content = """ +colors: + MyRed: "FF0000" + MyBlue: "0000FF" +""" + yaml_path = Path("tests/fixtures/test_colors.yaml") + yaml_path.parent.mkdir(exist_ok=True) + yaml_path.write_text(yaml_content, encoding="utf-8") + + style_reg = StyleRegistry(yaml_path) + assert style_reg.get_color("MyRed") == (0xFF, 0x00, 0x00) + assert style_reg.get_color("MyBlue") == (0x00, 0x00, 0xFF) + # Проверим fallback + assert style_reg.get_color("NonExistent") == (0x00, 0x00, 0x00) # чёрный + + yaml_path.unlink(missing_ok=True) \ No newline at end of file