forked from hippolippo/Unity-Mod-Maker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMarkdownViewer.py
More file actions
321 lines (261 loc) · 14.1 KB
/
Copy pathMarkdownViewer.py
File metadata and controls
321 lines (261 loc) · 14.1 KB
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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
import os
import re
import json
import pyro
import webbrowser
from GraphicalInterface import * # Ensure this module is present and correctly implemented.
import tkinter
from tkinter import Tk, ttk, simpledialog, Toplevel
from tkinterweb import HtmlFrame
import markdown
import requests
def load_theme(filename):
with open(filename, 'r') as file:
data = json.load(file)
return data
def load_settings():
with open("settings.json", 'r') as file:
data = json.load(file)
return data
def refresh_theme():
global InterfaceMenu_Background, InterfaceMenu_Geometry, InterfaceMenu_NewButtonBackground
global InterfaceMenu_OpenButtonBackground, InterfaceMenu_MouseEnter, InterfaceMenu_MouseExit
global InterfaceMenu_ButtonConfigFG, InterfaceMenu_ButtonConfigBG, PyroPrompt_Background
global PyroPrompt_Foreground, PyroPrompt_WarningTextColor, PyroPrompt_Selected, Click, Hover
# Reload the theme data
settings = load_settings()
theme_data = load_theme('resources/themes/' + settings.get("Selected Theme", "Isle Goblin") + ".json")
# Update global theme variables
InterfaceMenu_Background = theme_data.get("interfacemenu", {}).get("background", "")
InterfaceMenu_Geometry = theme_data.get("interfacemenu", {}).get("geometry", "")
InterfaceMenu_NewButtonBackground = theme_data.get("interfacemenu", {}).get("newbuttonbackground", "")
InterfaceMenu_OpenButtonBackground = theme_data.get("interfacemenu", {}).get("openbuttonbackground", "")
InterfaceMenu_MouseEnter = theme_data.get("interfacemenu", {}).get("mouseenter", "")
InterfaceMenu_MouseExit = theme_data.get("interfacemenu", {}).get("mouseexit", "")
InterfaceMenu_ButtonConfigFG = theme_data.get("buttonconfig", {}).get("foreground", "")
InterfaceMenu_ButtonConfigBG = theme_data.get("buttonconfig", {}).get("background", "")
PyroPrompt_Background = theme_data.get("pyroprompt", {}).get("background", "")
PyroPrompt_Foreground = theme_data.get("pyroprompt", {}).get("foreground", "")
PyroPrompt_WarningTextColor = theme_data.get("pyroprompt", {}).get("warningtextcolor", "")
PyroPrompt_Selected = theme_data.get("pyroprompt", {}).get("selected", "")
Click = theme_data.get("click", "")
Hover = theme_data.get("hover", "")
class MarkdownViewer(Tk):
def __init__(self, markdown_url, backup_file=None):
refresh_theme()
super().__init__()
self.title("GMM Documentation")
self.geometry("1000x600")
self.iconbitmap("resources/goblin-mod-maker.ico")
self.configure(bg=InterfaceMenu_Background)
# Search variables
self.search_term = None
self.total_matches = 0
self.current_match = 0
self.search_popup = None # Popup for search results
# Configure grid layout
self.columnconfigure(0, weight=1) # Treeview
self.columnconfigure(1, weight=4) # HtmlFrame
self.rowconfigure(0, weight=1) # Single row
# Initialize the treeview
self.tree = ttk.Treeview(self, selectmode='browse')
self.tree.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
self.tree.heading("#0", text="Table of Contents", anchor="w")
self.tree.bind("<<TreeviewSelect>>", self.on_toc_select)
# Initialize Menubar
self.menubar = tkinter.Menu(self)
self.menubar.add_command(label="Search", accelerator="Ctrl + F", command=self.search)
self.config(menu=self.menubar)
self.bind("<Control-KeyPress-f>", self.search)
self.bind("<Control-KeyPress-F>", self.search)
# Create a frame for the HtmlFrame
html_frame_container = ttk.Frame(self)
html_frame_container.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)
# Initialize HtmlFrame inside the ttk.Frame
self.html_frame = HtmlFrame(html_frame_container, messages_enabled=False)
self.html_frame.on_link_click(self.handle_link_click)
self.html_frame.pack(fill="both", expand=True)
# Setup Treeview style
self.setup_treeview_style()
self.header_count = {}
self.file = backup_file # Sets the backup file in case of emergency
self.url = markdown_url
self.load_markdown()
mainloop()
def search(self, event=None):
"""Search for a term and initialize match navigation."""
self.search_term = simpledialog.askstring("Search", "Enter search term:")
if self.search_term:
self.total_matches = self.html_frame.find_text(self.search_term, highlight_all=True)
self.current_match = 0 # Start from the first match
if self.total_matches > 0:
self.show_search_popup()
self.jump_to_search_result(0) # Jump to the first result
else:
print(f"No matches found for '{self.search_term}'")
def show_search_popup(self):
"""Create and show the search results popup."""
if self.search_popup is not None:
self.search_popup.destroy()
self.search_popup = Toplevel(self)
self.search_popup.iconbitmap("resources/goblin-mod-maker.ico")
self.search_popup.title("Search Results")
self.search_popup.geometry("300x150")
self.search_popup.configure(background=PyroPrompt_Background)
default_font = self.option_get('Font', 'default')
# This won't center imma crashout
self.match_label = Label(self.search_popup, text=f"{self.current_match + 1}/{self.total_matches}", justify=CENTER, font=(default_font, 16), background=PyroPrompt_Background)
self.match_label.pack(pady=10, padx=5, side=TOP, fill=X)
# Previous button
prev_button = Button(self.search_popup, text="Previous", command=self.previous_match, relief=RAISED, background=InterfaceMenu_MouseEnter)
prev_button.pack(side=LEFT, padx=5, pady=10)
# Next button
next_button = Button(self.search_popup, text="Next", command=self.next_match, relief=RAISED, background=InterfaceMenu_MouseEnter)
next_button.pack(side=RIGHT, padx=5, pady=10)
# Close button
close_button = Button(self.search_popup, text="Close", command=self.close_search_popup, relief=RAISED, background=InterfaceMenu_MouseEnter)
close_button.pack(side=BOTTOM, pady=10)
def close_search_popup(self):
"""Close the search results popup."""
if self.search_popup is not None:
self.search_popup.destroy()
self.search_popup = None
def jump_to_search_result(self, index):
"""Jump to the search result at the given index."""
if self.total_matches > 0:
self.html_frame.find_text(self.search_term, select=index + 1, highlight_all=True)
self.current_match = index
if self.search_popup:
self.match_label.config(text=f"{self.current_match + 1}/{self.total_matches}")
def next_match(self):
"""Go to the next search result."""
if self.total_matches > 0:
self.current_match = (self.current_match + 1) % self.total_matches
self.jump_to_search_result(self.current_match)
def previous_match(self):
"""Go to the previous search result."""
if self.total_matches > 0:
self.current_match = (self.current_match - 1 + self.total_matches) % self.total_matches
self.jump_to_search_result(self.current_match)
def handle_link_click(self, url):
"""Handle link clicks. If the link is internal, scroll to the appropriate section."""
if "#" in url: # Check if the URL contains an internal anchor
element_id = url.split("#")[-1] # Extract the part after the '#'
element_id = self.create_safe_id(element_id)
if element_id: # Ensure the element ID is not empty
self.html_frame.yview_toelement(f'#{element_id}') # Scroll to the element with the given id
else:
print(f"Ignoring invalid anchor in URL: {url}")
elif url.startswith("http"): # External links
webbrowser.open(url) # Open in the default browser
else:
print(f"Ignoring invalid URL: {url}")
def setup_treeview_style(self):
# Create a style for the Treeview
style = ttk.Style()
style.theme_use("clam")
style.configure("Treeview",
background=PyroPrompt_Background, # Background color for Treeview items
foreground=PyroPrompt_Foreground, # Text color for Treeview items
fieldbackground=InterfaceMenu_MouseEnter, # Field background color
bordercolor=PyroPrompt_Background) # Border color
# Add hover effects and selection color
style.map("Treeview",
background=[("selected", PyroPrompt_Selected)], # Color when an item is selected
foreground=[("selected", PyroPrompt_Foreground)], # Text color when selected
fieldbackground=[("hover", InterfaceMenu_MouseEnter)], # Background on hover
highlightcolor=[("hover", "#45d6cf")]) # Highlight color on hover
# Ensure the headers and scrollbars match the theme
style.configure("Treeview.Heading",
background=PyroPrompt_Background, # Background for headers
foreground=PyroPrompt_Foreground, # Header text color
bordercolor=InterfaceMenu_MouseEnter,
relief="flat")
# Apply this style to the treeview
self.tree.configure(style="Treeview")
self.tree.tag_configure('row', background=InterfaceMenu_MouseEnter, foreground=PyroPrompt_Foreground)
style.configure("Treeview.Column",
background=PyroPrompt_Background, # Background for headers
foreground=PyroPrompt_Foreground, # Header text color
bordercolor=InterfaceMenu_MouseEnter,
relief="flat")
def load_markdown(self):
markdown_content = self.read_markdown_file(url=self.url, file=self.file)
html_content = self.convert_markdown_to_html(markdown_content)
self.html_frame.load_html(html_content)
self.generate_toc(markdown_content)
def read_markdown_file(self, url=None, file=None):
if url:
try:
response = requests.get(url)
response.raise_for_status() # Checks for errors
return response.text
except requests.RequestException as e:
print(f"Error fetching markdown from URL: {e}")
# Optionally fallback to file
if file:
try:
with open(file, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
print(f"File not found: {file}")
def convert_markdown_to_html(self, markdown_content):
modified_content = self.add_ids_to_headers(markdown_content)
# Resolve image paths to absolute paths before converting to HTML
modified_content = self.resolve_image_paths(modified_content)
return markdown.markdown(modified_content, extensions=['tables'])
def resolve_image_paths(self, markdown_content):
# This will only modify local image paths, keeping URLs intact
if not self.file:
return markdown_content # If no file, no need to modify paths
markdown_dir = os.path.dirname(os.path.abspath(self.file)) if self.file else os.getcwd()
def replace_image_paths(match):
image_path = match.group(2)
# Modify only if it's not an external URL
if not image_path.startswith("http"):
absolute_path = os.path.join(markdown_dir, image_path)
normalized_path = os.path.normpath(absolute_path)
file_url = 'file:///' + normalized_path.replace(os.path.sep, '/')
return f''
return match.group(0) # Leave web URLs unchanged
return re.sub(r'!\[(.*?)\]\((.*?)\)', replace_image_paths, markdown_content)
def add_ids_to_headers(self, markdown_content):
def add_id(match):
level = len(match.group(1))
header_text = match.group(2).strip()
safe_id = self.create_safe_id(header_text)
return f'<h{level} id="{safe_id}" style="color: #333;">{header_text}</h{level}>'
return re.sub(r'^(#{1,6})\s+(.+)', add_id, markdown_content, flags=re.MULTILINE)
def create_safe_id(self, header_text):
return re.sub(r'[\s\'.,]+', '-', re.sub(r'[\\/:]', '', header_text.lower()))
def generate_toc(self, markdown_content):
headers = re.findall(r'^(#{1,6})\s+(.+)', markdown_content, re.MULTILINE)
parent_stack = []
for header in headers:
level, text = len(header[0]), header[1].strip()
safe_id = self.make_unique_id(text)
while len(parent_stack) >= level:
parent_stack.pop()
current_parent = self.tree.insert(parent_stack[-1] if parent_stack else "", "end", text=text, iid=safe_id)
parent_stack.append(safe_id)
# Expand the first item in the TOC
if self.tree.get_children():
self.tree.item(self.tree.get_children()[0], open=True)
self.setup_treeview_style()
def make_unique_id(self, text):
safe_id = self.create_safe_id(text)
if safe_id in self.header_count:
self.header_count[safe_id] += 1
return f"{safe_id}-{self.header_count[safe_id]}"
else:
self.header_count[safe_id] = 1
return safe_id
def on_toc_select(self, event):
selected_item = self.tree.selection()
if selected_item:
self.html_frame.yview_toelement(f'#{selected_item[0]}')
print(selected_item[0])
# Commented this because everytime i open the app this pops up
if __name__ == "__main__":
markdown_file = "https://raw.githubusercontent.com/EchoTheDeveloper/Goblin-Mod-Maker/refs/heads/main/DOCUMENTATION.md"
MarkdownViewer(markdown_file, "DOCUMENTATION.md")