Spaces:
Running
Running
Upload md_to_smf.py
Browse files- md_to_smf.py +144 -0
md_to_smf.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tkinter as tk
|
| 2 |
+
from tkinter import scrolledtext
|
| 3 |
+
from tkinterdnd2 import DND_FILES, TkinterDnD
|
| 4 |
+
import re
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
# --- Conversion Logic ---
|
| 8 |
+
|
| 9 |
+
def convert_md_to_smf(markdown_text):
|
| 10 |
+
"""
|
| 11 |
+
Converts a Markdown string to an SMF-compatible BBCode string.
|
| 12 |
+
|
| 13 |
+
The order of replacements is important to avoid conflicts.
|
| 14 |
+
"""
|
| 15 |
+
smf_text = markdown_text
|
| 16 |
+
|
| 17 |
+
# 1. Block elements (Code blocks, blockquotes)
|
| 18 |
+
# Code blocks (```...```) - DOTALL flag allows `.` to match newlines
|
| 19 |
+
smf_text = re.sub(r'```(.*?)```', r'[code]\1[/code]', smf_text, flags=re.DOTALL)
|
| 20 |
+
|
| 21 |
+
# Blockquotes (>)
|
| 22 |
+
smf_text = re.sub(r'^\> (.*)', r'[quote]\1[/quote]', smf_text, flags=re.MULTILINE)
|
| 23 |
+
|
| 24 |
+
# 2. Headings (must be at the start of a line)
|
| 25 |
+
smf_text = re.sub(r'^# (.*)', r'[size=18pt][b]\1[/b][/size]', smf_text, flags=re.MULTILINE)
|
| 26 |
+
smf_text = re.sub(r'^## (.*)', r'[size=16pt][b]\1[/b][/size]', smf_text, flags=re.MULTILINE)
|
| 27 |
+
smf_text = re.sub(r'^### (.*)', r'[size=14pt][b]\1[/b][/size]', smf_text, flags=re.MULTILINE)
|
| 28 |
+
smf_text = re.sub(r'^#### (.*)', r'[size=14pt][b]\1[/b][/size]', smf_text, flags=re.MULTILINE)
|
| 29 |
+
|
| 30 |
+
# 3. Lists (must be at the start of a line)
|
| 31 |
+
# This is a simplified conversion. It doesn't wrap the whole block in [list] tags,
|
| 32 |
+
# as that requires more complex state management, but it's often sufficient.
|
| 33 |
+
smf_text = re.sub(r'^\* (.*)', r'[li]\1[/li]', smf_text, flags=re.MULTILINE)
|
| 34 |
+
smf_text = re.sub(r'^- (.*)', r'[li]\1[/li]', smf_text, flags=re.MULTILINE)
|
| 35 |
+
smf_text = re.sub(r'^\d+\. (.*)', r'[li]\1[/li]', smf_text, flags=re.MULTILINE) # Note: SMF may need [list type=decimal] wrapper
|
| 36 |
+
|
| 37 |
+
# 4. Images and Links
|
| 38 |
+
smf_text = re.sub(r'!\[.*?\]\((.*?)\)', r'[img]\1[/img]', smf_text) # Images
|
| 39 |
+
smf_text = re.sub(r'\[(.*?)\]\((.*?)\)', r'[url=\2]\1[/url]', smf_text) # Links
|
| 40 |
+
|
| 41 |
+
# 5. Inline formatting
|
| 42 |
+
smf_text = re.sub(r'\*\*(.*?)\*\*', r'[b]\1[/b]', smf_text) # Bold (**)
|
| 43 |
+
smf_text = re.sub(r'__(.*?)__', r'[b]\1[/b]', smf_text) # Bold (__)
|
| 44 |
+
smf_text = re.sub(r'\*(.*?)\*', r'[i]\1[/i]', smf_text) # Italic (*)
|
| 45 |
+
smf_text = re.sub(r'_(.*?)_', r'[i]\1[/i]', smf_text) # Italic (_)
|
| 46 |
+
smf_text = re.sub(r'~~(.*?)~~', r'[s]\1[/s]', smf_text) # Strikethrough
|
| 47 |
+
smf_text = re.sub(r'`(.*?)`', r'[code]\1[/code]', smf_text) # Inline code
|
| 48 |
+
|
| 49 |
+
# 6. Horizontal Rule
|
| 50 |
+
smf_text = re.sub(r'^\s*---\s*$', '\n[hr]\n', smf_text, flags=re.MULTILINE)
|
| 51 |
+
|
| 52 |
+
return smf_text
|
| 53 |
+
|
| 54 |
+
# --- GUI Application ---
|
| 55 |
+
|
| 56 |
+
class App(TkinterDnD.Tk):
|
| 57 |
+
def __init__(self):
|
| 58 |
+
super().__init__()
|
| 59 |
+
self.title("Markdown to SMF Converter")
|
| 60 |
+
self.geometry("500x400")
|
| 61 |
+
|
| 62 |
+
# Main frame
|
| 63 |
+
main_frame = tk.Frame(self)
|
| 64 |
+
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
| 65 |
+
|
| 66 |
+
# Label for instructions
|
| 67 |
+
self.drop_label = tk.Label(
|
| 68 |
+
main_frame,
|
| 69 |
+
text="Drag and Drop .md files here",
|
| 70 |
+
font=("Arial", 14),
|
| 71 |
+
bg="lightblue",
|
| 72 |
+
relief="solid",
|
| 73 |
+
bd=2
|
| 74 |
+
)
|
| 75 |
+
self.drop_label.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
| 76 |
+
|
| 77 |
+
# Scrolled text for status logs
|
| 78 |
+
self.status_log = scrolledtext.ScrolledText(
|
| 79 |
+
main_frame,
|
| 80 |
+
height=10,
|
| 81 |
+
state='disabled' # Make it read-only
|
| 82 |
+
)
|
| 83 |
+
self.status_log.pack(fill=tk.BOTH, expand=True)
|
| 84 |
+
|
| 85 |
+
# Register the drop zone
|
| 86 |
+
self.drop_target_register(DND_FILES)
|
| 87 |
+
self.dnd_bind('<<Drop>>', self.handle_drop)
|
| 88 |
+
|
| 89 |
+
self.log("Ready to convert files.")
|
| 90 |
+
|
| 91 |
+
def log(self, message):
|
| 92 |
+
"""Adds a message to the status log."""
|
| 93 |
+
self.status_log.config(state='normal')
|
| 94 |
+
self.status_log.insert(tk.END, message + "\n")
|
| 95 |
+
self.status_log.see(tk.END) # Auto-scroll to the bottom
|
| 96 |
+
self.status_log.config(state='disabled')
|
| 97 |
+
|
| 98 |
+
def handle_drop(self, event):
|
| 99 |
+
"""Handles the file drop event."""
|
| 100 |
+
# The event.data is a string of file paths, sometimes in curly braces
|
| 101 |
+
# We clean it up and split it into a list of individual paths
|
| 102 |
+
file_paths = self.tk.splitlist(event.data)
|
| 103 |
+
|
| 104 |
+
self.log("\n--- New Drop Detected ---")
|
| 105 |
+
|
| 106 |
+
for file_path in file_paths:
|
| 107 |
+
if file_path.lower().endswith('.md'):
|
| 108 |
+
self.process_file(file_path)
|
| 109 |
+
else:
|
| 110 |
+
self.log(f"Skipped: '{os.path.basename(file_path)}' is not a .md file.")
|
| 111 |
+
|
| 112 |
+
self.log("--- All files processed ---")
|
| 113 |
+
|
| 114 |
+
def process_file(self, file_path):
|
| 115 |
+
"""Reads, converts, and saves a single markdown file."""
|
| 116 |
+
filename = os.path.basename(file_path)
|
| 117 |
+
self.log(f"Processing: '{filename}'...")
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
# Read the markdown file
|
| 121 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 122 |
+
markdown_content = f.read()
|
| 123 |
+
|
| 124 |
+
# Convert the content
|
| 125 |
+
smf_content = convert_md_to_smf(markdown_content)
|
| 126 |
+
|
| 127 |
+
# Define the output path
|
| 128 |
+
base_name = os.path.splitext(file_path)[0]
|
| 129 |
+
output_path = f"{base_name}_smf.txt"
|
| 130 |
+
|
| 131 |
+
# Save the converted content
|
| 132 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
| 133 |
+
f.write(smf_content)
|
| 134 |
+
|
| 135 |
+
self.log(f"Success! Saved to: '{os.path.basename(output_path)}'")
|
| 136 |
+
|
| 137 |
+
except Exception as e:
|
| 138 |
+
self.log(f"Error processing '{filename}': {e}")
|
| 139 |
+
|
| 140 |
+
# --- Main Execution ---
|
| 141 |
+
|
| 142 |
+
if __name__ == "__main__":
|
| 143 |
+
app = App()
|
| 144 |
+
app.mainloop()
|