import json import re import textwrap from graphviz import Digraph import os def wrap_text(text, max_chars): """ 对文本进行自动换行包装,每行最大字符数为 max_chars。 """ return textwrap.fill(text, width=max_chars) def parse_md_refs(md_content): """ 解析 Markdown 内容,提取以 x.y.z 格式标题对应的引用。 对于每个满足格式的 section,其内容中所有形如 [数字] 的引用 将被抽取出来,去重后按数字升序排序,生成类似 "[1,2,3]" 的引用字符串。 如果遇到 undesired header(如 "6 Future Directions" 或 "7 Conclusion"), 则停止后续内容的解析,确保最后一个 section 仅包含到该 header 之前的内容。 返回字典,键为 section 编号(例如 "3.1.1"),值为引用字符串(例如 "[1,2,3]")。 """ ref_dict = {} # 处理 Markdown 内容(按行拆分) lines = md_content.split("\n") if md_content else [] # 匹配 Markdown 标题中以 x.y.z 开头的叶节点(例如 "5.1.1 Neural Topic...") section_header_regex = re.compile(r'^\s*#+\s*(\d+\.\d+\.\d+).*') # 匹配 undesired header,如 "6 Future Directions" 或 "7 Conclusion" undesired_header_regex = re.compile(r'^\s*#+\s*(6 Future Directions|7 Conclusion)\b') # 匹配引用,例如 [数字] ref_pattern = re.compile(r'\[(\d+)\]') current_section = None current_content = [] for line in lines: # 如果检测到 undesired header,则先处理当前 section,再退出循环 if undesired_header_regex.match(line): break header_match = section_header_regex.match(line) if header_match: # 处理上一个 section if current_section is not None: all_refs = [int(num) for content_line in current_content for num in ref_pattern.findall(content_line)] if all_refs: ref_dict[current_section] = "[" + ",".join(map(str, sorted(set(all_refs)))) + "]" # 更新当前 section current_section = header_match.group(1) current_content = [] else: if current_section is not None: current_content.append(line) # 处理最后一个 section if current_section is not None and current_content: all_refs = [int(num) for content_line in current_content for num in ref_pattern.findall(content_line)] if all_refs: ref_dict[current_section] = "[" + ",".join(map(str, sorted(set(all_refs)))) + "]" return ref_dict def generate_graphviz_png(json_path, output_png_path, md_content=None, title="Document Outline", max_root_chars=20): """ 从 JSON 文件中读取大纲,构造树状结构,并生成 mindmap 的 PNG 图片。 如果提供了 md_content,则根据 Markdown 内容中以 x.y.z 格式标题对应的引用, 在生成 mindmap 时,对于叶节点(没有子节点且标题以 x.y.z 开头)的标签, 在原文本后追加一个换行,然后添加引用信息(例如 "[1,2,3]"), 且引用经过数字排序。 同时,仅对根节点文本进行自动换行包装,以限制根节点的最大宽度, 其它节点保持原始文本格式。 参数: json_path: JSON 文件路径(包含大纲) output_png_path: 输出 PNG 文件路径(不带后缀) md_content: Markdown 文本内容(字符串),可选 title: 用于替换 mindmap 中根节点的标题,默认 "Document Outline" max_root_chars: 限制根节点每行最大字符数,默认 20 """ # 解析 Markdown 内容的引用 ref_dict = parse_md_refs(md_content) if md_content else {} # 读取 JSON 大纲 with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) outline_str = data.get("outline", "") # 解析形如 [层级, '标题'] 的项 pattern = re.compile(r"\[(\d+),\s*'([^']+)'\]") items = pattern.findall(outline_str) items = [(int(level), title) for level, title in items] # 不需要的标题关键词 undesired_keywords = {"Abstract", "Introduction", "Future Directions", "Conclusion"} # 过滤掉不需要的条目 filtered_items = [ (lvl, title) for lvl, title in items if not re.match(r"^\d+\s+(.+)", title) or re.match(r"^\d+\s+(.+)", title).group(1) not in undesired_keywords ] # 构造树状结构 tree = [] stack = [] for lvl, title_item in filtered_items: node = {"title": title_item, "children": []} while stack and lvl <= stack[-1][0]: stack.pop() if stack: stack[-1][1]["children"].append(node) else: tree.append(node) stack.append((lvl, node)) # 生成 Mindmap dot = Digraph(comment=title, format='png', engine='dot') dot.graph_attr.update(rankdir='LR', splines='ortho', bgcolor='white', dpi="150") dot.attr('node', shape='box', style='rounded,filled', fillcolor='white', color='gray') dot.edge_attr.update(arrowhead='none', color="black") # 处理根节点 wrapped_title = wrap_text(title, max_root_chars) dot.node('root', label=wrapped_title, shape='ellipse', style='filled', fillcolor='lightgray') node_counter = [0] section_pattern = re.compile(r'^(\d+\.\d+\.\d+)') def add_nodes(node, parent_id): current_id = f'node_{node_counter[0]}' node_counter[0] += 1 safe_label = node['title'].replace('"', r'\"') # 如果是叶节点且标题以 x.y.z 开头,则追加引用信息(如果存在) if not node["children"]: m = section_pattern.match(safe_label) if m: section_id = m.group(1) if section_id in ref_dict: safe_label += "\n" + ref_dict[section_id] dot.node(current_id, label=safe_label) dot.edge(parent_id, current_id) for child in node.get("children", []): add_nodes(child, current_id) for top_node in tree: add_nodes(top_node, "root") dot.render(output_png_path, cleanup=True) print("生成 PNG 文件:", output_png_path + ".png") return output_png_path + ".png" def insert_outline_image(png_path, md_content, survey_title): """ 在给定的 Markdown 内容字符串中查找 "2 Introduction" 这一行, 然后在该位置之前插入 outline 图片的 HTML 代码块,确保渲染时 HTML 块与后续 Markdown 内容间有足够空行分隔开。 参数: png_path: 要插入的 PNG 图片路径,将作为 img 的 src 属性值。 md_content: Markdown 文件内容字符串。 survey_title: 用于生成图片说明文字的问卷标题。 插入的 HTML 格式如下:
Outline
Fig 1. The outline of the {survey_title}
函数返回更新后的 Markdown 内容字符串。 """ # 将 Markdown 内容字符串分割成行(保留换行符) lines = md_content.splitlines(keepends=True) print(lines) # 查找包含 "2 Introduction" 的行的索引 intro_index = None for i, line in enumerate(lines): if '2 Introduction' in line: intro_index = i break if intro_index is None: print("没有找到 '2 Introduction' 这一行!") return md_content # 确保路径中的反斜杠被替换成正斜杠 png_path_fixed = png_path.replace("\\", "/") # 构造需要插入的 HTML 代码块,在前后增加空行 html_snippet = ( "\n\n" # 添加换行确保与上文/下文分隔 f'
\n' f' Outline\n' f'
\n' f'
\n' f' Fig 1. The outline of the {survey_title}\n' f'
\n' "\n" # 再添加一个空行确保与下方内容分隔 ) print(f"将在第 {intro_index} 行插入如下 HTML 代码块(插入在 '2 Introduction' 之前):\n{html_snippet}") # 在找到的 "2 Introduction" 这一行之前插入 html_snippet lines.insert(intro_index, html_snippet) # 合并所有行,构造更新后的 Markdown 内容 updated_md = "".join(lines) print("已在 Markdown 内容中插入 outline 图片。") return updated_md def insert_outline_figure(png_path, tex_content, survey_title): """ 在给定的 TeX 文件内容字符串中查找 "2 Introduction" 这一行, 然后在其之前插入一个跨页(双栏)的 figure* 环境,包括整页显示的图片。 它将生成类似如下 LaTeX 片段: \begin{figure*}[htbp] \centering \includegraphics[width=\textwidth]{path/to/xxx.png} \caption{Fig 1. The outline of the XXX} \end{figure*} 参数: png_path: 要插入的 PNG 图片路径 tex_content: TeX 文件内容字符串 survey_title: 用于生成图片 caption 的文献/问卷标题 返回: 更新后的 TeX 文本字符串 """ # 将 TeX 内容逐行分割(保留换行符) lines = tex_content.splitlines(keepends=True) # 查找包含 "2 Introduction" 的行索引 intro_index = None for i, line in enumerate(lines): if 'Introduction' in line: intro_index = i break # 如果找不到,就直接返回原文 if intro_index is None: print("没有找到 'Introduction' 这一行,未执行插入。") return tex_content # 构造 TeX 的 figure* 代码块 # 为确保整页,可用 [p] 或者 [htbp],具体可根据排版需要调整 # 也可替换成普通 \begin{figure} ... \end{figure},如果不需要跨双栏 figure_block = ( "\n" # 加一个空行,确保与上文分隔 "\\begin{figure*}[htbp]\n" " \\centering\n" f" \\includegraphics[width=\\textwidth]{{{png_path}}}\n" f" \\caption{{The outline of our survey: {survey_title}}}\n" "\\end{figure*}\n\n" # 再留一个空行分隔 ) # 在找到的 "2 Introduction" 所在行之前插入 figure 环境 lines.insert(intro_index, figure_block) # 重新拼接所有行 updated_tex = "".join(lines) return updated_tex # 使用示例: # if __name__ == "__main__": # png_path = 'src/static/data/info/test_4/outline.png' # md_content = '' # survey_title = "My Survey Title" # updated_md = insert_outline_image(png_path, md_content, survey_title) # -------------------------- # 使用示例 # -------------------------- if __name__ == "__main__": json_path = os.path.join("src", "static", "data", "txt", 'test_2', "outline.json") output_png_path = os.path.join("src", "static", "data", "info", 'test_2', "outline") md_path = os.path.join("src", "static", "data", "info", 'test_2', f"survey_{'test_2'}_processed.md") flowchart_results_path = os.path.join("src", "static", "data", "info", 'test_2', "flowchart_results.json") png_path = generate_graphviz_png( json_path=json_path, output_png_path=output_png_path, md_path=md_path, title='test', max_root_chars=30 ) # generate_graphviz_png(json_file_path, output_png_file, md_file_path, title=mindmap_title, max_root_chars=20)