完善模板编辑与模型管理体验
This commit is contained in:
@@ -56,6 +56,34 @@ def _copy_run_format(target_run, source_paragraph: Paragraph | None):
|
||||
break
|
||||
|
||||
|
||||
def _extract_first_run_format(source_paragraph: Paragraph | None):
|
||||
if source_paragraph is None:
|
||||
return None
|
||||
for source_run in source_paragraph.runs:
|
||||
if source_run._element.rPr is not None:
|
||||
return deepcopy(source_run._element.rPr)
|
||||
return None
|
||||
|
||||
|
||||
def _set_paragraph_text(
|
||||
paragraph: Paragraph,
|
||||
text: str,
|
||||
style_name: str | None = None,
|
||||
template_paragraph: Paragraph | None = None,
|
||||
):
|
||||
run_format = _extract_first_run_format(template_paragraph)
|
||||
_clear_paragraph(paragraph)
|
||||
if style_name:
|
||||
try:
|
||||
paragraph.style = style_name
|
||||
except Exception:
|
||||
pass
|
||||
if text:
|
||||
run = paragraph.add_run(text)
|
||||
if run_format is not None:
|
||||
run._element.insert(0, run_format)
|
||||
|
||||
|
||||
def _append_paragraph_after(
|
||||
paragraph: Paragraph,
|
||||
text: str,
|
||||
@@ -166,16 +194,12 @@ def _find_heading_paragraph(document: DocumentObject, heading_text: str, after_e
|
||||
return None
|
||||
|
||||
|
||||
def _replace_section_content(document: DocumentObject, heading_title: str, content: dict, after_element=None):
|
||||
heading = _find_heading_paragraph(document, heading_title, after_element)
|
||||
if heading is None:
|
||||
return after_element
|
||||
|
||||
def _collect_section_templates(heading: Paragraph):
|
||||
first_body_style = None
|
||||
paragraph_template = None
|
||||
table_template = None
|
||||
blocks = []
|
||||
current = heading._element.getnext()
|
||||
blocks_to_remove = []
|
||||
while current is not None:
|
||||
if isinstance(current, CT_P):
|
||||
current_paragraph = Paragraph(current, heading._parent)
|
||||
@@ -185,49 +209,132 @@ def _replace_section_content(document: DocumentObject, heading_title: str, conte
|
||||
first_body_style = current_paragraph.style.name
|
||||
if paragraph_template is None:
|
||||
paragraph_template = current_paragraph
|
||||
blocks_to_remove.append(current_paragraph)
|
||||
blocks.append(current_paragraph)
|
||||
elif isinstance(current, CT_Tbl):
|
||||
current_table = Table(current, heading._parent)
|
||||
if table_template is None:
|
||||
table_template = current_table
|
||||
blocks_to_remove.append(current_table)
|
||||
blocks.append(current_table)
|
||||
current = current.getnext()
|
||||
return first_body_style, paragraph_template, table_template, blocks
|
||||
|
||||
for block in blocks_to_remove:
|
||||
_delete_block(block)
|
||||
|
||||
insert_after = heading
|
||||
def _insert_content_after(
|
||||
insert_after: Paragraph,
|
||||
content: dict,
|
||||
first_body_style: str | None,
|
||||
paragraph_template: Paragraph | None,
|
||||
table_template: Table | None,
|
||||
):
|
||||
current_anchor: Paragraph = insert_after
|
||||
content_blocks = content.get("content", [])
|
||||
for block in content_blocks:
|
||||
block_type = block.get("type")
|
||||
if block_type == "table":
|
||||
rows = [list(row) for row in block.get("rows", [])]
|
||||
headers = block.get("headers") or []
|
||||
table = _append_table_after(insert_after, rows, headers, table_template)
|
||||
insert_after = _append_empty_paragraph_after_table(table, first_body_style)
|
||||
table = _append_table_after(current_anchor, rows, headers, table_template)
|
||||
current_anchor = _append_empty_paragraph_after_table(table, first_body_style)
|
||||
else:
|
||||
text = block.get("text", "")
|
||||
text_parts = [item for item in text.split("\n") if item] or [text]
|
||||
for text_part in text_parts:
|
||||
insert_after = _append_paragraph_after(
|
||||
insert_after,
|
||||
current_anchor = _append_paragraph_after(
|
||||
current_anchor,
|
||||
text_part,
|
||||
first_body_style,
|
||||
paragraph_template,
|
||||
)
|
||||
return current_anchor
|
||||
|
||||
|
||||
def _replace_section_content(
|
||||
document: DocumentObject,
|
||||
anchor_title: str,
|
||||
target_title: str,
|
||||
content: dict,
|
||||
write_mode: str,
|
||||
after_element=None,
|
||||
):
|
||||
heading = _find_heading_paragraph(document, anchor_title, after_element)
|
||||
if heading is None:
|
||||
return after_element
|
||||
_set_paragraph_text(heading, target_title, heading.style.name if heading.style is not None else None, heading)
|
||||
first_body_style, paragraph_template, table_template, blocks_to_remove = _collect_section_templates(heading)
|
||||
|
||||
if write_mode == "replace_heading_only":
|
||||
return heading._element
|
||||
|
||||
if write_mode == "replace_section":
|
||||
for block in blocks_to_remove:
|
||||
_delete_block(block)
|
||||
|
||||
_insert_content_after(
|
||||
heading,
|
||||
content,
|
||||
first_body_style,
|
||||
paragraph_template,
|
||||
table_template,
|
||||
)
|
||||
return heading._element
|
||||
|
||||
|
||||
def _group_logs(logs: list[dict]) -> list[list[dict]]:
|
||||
groups: list[list[dict]] = []
|
||||
for item in logs:
|
||||
anchor_title = item.get("anchor_title") or item.get("title") or ""
|
||||
if not groups:
|
||||
groups.append([item])
|
||||
continue
|
||||
last_group = groups[-1]
|
||||
last_anchor = last_group[0].get("anchor_title") or last_group[0].get("title") or ""
|
||||
if anchor_title == last_anchor:
|
||||
last_group.append(item)
|
||||
else:
|
||||
groups.append([item])
|
||||
return groups
|
||||
|
||||
|
||||
def _replace_section_group(
|
||||
document: DocumentObject,
|
||||
items: list[dict],
|
||||
after_element=None,
|
||||
):
|
||||
first_item = items[0]
|
||||
anchor_title = first_item.get("anchor_title") or first_item.get("title") or ""
|
||||
target_title = first_item.get("title") or anchor_title
|
||||
heading = _find_heading_paragraph(document, anchor_title, after_element)
|
||||
if heading is None:
|
||||
return after_element
|
||||
|
||||
_set_paragraph_text(heading, target_title, heading.style.name if heading.style is not None else None, heading)
|
||||
first_body_style, paragraph_template, table_template, blocks_to_remove = _collect_section_templates(heading)
|
||||
|
||||
if len(items) == 1 and first_item.get("write_mode") == "replace_heading_only":
|
||||
return heading._element
|
||||
|
||||
preserve_existing = len(items) == 1 and first_item.get("write_mode") == "append_after_heading"
|
||||
if not preserve_existing:
|
||||
for block in blocks_to_remove:
|
||||
_delete_block(block)
|
||||
|
||||
current_anchor = heading
|
||||
for item in items:
|
||||
current_anchor = _insert_content_after(
|
||||
current_anchor,
|
||||
item.get("content") or {"content": []},
|
||||
first_body_style,
|
||||
paragraph_template,
|
||||
table_template,
|
||||
)
|
||||
return heading._element
|
||||
|
||||
|
||||
def export_document_bytes(template_bytes: bytes, logs: list[dict]) -> bytes:
|
||||
document = Document(BytesIO(template_bytes))
|
||||
last_heading_element = None
|
||||
for item in logs:
|
||||
last_heading_element = _replace_section_content(
|
||||
document,
|
||||
item["title"],
|
||||
item["content"],
|
||||
last_heading_element,
|
||||
)
|
||||
for group in _group_logs(logs):
|
||||
last_heading_element = _replace_section_group(document, group, last_heading_element)
|
||||
|
||||
output = BytesIO()
|
||||
document.save(output)
|
||||
|
||||
Reference in New Issue
Block a user