fix(writer): 防静默丢章告警 + 占位符正则宽容化(大小写/全角冒号)
This commit is contained in:
@@ -48,6 +48,28 @@ def test_section_placeholder_replaced(tmp_path):
|
||||
assert "以下がDB表定义です。" in full_text
|
||||
|
||||
|
||||
def test_section_placeholder_case_insensitive(tmp_path):
|
||||
# 宽容:docx 中锚点为 {{Section:id}}(大写键)也能注入
|
||||
tpl = _make_template(tmp_path, "{{Section:db_design}}")
|
||||
inj = DocxInjector(tpl)
|
||||
out = inj.inject({"db_design": _section_blocks()}, {"doc_title": "概要設計書"})
|
||||
|
||||
full_text = "\n".join(p.text for p in out.paragraphs)
|
||||
assert "{{Section:db_design}}" not in full_text
|
||||
assert "3.1 テーブル一覧" in full_text
|
||||
|
||||
|
||||
def test_section_placeholder_fullwidth_colon(tmp_path):
|
||||
# 宽容:docx 中锚点为 {{section:id}}(全角冒号)也能注入
|
||||
tpl = _make_template(tmp_path, "{{section:db_design}}")
|
||||
inj = DocxInjector(tpl)
|
||||
out = inj.inject({"db_design": _section_blocks()}, {"doc_title": "概要設計書"})
|
||||
|
||||
full_text = "\n".join(p.text for p in out.paragraphs)
|
||||
assert "{{section:db_design}}" not in full_text
|
||||
assert "3.1 テーブル一覧" in full_text
|
||||
|
||||
|
||||
# ---------- 行内占位符替换 ----------
|
||||
|
||||
def test_inline_meta_replaced(tmp_path):
|
||||
|
||||
@@ -30,3 +30,15 @@ def test_map_template_falls_back_without_placeholder():
|
||||
specs = map_template(parsed)
|
||||
assert specs[0].chapter_id == "chapter_1"
|
||||
assert specs[0].section_placeholder is None
|
||||
|
||||
|
||||
def test_map_template_section_re_case_insensitive():
|
||||
# 宽容:占位符键名大小写不敏感(解析端已归一,防御性双保险)
|
||||
sections = [
|
||||
ChapterMarker(type="heading", name="画面一覧", level=1),
|
||||
ChapterMarker(type="placeholder", name="Section:2", level=0),
|
||||
]
|
||||
parsed = ParsedTemplate(file_name="t.docx", sections=sections, placeholders={}, styles={})
|
||||
specs = map_template(parsed)
|
||||
assert specs[0].chapter_id == "2"
|
||||
assert specs[0].section_placeholder == "Section:2"
|
||||
|
||||
@@ -83,3 +83,28 @@ def test_generate_delegates_to_factory_when_engine_none(monkeypatch, tmp_path):
|
||||
assert len(contents) == 1
|
||||
loaded = Document(str(out))
|
||||
assert "自动生成的内容" in "\n".join(p.text for p in loaded.paragraphs)
|
||||
|
||||
|
||||
def test_generate_warns_on_unanchored_heading(tmp_path, caplog):
|
||||
# 防静默丢章:无 {{section:id}} 锚点的章 → 打 WARNING 并列出章名
|
||||
tpl = tmp_path / "tpl.docx"
|
||||
out = tmp_path / "out.docx"
|
||||
doc = Document()
|
||||
doc.add_paragraph("附録", style="Heading 1")
|
||||
doc.save(str(tpl))
|
||||
parsed = ParsedTemplate(
|
||||
file_name=str(tpl),
|
||||
sections=[ChapterMarker(type="heading", name="附録", level=1)],
|
||||
placeholders={},
|
||||
styles={"defined": ["Heading 1"], "used": ["Heading 1"]},
|
||||
)
|
||||
orch = WriteOrchestrator()
|
||||
with caplog.at_level("WARNING", logger="genesis.writer.orchestrator"):
|
||||
orch.generate(
|
||||
SimpleNamespace(template=parsed),
|
||||
str(out),
|
||||
samples_dir="nonexistent_dir_xyz",
|
||||
engine=FakeEngine(),
|
||||
template_path=str(tpl),
|
||||
)
|
||||
assert any("附録" in r.message for r in caplog.records)
|
||||
|
||||
@@ -104,3 +104,29 @@ def test_parse_styles_collected(tmp_path):
|
||||
assert "Heading 1" in result.styles["used"]
|
||||
assert "Normal" in result.styles["used"]
|
||||
assert "Heading 1" in result.styles["defined"]
|
||||
|
||||
|
||||
def test_parse_extracts_placeholder_case_insensitive(tmp_path):
|
||||
# 宽容:键名大小写不敏感 → 归一为小写 section:id
|
||||
doc = new_document()
|
||||
doc.add_paragraph("{{Section:2}}")
|
||||
path = save_document(tmp_path, doc)
|
||||
|
||||
result = WordTemplateParser().parse(path)
|
||||
|
||||
assert result.placeholders == {"section:2": "{{Section:2}}"}
|
||||
ph = [s for s in result.sections if s.type == "placeholder"]
|
||||
assert [s.name for s in ph] == ["section:2"]
|
||||
|
||||
|
||||
def test_parse_extracts_placeholder_fullwidth_colon(tmp_path):
|
||||
# 宽容:全角冒号(:)也视为分隔符 → 归一为半角 section:id
|
||||
doc = new_document()
|
||||
doc.add_paragraph("{{section:2}}")
|
||||
path = save_document(tmp_path, doc)
|
||||
|
||||
result = WordTemplateParser().parse(path)
|
||||
|
||||
assert result.placeholders == {"section:2": "{{section:2}}"}
|
||||
ph = [s for s in result.sections if s.type == "placeholder"]
|
||||
assert [s.name for s in ph] == ["section:2"]
|
||||
|
||||
Reference in New Issue
Block a user