[AI]
ГОСТ
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
# Тема: ГОСТ 2.105-95 и 7.1-2003
|
|
||||||
|
|
||||||
page:
|
page:
|
||||||
margins:
|
margins:
|
||||||
top: 2.0 # cm
|
top: 2.0 # cm
|
||||||
@@ -22,6 +20,7 @@ styles:
|
|||||||
name: "Times New Roman"
|
name: "Times New Roman"
|
||||||
size: 14
|
size: 14
|
||||||
bold: true
|
bold: true
|
||||||
|
color: "000000" # черный
|
||||||
paragraph:
|
paragraph:
|
||||||
alignment: center
|
alignment: center
|
||||||
space_before: 12
|
space_before: 12
|
||||||
@@ -33,6 +32,7 @@ styles:
|
|||||||
name: "Times New Roman"
|
name: "Times New Roman"
|
||||||
size: 14
|
size: 14
|
||||||
bold: true
|
bold: true
|
||||||
|
color: "000000"
|
||||||
paragraph:
|
paragraph:
|
||||||
alignment: center
|
alignment: center
|
||||||
space_before: 10
|
space_before: 10
|
||||||
@@ -44,16 +44,33 @@ styles:
|
|||||||
name: "Times New Roman"
|
name: "Times New Roman"
|
||||||
size: 14
|
size: 14
|
||||||
bold: true
|
bold: true
|
||||||
|
color: "000000"
|
||||||
paragraph:
|
paragraph:
|
||||||
alignment: center
|
alignment: center
|
||||||
space_before: 8
|
space_before: 8
|
||||||
space_after: 4
|
space_after: 4
|
||||||
keep_with_next: true
|
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:
|
Caption:
|
||||||
font:
|
font:
|
||||||
name: "Times New Roman"
|
name: "Times New Roman"
|
||||||
size: 12
|
size: 14
|
||||||
|
bold: false
|
||||||
|
color: "000000"
|
||||||
paragraph:
|
paragraph:
|
||||||
alignment: center
|
alignment: center
|
||||||
space_before: 4
|
space_before: 4
|
||||||
@@ -67,4 +84,4 @@ styles:
|
|||||||
line_spacing: 1.0
|
line_spacing: 1.0
|
||||||
|
|
||||||
colors:
|
colors:
|
||||||
LinkColor: "000"
|
LinkColor: "0000FF"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import yaml
|
import yaml
|
||||||
from docx import Document
|
from docx import Document
|
||||||
from docx.enum.style import WD_STYLE_TYPE
|
from docx.enum.style import WD_STYLE_TYPE
|
||||||
|
from docx.oxml.ns import qn
|
||||||
from docx.shared import Pt, Cm, RGBColor
|
from docx.shared import Pt, Cm, RGBColor
|
||||||
from docx.enum.text import WD_LINE_SPACING, WD_ALIGN_PARAGRAPH
|
from docx.enum.text import WD_LINE_SPACING, WD_ALIGN_PARAGRAPH
|
||||||
|
|
||||||
@@ -13,9 +14,10 @@ class StyleRegistry:
|
|||||||
"""Создаёт недостающие стили в документе, если их нет."""
|
"""Создаёт недостающие стили в документе, если их нет."""
|
||||||
for name, cfg in self.config["styles"].items():
|
for name, cfg in self.config["styles"].items():
|
||||||
try:
|
try:
|
||||||
doc.styles[name]
|
style = doc.styles[name]
|
||||||
|
print(f"DEBUG: Стиль {name} уже существует, использую его")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# Создаём стиль
|
print(f"DEBUG: Стиль {name} не найден, создаю новый")
|
||||||
style = doc.styles.add_style(name, WD_STYLE_TYPE.PARAGRAPH)
|
style = doc.styles.add_style(name, WD_STYLE_TYPE.PARAGRAPH)
|
||||||
self.apply_style(style, name)
|
self.apply_style(style, name)
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ class StyleRegistry:
|
|||||||
section.right_margin = Cm(margins["right"])
|
section.right_margin = Cm(margins["right"])
|
||||||
|
|
||||||
def apply_style(self, style, style_name: str):
|
def apply_style(self, style, style_name: str):
|
||||||
|
print(f"DEBUG: Применяю стиль {style_name} к {style.name}")
|
||||||
cfg = self.config["styles"].get(style_name)
|
cfg = self.config["styles"].get(style_name)
|
||||||
if not cfg:
|
if not cfg:
|
||||||
return
|
return
|
||||||
@@ -47,11 +50,21 @@ class StyleRegistry:
|
|||||||
|
|
||||||
if font_cfg:
|
if font_cfg:
|
||||||
font = style.font
|
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:
|
if "size" in font_cfg:
|
||||||
font.size = Pt(font_cfg["size"])
|
font.size = Pt(font_cfg["size"])
|
||||||
font.bold = font_cfg.get("bold", font.bold)
|
if "bold" in font_cfg:
|
||||||
font.italic = font_cfg.get("italic", font.italic)
|
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:
|
if para_cfg:
|
||||||
pf = style.paragraph_format
|
pf = style.paragraph_format
|
||||||
@@ -70,6 +83,9 @@ class StyleRegistry:
|
|||||||
if "first_line_indent" in para_cfg:
|
if "first_line_indent" in para_cfg:
|
||||||
from docx.shared import Mm
|
from docx.shared import Mm
|
||||||
pf.first_line_indent = Mm(para_cfg["first_line_indent"])
|
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):
|
def get_color(self, key: str):
|
||||||
hex_color = self.config.get("colors", {}).get(key, "000000")
|
hex_color = self.config.get("colors", {}).get(key, "000000")
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ def _render_inline_tokens(p, tokens, doc, images_dir, style_reg):
|
|||||||
p.add_run().add_break()
|
p.add_run().add_break()
|
||||||
elif token["type"] == "image":
|
elif token["type"] == "image":
|
||||||
handle_image(token, doc, images_dir, style_reg)
|
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")
|
@register_handler("paragraph")
|
||||||
def handle_paragraph(node: dict, doc, images_dir: Path, style_reg):
|
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")
|
@register_handler("list")
|
||||||
def handle_list(node: dict, doc, images_dir: Path, style_reg):
|
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"]:
|
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)
|
_render_inline_tokens(p, item["children"], doc, images_dir, style_reg)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
140
tests/test_formatting.py
Normal file
140
tests/test_formatting.py
Normal file
@@ -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"
|
||||||
@@ -87,6 +87,24 @@ def test_handle_list():
|
|||||||
assert doc.paragraphs[0].text == "Элемент 1"
|
assert doc.paragraphs[0].text == "Элемент 1"
|
||||||
assert doc.paragraphs[1].text == "Элемент 2"
|
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():
|
def test_handle_table():
|
||||||
doc = Document()
|
doc = Document()
|
||||||
|
|||||||
87
tests/test_style_registry.py
Normal file
87
tests/test_style_registry.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user