马一丁

Update demo program

1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 """ 2 """
3 -生成覆盖全部允许block类型的演示 IR,用于验证 HTML 与 PDF 渲染。 3 +生成覆盖全部允许block类型的演示 IR,用于验证 HTML / PDF / Markdown 渲染。
4 4
5 执行后会在 `final_reports/ir` 写入一份带时间戳的 IR, 5 执行后会在 `final_reports/ir` 写入一份带时间戳的 IR,
6 -并分别在 `final_reports/html` 与 `final_reports/pdf` 输出对应的渲染文件。 6 +并分别在 `final_reports/html`、`final_reports/pdf` 与 `final_reports/md`
  7 +输出对应的渲染文件。
7 """ 8 """
8 9
9 from __future__ import annotations 10 from __future__ import annotations
@@ -21,7 +22,7 @@ if str(ROOT) not in sys.path: @@ -21,7 +22,7 @@ if str(ROOT) not in sys.path:
21 from ReportEngine.core import DocumentComposer 22 from ReportEngine.core import DocumentComposer
22 from ReportEngine.ir import IRValidator 23 from ReportEngine.ir import IRValidator
23 from ReportEngine.ir.schema import ENGINE_AGENT_TITLES 24 from ReportEngine.ir.schema import ENGINE_AGENT_TITLES
24 -from ReportEngine.renderers import HTMLRenderer, PDFRenderer 25 +from ReportEngine.renderers import HTMLRenderer, MarkdownRenderer, PDFRenderer
25 from ReportEngine.utils.config import settings 26 from ReportEngine.utils.config import settings
26 27
27 28
@@ -508,6 +509,49 @@ def build_chapters() -> list[dict]: @@ -508,6 +509,49 @@ def build_chapters() -> list[dict]:
508 ], 509 ],
509 }, 510 },
510 } 511 }
  512 + horizontal_bar_chart_block = {
  513 + "type": "widget",
  514 + "widgetId": "demo-horizontal-voice",
  515 + "widgetType": "chart.js/bar",
  516 + "props": {
  517 + # 通过 indexAxis 切换横向柱状图
  518 + "type": "bar",
  519 + "options": {
  520 + "indexAxis": "y",
  521 + "plugins": {"legend": {"position": "right"}},
  522 + "scales": {"x": {"title": {"display": True, "text": "提及量(万)"}}},
  523 + },
  524 + },
  525 + "data": {
  526 + "labels": ["微博", "短视频", "社区论坛", "新闻客户端"],
  527 + "datasets": [
  528 + {
  529 + "label": "声量对比",
  530 + "data": [42, 58, 27, 36],
  531 + "backgroundColor": ["#2ecc71", "#3498db", "#9b59b6", "#f39c12"],
  532 + }
  533 + ],
  534 + },
  535 + }
  536 + pie_chart_block = {
  537 + "type": "widget",
  538 + "widgetId": "demo-stance-pie",
  539 + "widgetType": "chart.js/pie",
  540 + "props": {
  541 + "type": "pie",
  542 + "options": {"plugins": {"legend": {"position": "bottom"}}},
  543 + },
  544 + "data": {
  545 + "labels": ["支持", "中立", "质疑"],
  546 + "datasets": [
  547 + {
  548 + "label": "立场分布",
  549 + "data": [36, 28, 21],
  550 + "backgroundColor": ["#27ae60", "#f1c40f", "#c0392b"],
  551 + }
  552 + ],
  553 + },
  554 + }
511 doughnut_chart_block = { 555 doughnut_chart_block = {
512 "type": "widget", 556 "type": "widget",
513 "widgetId": "demo-sentiment-share", 557 "widgetId": "demo-sentiment-share",
@@ -713,12 +757,14 @@ def build_chapters() -> list[dict]: @@ -713,12 +757,14 @@ def build_chapters() -> list[dict]:
713 "type": "paragraph", 757 "type": "paragraph",
714 "inlines": [ 758 "inlines": [
715 { 759 {
716 - "text": "折线/柱状/饼图/雷达/极区/散点/气泡等多类型图表,用于验证 Chart.js 兼容性。", 760 + "text": "折线 / 柱状(含横向、堆叠)/ 饼图 / 圆环 / 雷达 / 极区 / 散点 / 气泡等多类型图表,用于验证 Chart.js 兼容性。",
717 } 761 }
718 ], 762 ],
719 }, 763 },
720 widget_block, 764 widget_block,
721 stacked_bar_chart_block, 765 stacked_bar_chart_block,
  766 + horizontal_bar_chart_block,
  767 + pie_chart_block,
722 doughnut_chart_block, 768 doughnut_chart_block,
723 radar_chart_block, 769 radar_chart_block,
724 polar_area_chart_block, 770 polar_area_chart_block,
@@ -768,14 +814,16 @@ def validate_chapters(chapters: list[dict]) -> None: @@ -768,14 +814,16 @@ def validate_chapters(chapters: list[dict]) -> None:
768 raise ValueError(f"{chapter.get('chapterId', 'unknown')} 校验失败: {errors}") 814 raise ValueError(f"{chapter.get('chapterId', 'unknown')} 校验失败: {errors}")
769 815
770 816
771 -def render_and_save(document_ir: dict, timestamp: str) -> tuple[Path, Path, Path]:  
772 - """将 IR 保存为 JSON,并渲染 HTML / PDF,返回三个路径。""" 817 +def render_and_save(document_ir: dict, timestamp: str) -> tuple[Path, Path, Path, Path]:
  818 + """将 IR 保存为 JSON,并渲染 HTML / PDF / Markdown,返回四个路径。"""
773 ir_dir = Path(settings.DOCUMENT_IR_OUTPUT_DIR) 819 ir_dir = Path(settings.DOCUMENT_IR_OUTPUT_DIR)
774 html_dir = Path(settings.OUTPUT_DIR) / "html" 820 html_dir = Path(settings.OUTPUT_DIR) / "html"
775 pdf_dir = Path(settings.OUTPUT_DIR) / "pdf" 821 pdf_dir = Path(settings.OUTPUT_DIR) / "pdf"
  822 + md_dir = Path(settings.OUTPUT_DIR) / "md"
776 ir_dir.mkdir(parents=True, exist_ok=True) 823 ir_dir.mkdir(parents=True, exist_ok=True)
777 html_dir.mkdir(parents=True, exist_ok=True) 824 html_dir.mkdir(parents=True, exist_ok=True)
778 pdf_dir.mkdir(parents=True, exist_ok=True) 825 pdf_dir.mkdir(parents=True, exist_ok=True)
  826 + md_dir.mkdir(parents=True, exist_ok=True)
779 827
780 ir_path = ir_dir / f"report_ir_all_blocks_demo_{timestamp}.json" 828 ir_path = ir_dir / f"report_ir_all_blocks_demo_{timestamp}.json"
781 ir_path.write_text(json.dumps(document_ir, ensure_ascii=False, indent=2), encoding="utf-8") 829 ir_path.write_text(json.dumps(document_ir, ensure_ascii=False, indent=2), encoding="utf-8")
@@ -789,7 +837,12 @@ def render_and_save(document_ir: dict, timestamp: str) -> tuple[Path, Path, Path @@ -789,7 +837,12 @@ def render_and_save(document_ir: dict, timestamp: str) -> tuple[Path, Path, Path
789 pdf_path = pdf_dir / f"report_pdf_all_blocks_demo_{timestamp}.pdf" 837 pdf_path = pdf_dir / f"report_pdf_all_blocks_demo_{timestamp}.pdf"
790 pdf_renderer.render_to_pdf(document_ir, pdf_path) 838 pdf_renderer.render_to_pdf(document_ir, pdf_path)
791 839
792 - return ir_path, html_path, pdf_path 840 + md_renderer = MarkdownRenderer()
  841 + md_content = md_renderer.render(document_ir, ir_file_path=str(ir_path))
  842 + md_path = md_dir / f"report_md_all_blocks_demo_{timestamp}.md"
  843 + md_path.write_text(md_content, encoding="utf-8")
  844 +
  845 + return ir_path, html_path, pdf_path, md_path
793 846
794 847
795 def main() -> int: 848 def main() -> int:
@@ -817,12 +870,13 @@ def main() -> int: @@ -817,12 +870,13 @@ def main() -> int:
817 composer = DocumentComposer() 870 composer = DocumentComposer()
818 document_ir = composer.build_document(report_id, metadata, chapters) 871 document_ir = composer.build_document(report_id, metadata, chapters)
819 872
820 - ir_path, html_path, pdf_path = render_and_save(document_ir, timestamp) 873 + ir_path, html_path, pdf_path, md_path = render_and_save(document_ir, timestamp)
821 874
822 print("✅ 演示 IR 生成完成") 875 print("✅ 演示 IR 生成完成")
823 print(f"IR: {ir_path}") 876 print(f"IR: {ir_path}")
824 print(f"HTML: {html_path}") 877 print(f"HTML: {html_path}")
825 print(f"PDF: {pdf_path}") 878 print(f"PDF: {pdf_path}")
  879 + print(f"MD: {md_path}")
826 return 0 880 return 0
827 881
828 882
@@ -13,8 +13,10 @@ @@ -13,8 +13,10 @@
13 - pie (饼图) 13 - pie (饼图)
14 - doughnut (圆环图) 14 - doughnut (圆环图)
15 - radar (雷达图) 15 - radar (雷达图)
16 -- polarArea (极地区域图) 16 +- polararea (极地区域图)
17 - scatter (散点图) 17 - scatter (散点图)
  18 +- bubble (气泡图)
  19 +- horizontalbar (横向柱状图)
18 """ 20 """
19 21
20 from __future__ import annotations 22 from __future__ import annotations
@@ -66,18 +68,18 @@ class ChartValidator: @@ -66,18 +68,18 @@ class ChartValidator:
66 68
67 # 支持的图表类型 69 # 支持的图表类型
68 SUPPORTED_CHART_TYPES = { 70 SUPPORTED_CHART_TYPES = {
69 - 'line', 'bar', 'pie', 'doughnut', 'radar', 'polarArea', 'scatter',  
70 - 'bubble', 'horizontalBar' 71 + 'line', 'bar', 'pie', 'doughnut', 'radar', 'polararea', 'scatter',
  72 + 'bubble', 'horizontalbar'
71 } 73 }
72 74
73 # 需要labels的图表类型 75 # 需要labels的图表类型
74 LABEL_REQUIRED_TYPES = { 76 LABEL_REQUIRED_TYPES = {
75 - 'line', 'bar', 'radar', 'polarArea', 'pie', 'doughnut' 77 + 'line', 'bar', 'radar', 'polararea', 'pie', 'doughnut'
76 } 78 }
77 79
78 # 需要数值数据的图表类型 80 # 需要数值数据的图表类型
79 NUMERIC_DATA_TYPES = { 81 NUMERIC_DATA_TYPES = {
80 - 'line', 'bar', 'radar', 'polarArea', 'pie', 'doughnut' 82 + 'line', 'bar', 'radar', 'polararea', 'pie', 'doughnut'
81 } 83 }
82 84
83 # 需要特殊数据格式的图表类型 85 # 需要特殊数据格式的图表类型