File size: 11,661 Bytes
a97d040
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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 格式如下:

      <div style="text-align:center">
          <img src="{png_path}" alt="Outline" style="width:100%;"/>
      </div>
      <div style="text-align:center">
          Fig 1. The outline of the {survey_title}
      </div>

    函数返回更新后的 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'<div style="text-align:center">\n'
        f'    <img src="{png_path_fixed}" alt="Outline" style="width:100%;"/>\n'
        f'</div>\n'
        f'<div style="text-align:center">\n'
        f'    Fig 1. The outline of the {survey_title}\n'
        f'</div>\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)