feat(export): 新增 PDF/DOCX 导出与 StaticRenderer 内部契约
- 新增 PdfExporter(reportlab)与 DocxExporter(python-docx),实现与 HtmlExporter 一致的同步 render + 异步 export,v1 文本优先(标题/段落/ 行内强调与链接/列表/引用/表格/代码块/数学文本),function_plot 与 mermaid 保留源码占位并记 warning。 - service 层加 _EXPORTERS 注册表按格式分发,删除 format!=html 硬限制, 扩展名/MIME/产物清理泛化到 html/pdf/docx 三种格式。 - 新增 app/plot/renderer.py:StaticRenderRequest + StaticRenderer Protocol + FunctionPlotStaticRenderer + MermaidStaticRenderer;HtmlExporter 改经 FunctionPlotStaticRenderer 消费,去除对 render_svg 的直接依赖。 - 补齐 PDF/DOCX 魔法字节、CJK 字体、占位 warning 与 StaticRenderer 契约测试。 - 更新 Export开发说明.md。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,11 @@ from app.export.exporters.html import HtmlExporter
|
||||
from app.export.markdown import parse_document
|
||||
from app.plot.parser import PlotParseError, evaluate, parse_expression, parse_source
|
||||
from app.plot.render import render_svg
|
||||
from app.plot.renderer import (
|
||||
FunctionPlotStaticRenderer,
|
||||
MermaidStaticRenderer,
|
||||
StaticRenderRequest,
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -181,12 +186,12 @@ def test_render_svg_nonfinite_range_falls_back() -> None:
|
||||
|
||||
def test_html_exporter_function_plot_render_error_falls_back(monkeypatch) -> None:
|
||||
# P2:渲染异常不阻断整篇导出,回退占位并记 warning
|
||||
import app.export.exporters.html as html_mod
|
||||
import app.plot.renderer as renderer_mod
|
||||
|
||||
def boom(plot):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(html_mod, "render_svg", boom)
|
||||
monkeypatch.setattr(renderer_mod, "render_svg", boom)
|
||||
md = "```function-plot\ny = x\n```"
|
||||
result = asyncio.run(HtmlExporter().export(parse_document(md), ExportOptions()))
|
||||
html = result.content.decode("utf-8")
|
||||
@@ -283,3 +288,37 @@ def test_html_exporter_limits_total_plot_nodes(monkeypatch) -> None:
|
||||
assert html.count('<figure class="function-plot">') == 1
|
||||
assert html.count('<pre class="function-plot">') == 1
|
||||
assert any("累计复杂度" in w for w in result.warnings)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# StaticRenderer 内部契约(§10.4)
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_function_plot_static_renderer_renders_svg() -> None:
|
||||
renderer = FunctionPlotStaticRenderer()
|
||||
result = renderer.render(StaticRenderRequest(kind="function_plot", source="y = x^2"))
|
||||
assert "<svg" in result.content
|
||||
assert "<polyline" in result.content
|
||||
assert result.mime_type == "image/svg+xml"
|
||||
assert result.width == 640
|
||||
assert result.height == 480
|
||||
|
||||
|
||||
def test_function_plot_static_renderer_parse_exposes_node_count() -> None:
|
||||
renderer = FunctionPlotStaticRenderer()
|
||||
parsed = renderer.parse(StaticRenderRequest(kind="function_plot", source="y = x + x"))
|
||||
assert parsed.plot is not None
|
||||
assert parsed.plot.node_count > 0
|
||||
|
||||
|
||||
def test_function_plot_static_renderer_raises_on_no_plot() -> None:
|
||||
renderer = FunctionPlotStaticRenderer()
|
||||
request = StaticRenderRequest(kind="function_plot", source="y = os.system('x')")
|
||||
with pytest.raises(ValueError):
|
||||
renderer.render(request)
|
||||
|
||||
|
||||
def test_mermaid_static_renderer_returns_placeholder() -> None:
|
||||
renderer = MermaidStaticRenderer()
|
||||
result = renderer.render(StaticRenderRequest(kind="mermaid", source="graph LR"))
|
||||
assert result.content == ""
|
||||
assert any("mermaid" in w for w in result.warnings)
|
||||
|
||||
Reference in New Issue
Block a user