table + list
This commit is contained in:
@@ -23,17 +23,26 @@ def _render_inline_tokens(p, tokens, doc, images_dir, style_reg):
|
|||||||
elif token["type"] == "strong":
|
elif token["type"] == "strong":
|
||||||
run = p.add_run()
|
run = p.add_run()
|
||||||
run.bold = True
|
run.bold = True
|
||||||
run.text = token["children"][0]["raw"] if token["children"] else ""
|
# Обрабатываем children внутри этого же run
|
||||||
|
for child in token["children"]:
|
||||||
|
if child["type"] == "text":
|
||||||
|
run.text += child["raw"]
|
||||||
elif token["type"] == "emphasis":
|
elif token["type"] == "emphasis":
|
||||||
run = p.add_run()
|
run = p.add_run()
|
||||||
run.italic = True
|
run.italic = True
|
||||||
run.text = token["children"][0]["raw"] if token["children"] else ""
|
for child in token["children"]:
|
||||||
|
if child["type"] == "text":
|
||||||
|
run.text += child["raw"]
|
||||||
elif token["type"] == "link":
|
elif token["type"] == "link":
|
||||||
run = p.add_run()
|
run = p.add_run()
|
||||||
run.text = token["children"][0]["raw"] if token["children"] else ""
|
run.text = token["children"][0]["raw"] if token["children"] else ""
|
||||||
run.font.color.rgb = style_reg.get_color("LinkColor")
|
run.font.color.rgb = style_reg.get_color("LinkColor")
|
||||||
|
elif token["type"] == "softbreak" or token["type"] == "linebreak":
|
||||||
|
p.add_run().add_break()
|
||||||
|
elif token["type"] == "html":
|
||||||
|
if "<br" in token["raw"]:
|
||||||
|
p.add_run().add_break()
|
||||||
elif token["type"] == "image":
|
elif token["type"] == "image":
|
||||||
# Если image внутри параграфа — вызываем обработчик
|
|
||||||
handle_image(token, doc, images_dir, style_reg)
|
handle_image(token, doc, images_dir, style_reg)
|
||||||
|
|
||||||
@register_handler("paragraph")
|
@register_handler("paragraph")
|
||||||
@@ -48,37 +57,58 @@ def handle_image(node: dict, doc, images_dir: Path, style_reg):
|
|||||||
alt = node.get("alt", "")
|
alt = node.get("alt", "")
|
||||||
img_path = images_dir / src
|
img_path = images_dir / src
|
||||||
if img_path.exists():
|
if img_path.exists():
|
||||||
logger.debug(f"Обработка изображения: {img_path}, exists: {img_path.exists()}")
|
|
||||||
p = doc.add_paragraph()
|
p = doc.add_paragraph()
|
||||||
run = p.add_run()
|
run = p.add_run()
|
||||||
logger.debug(f"Добавлен параграф: {len(doc.paragraphs)} до add_picture")
|
|
||||||
try:
|
try:
|
||||||
run.add_picture(str(img_path), width=Inches(5))
|
run.add_picture(str(img_path), width=Inches(5))
|
||||||
logger.debug("add_picture успешно выполнен")
|
|
||||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при добавлении изображения {img_path}: {e}")
|
logger.error(f"Ошибка при добавлении изображения {img_path}: {e}")
|
||||||
doc.add_paragraph(f"[Изображение не загружено: {src}]")
|
doc.add_paragraph(f"[Изображение не загружено: {src}]")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug(f"После add_picture: {len(doc.paragraphs)} параграфов")
|
|
||||||
if alt:
|
if alt:
|
||||||
try:
|
try:
|
||||||
cap_p = doc.add_paragraph(alt, style="Caption")
|
cap_p = doc.add_paragraph(alt, style="Caption")
|
||||||
logger.debug(f"Добавлена подпись: {alt}, всего параграфов: {len(doc.paragraphs)}")
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
cap_p = doc.add_paragraph(alt)
|
cap_p = doc.add_paragraph(alt)
|
||||||
style_reg.apply_style(cap_p.style, "Caption")
|
style_reg.apply_style(cap_p.style, "Caption")
|
||||||
logger.debug(f"Добавлена подпись без стиля: {alt}")
|
logger.debug(f"Добавлена подпись без стиля: {alt}")
|
||||||
else:
|
else:
|
||||||
doc.add_paragraph(f"[Изображение не найдено: {src}]")
|
doc.add_paragraph(f"[Изображение не найдено: {src}]")
|
||||||
|
logger.error(f"[Изображение не найдено: {src}]")
|
||||||
|
|
||||||
@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("ordered", False)
|
||||||
for item in node["children"]:
|
for item in node["children"]:
|
||||||
p = doc.add_paragraph(style="List Number" if ordered else "List Bullet")
|
p = doc.add_paragraph(style="List Number" if ordered else "List Bullet")
|
||||||
for child in item["children"]:
|
_render_inline_tokens(p, item["children"], doc, images_dir, style_reg)
|
||||||
if child["type"] == "text":
|
|
||||||
p.add_run(child["raw"])
|
|
||||||
|
@register_handler("table")
|
||||||
|
def handle_table(node: dict, doc, images_dir: Path, style_reg):
|
||||||
|
rows_data = node["children"] # [header_row, row1, row2...]
|
||||||
|
if not rows_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
header_row = rows_data[0]["children"]
|
||||||
|
body_rows = rows_data[1:]
|
||||||
|
|
||||||
|
num_cols = len(header_row)
|
||||||
|
table = doc.add_table(rows=1, cols=num_cols)
|
||||||
|
table.style = "Table Grid" # можно настроить в YAML
|
||||||
|
|
||||||
|
# Заполняем заголовок
|
||||||
|
hdr_cells = table.rows[0].cells
|
||||||
|
for i, cell_data in enumerate(header_row):
|
||||||
|
if i < num_cols:
|
||||||
|
cell = hdr_cells[i]
|
||||||
|
_render_inline_tokens(cell.paragraphs[0], cell_data["children"], doc, images_dir, style_reg)
|
||||||
|
|
||||||
|
# Заполняем тело
|
||||||
|
for row_data in body_rows:
|
||||||
|
cells = table.add_row().cells
|
||||||
|
for i, cell_data in enumerate(row_data["children"]):
|
||||||
|
if i < num_cols:
|
||||||
|
cell = cells[i]
|
||||||
|
_render_inline_tokens(cell.paragraphs[0], cell_data["children"], doc, images_dir, style_reg)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from docx import Document
|
from docx import Document
|
||||||
from src.handlers.builtin import handle_heading, handle_paragraph, handle_image, handle_list
|
from src.handlers.builtin import handle_heading, handle_paragraph, handle_image, handle_list, handle_table
|
||||||
from src.core.style_registry import StyleRegistry
|
from src.core.style_registry import StyleRegistry
|
||||||
|
|
||||||
# Тесты для обработчиков
|
# Тесты для обработчиков
|
||||||
@@ -40,29 +40,6 @@ def test_handle_image():
|
|||||||
assert len(doc.paragraphs) == 2
|
assert len(doc.paragraphs) == 2
|
||||||
assert doc.paragraphs[1].text == "Логотип" # подпись
|
assert doc.paragraphs[1].text == "Логотип" # подпись
|
||||||
|
|
||||||
|
|
||||||
def test_debug_handle_image():
|
|
||||||
from PIL import Image
|
|
||||||
doc = Document()
|
|
||||||
style_reg = StyleRegistry(Path("resources/style_config.yaml"))
|
|
||||||
style_reg.ensure_styles_in_doc(doc)
|
|
||||||
|
|
||||||
# Проверим, есть ли стиль "Caption" в документе
|
|
||||||
print("Доступные стили:", [s.name for s in doc.styles if s.type.name == 'PARAGRAPH'])
|
|
||||||
assert "Caption" in [s.name for s in doc.styles if s.type.name == 'PARAGRAPH']
|
|
||||||
|
|
||||||
node = {"type": "image", "src": "logo.png", "alt": "Логотип"}
|
|
||||||
img_path = Path("tests/fixtures/images/logo.png")
|
|
||||||
img_path.parent.mkdir(exist_ok=True)
|
|
||||||
img = Image.new("RGB", (100, 100), color="red")
|
|
||||||
img.save(img_path)
|
|
||||||
|
|
||||||
handle_image(node, doc, Path("tests/fixtures"), style_reg)
|
|
||||||
|
|
||||||
print("Количество параграфов:", len(doc.paragraphs))
|
|
||||||
for i, p in enumerate(doc.paragraphs):
|
|
||||||
print(f"Параграф {i}: '{p.text}', стиль: {p.style.name if p.style else 'None'}")
|
|
||||||
|
|
||||||
def test_handle_paragraph_with_inline():
|
def test_handle_paragraph_with_inline():
|
||||||
doc = Document()
|
doc = Document()
|
||||||
style_reg = StyleRegistry(Path("resources/style_config.yaml"))
|
style_reg = StyleRegistry(Path("resources/style_config.yaml"))
|
||||||
@@ -88,3 +65,87 @@ def test_handle_paragraph_with_inline():
|
|||||||
assert runs[3].text == "курсив"
|
assert runs[3].text == "курсив"
|
||||||
assert runs[3].italic is True
|
assert runs[3].italic is True
|
||||||
assert runs[4].text == "."
|
assert runs[4].text == "."
|
||||||
|
|
||||||
|
def test_handle_list():
|
||||||
|
doc = Document()
|
||||||
|
style_reg = StyleRegistry(Path("resources/style_config.yaml"))
|
||||||
|
node = {
|
||||||
|
"type": "list",
|
||||||
|
"ordered": False,
|
||||||
|
"children": [
|
||||||
|
{"type": "list_item", "children": [{"type": "text", "raw": "Элемент 1"}]},
|
||||||
|
{"type": "list_item", "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()
|
||||||
|
style_reg = StyleRegistry(Path("resources/style_config.yaml"))
|
||||||
|
node = {
|
||||||
|
"type": "table",
|
||||||
|
"children": [
|
||||||
|
{ # header
|
||||||
|
"type": "table_row",
|
||||||
|
"children": [
|
||||||
|
{"type": "table_cell", "children": [{"type": "text", "raw": "Заголовок 1"}]},
|
||||||
|
{"type": "table_cell", "children": [{"type": "text", "raw": "Заголовок 2"}]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ # row 1
|
||||||
|
"type": "table_row",
|
||||||
|
"children": [
|
||||||
|
{"type": "table_cell", "children": [{"type": "text", "raw": "Ячейка 1"}]},
|
||||||
|
{"type": "table_cell", "children": [{"type": "text", "raw": "Ячейка 2"}]},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
handle_table(node, doc, Path("."), style_reg)
|
||||||
|
|
||||||
|
assert len(doc.tables) == 1
|
||||||
|
table = doc.tables[0]
|
||||||
|
assert len(table.rows) == 2
|
||||||
|
assert table.cell(0, 0).text == "Заголовок 1"
|
||||||
|
assert table.cell(1, 1).text == "Ячейка 2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_table_with_br():
|
||||||
|
doc = Document()
|
||||||
|
style_reg = StyleRegistry(Path("resources/style_config.yaml"))
|
||||||
|
node = {
|
||||||
|
"type": "table",
|
||||||
|
"children": [
|
||||||
|
{ # header
|
||||||
|
"type": "table_row",
|
||||||
|
"children": [
|
||||||
|
{"type": "table_cell", "children": [{"type": "text", "raw": "Заголовок 1"}]},
|
||||||
|
{"type": "table_cell", "children": [{"type": "text", "raw": "Заголовок 2"}]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ # row 1
|
||||||
|
"type": "table_row",
|
||||||
|
"children": [
|
||||||
|
{"type": "table_cell", "children": [
|
||||||
|
{"type": "text", "raw": "Первая строка"},
|
||||||
|
{"type": "softbreak"}, # или linebreak/html
|
||||||
|
{"type": "text", "raw": "Вторая строка"}
|
||||||
|
]},
|
||||||
|
{"type": "table_cell", "children": [{"type": "text", "raw": "Ячейка 2"}]},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
handle_table(node, doc, Path("."), style_reg)
|
||||||
|
|
||||||
|
assert len(doc.tables) == 1
|
||||||
|
table = doc.tables[0]
|
||||||
|
cell_text = table.cell(1, 0).text # "Первая строка\nВторая строка"
|
||||||
|
assert "Первая строка" in cell_text
|
||||||
|
assert "Вторая строка" in cell_text
|
||||||
|
|||||||
Reference in New Issue
Block a user