Skip to content

Commit 3a4e5ba

Browse files
authored
feat: Initial Version
1 parent 7f60094 commit 3a4e5ba

File tree

1 file changed

+343
-0
lines changed

1 file changed

+343
-0
lines changed

markdown_to_bbcode.py

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
import re
2+
import argparse
3+
import sys
4+
import os
5+
import subprocess
6+
7+
def convert_markdown_to_bbcode(markdown_text, repo_name=None, bbcode_type='egosoft', relative_path=None):
8+
"""
9+
Converts Markdown formatted text to BBCode formatted text.
10+
11+
Args:
12+
markdown_text (str): The Markdown text to be converted.
13+
repo_name (str, optional): GitHub repository name in the format 'username/repo'.
14+
Used to generate absolute URLs for images and links.
15+
bbcode_type (str): Type of BBCode format ('egosoft', 'nexus', or 'steam').
16+
relative_path (str, optional): Relative path of the input file.
17+
18+
Returns:
19+
str: The converted BBCode text.
20+
"""
21+
22+
bbcode_text = markdown_text
23+
24+
# 1. Headers
25+
# Define size mapping based on BBCode type
26+
header_level_mapping = {
27+
'egosoft': {
28+
1: {'size': 140, 'underline': True, 'italic': False, 'bold': False},
29+
2: {'size': 130, 'underline': True, 'italic': False, 'bold': False},
30+
3: {'size': 125, 'underline': True, 'italic': False, 'bold': False},
31+
4: {'size': 120, 'underline': True, 'italic': False, 'bold': False},
32+
5: {'size': 115, 'underline': True, 'italic': False, 'bold': False},
33+
6: {'size': 110, 'underline': True, 'italic': False, 'bold': False}
34+
},
35+
'nexus': {
36+
1: {'size': 4, 'underline': True, 'italic': False, 'bold': False},
37+
2: {'size': 3, 'underline': True, 'italic': False, 'bold': True},
38+
3: {'size': 3, 'underline': True, 'italic': True, 'bold': False},
39+
4: {'size': 3, 'underline': True, 'italic': False, 'bold': False},
40+
5: {'size': 2, 'underline': True, 'italic': True, 'bold': False},
41+
6: {'size': 2, 'underline': True, 'italic': False, 'bold': False}
42+
},
43+
'steam': {
44+
1: {'h': 1, 'underline': False, 'italic': False, 'bold': False},
45+
2: {'h': 2, 'underline': False, 'italic': False, 'bold': True},
46+
3: {'h': 2, 'underline': False, 'italic': False, 'bold': False},
47+
4: {'h': 3, 'underline': False, 'italic': False, 'bold': True},
48+
5: {'h': 3, 'underline': True, 'italic': False, 'bold': False},
49+
6: {'h': 3, 'underline': False, 'italic': False, 'bold': False}
50+
}
51+
}
52+
53+
current_header_level_mapping = header_level_mapping.get(bbcode_type, header_level_mapping['egosoft'])
54+
55+
# Convert Markdown headers to BBCode with size, bold, underline, and italic
56+
def replace_headers(match):
57+
hashes, header_text = match.groups()
58+
level = len(hashes)
59+
header_attrs = current_header_level_mapping.get(level, {'size': 100, 'underline': True, 'italic': False, 'bold': True})
60+
if bbcode_type == 'steam':
61+
h = header_attrs['h']
62+
underline = '[u]' if header_attrs['underline'] else ''
63+
italic = '[i]' if header_attrs['italic'] else ''
64+
bold = '[b]' if header_attrs['bold'] else ''
65+
underline_close = '[/u]' if header_attrs['underline'] else ''
66+
italic_close = '[/i]' if header_attrs['italic'] else ''
67+
bold_close = '[/b]' if header_attrs['bold'] else ''
68+
return f"[h{h}]{underline}{italic}{bold}{header_text.strip()}{bold_close}{italic_close}{underline_close}[/h{h}]"
69+
else:
70+
size = header_attrs['size']
71+
underline = '[u]' if header_attrs['underline'] else ''
72+
italic = '[i]' if header_attrs['italic'] else ''
73+
bold = '[b]' if header_attrs['bold'] else ''
74+
underline_close = '[/u]' if header_attrs['underline'] else ''
75+
italic_close = '[/i]' if header_attrs['italic'] else ''
76+
bold_close = '[/b]' if header_attrs['bold'] else ''
77+
return f"[size={size}]{underline}{italic}{bold}{header_text.strip()}{bold_close}{italic_close}{underline_close}[/size]"
78+
79+
bbcode_text = re.sub(r'^(#{1,6})\s+(.*)', replace_headers, bbcode_text, flags=re.MULTILINE)
80+
81+
# 2. Images
82+
def replace_images(match):
83+
image_url = match.group(1)
84+
if repo_name and not re.match(r'^https?://', image_url):
85+
absolute_url = f"https://github.com/{repo_name}/raw/main/{relative_path}/{image_url}"
86+
if bbcode_type == 'egosoft':
87+
return f"[spoiler][img]{absolute_url}[/img][/spoiler]"
88+
else:
89+
return f"[img]{absolute_url}[/img]"
90+
else:
91+
if bbcode_type == 'egosoft':
92+
return f"[spoiler][img]{image_url}[/img][/spoiler]"
93+
else:
94+
return f"[img]{image_url}[/img]"
95+
96+
bbcode_text = re.sub(r'!\[.*?\]\((.*?)\)', replace_images, bbcode_text)
97+
98+
# 3. Block Code
99+
# Convert ```language\ncode\n``` to [code=language]code[/code] for 'egosoft'
100+
# and to [code]code[/code] for 'nexus' and 'steam'
101+
def replace_block_code(match):
102+
lang = match.group(1) if match.group(1) else ''
103+
code = match.group(2)
104+
indent = match.group(3)
105+
if bbcode_type == 'egosoft' and lang:
106+
return f"[code={lang}]\n{code}\n{indent}[/code]"
107+
else:
108+
return f"[code]\n{code}\n{indent}[/code]"
109+
110+
bbcode_text = re.sub(r'```(\w+)?\n([\s\S]*?)\n(\s*)```', replace_block_code, bbcode_text)
111+
112+
# 4. Links
113+
# Convert [text](url) to [url=url]text[/url]
114+
def replace_links(match):
115+
link_text, link_url = match.groups()
116+
if repo_name and not re.match(r'^https?://', link_url):
117+
absolute_url = f"https://github.com/{repo_name}/raw/main/{relative_path}/{link_url}"
118+
return f"[url={absolute_url}]{link_text}[/url]"
119+
else:
120+
return f"[url={link_url}]{link_text}[/url]"
121+
122+
bbcode_text = re.sub(r'\[(.*?)\]\((.*?)\)', replace_links, bbcode_text)
123+
124+
# 5. Bold
125+
# Convert **text** or __text__ to [b]text[/b]
126+
bbcode_text = re.sub(r'(\*\*|__)(.*?)\1', r'[b]\2[/b]', bbcode_text)
127+
128+
# 6. Italics
129+
# Convert *text* or _text_ to [i]text[/i]
130+
# Only match if the marker is preceded by a space or start of line
131+
# This prevents matching underscores within URLs or words like some_word
132+
bbcode_text = re.sub(r'(^|\s)(\*|_)(?!\2)(.*?)\2', r'\1[i]\3[/i]', bbcode_text)
133+
134+
# 7. Inline Code
135+
# Convert `text` to [b]text[/b]
136+
bbcode_text = re.sub(r'`([^`\n]+)`', r'[b]\1[/b]', bbcode_text)
137+
138+
# 8. Lists
139+
# Convert unordered and ordered lists to BBCode
140+
def parse_list_items(lines):
141+
list_stack = []
142+
current_list = []
143+
current_indent = 0
144+
list_type = 'unordered'
145+
146+
for line in lines:
147+
stripped_line = line.lstrip()
148+
indent = len(line) - len(stripped_line)
149+
if stripped_line.startswith(('-', '*', '+')):
150+
item = stripped_line[1:].strip()
151+
if indent > current_indent:
152+
list_stack.append((current_list, current_indent, list_type))
153+
current_list = []
154+
current_indent = indent
155+
elif indent < current_indent:
156+
while list_stack and indent < current_indent:
157+
parent_list, parent_indent, parent_type = list_stack.pop()
158+
if parent_type == 'ordered':
159+
if bbcode_type == 'nexus':
160+
parent_list.append(f"[olist]\n" + "\n".join(current_list) + "\n[/olist]")
161+
else:
162+
parent_list.append(f"[list=1]\n" + "\n".join(current_list) + "\n[/list]")
163+
else:
164+
parent_list.append(f"[list]\n" + "\n".join(current_list) + "\n[/list]")
165+
current_list = parent_list
166+
current_indent = parent_indent
167+
list_type = parent_type
168+
current_list.append(f"[*] {item}")
169+
list_type = 'unordered'
170+
elif re.match(r'^\s*\d+\.\s+', stripped_line):
171+
item = re.sub(r'^\s*\d+\.\s+', '', stripped_line)
172+
if indent > current_indent:
173+
list_stack.append((current_list, current_indent, list_type))
174+
current_list = []
175+
current_indent = indent
176+
elif indent < current_indent:
177+
while list_stack and indent < current_indent:
178+
parent_list, parent_indent, parent_type = list_stack.pop()
179+
if parent_type == 'ordered':
180+
if bbcode_type == 'nexus':
181+
parent_list.append(f"[olist]\n" + "\n".join(current_list) + "\n[/olist]")
182+
else:
183+
parent_list.append(f"[list=1]\n" + "\n".join(current_list) + "\n[/list]")
184+
else:
185+
parent_list.append(f"[list]\n" + "\n".join(current_list) + "\n[/list]")
186+
current_list = parent_list
187+
current_indent = parent_indent
188+
list_type = parent_type
189+
current_list.append(f"[*] {item}")
190+
list_type = 'ordered'
191+
else:
192+
current_list.append(line)
193+
194+
while list_stack:
195+
parent_list, parent_indent, parent_type = list_stack.pop()
196+
if parent_type == 'ordered':
197+
if bbcode_type == 'nexus':
198+
parent_list.append(f"[olist]\n" + "\n".join(current_list) + "\n[/olist]")
199+
else:
200+
parent_list.append(f"[list=1]\n" + "\n".join(current_list) + "\n[/list]")
201+
else:
202+
parent_list.append(f"[list]\n" + "\n".join(current_list) + "\n[/list]")
203+
current_list = parent_list
204+
205+
if list_type == 'ordered':
206+
if bbcode_type == 'nexus':
207+
return "[olist]" + "\n".join(current_list) + "\n[/olist]"
208+
else:
209+
return "[list=1]" + "\n".join(current_list) + "\n[/list]"
210+
else:
211+
return "[list]" + "\n".join(current_list) + "\n[/list]"
212+
213+
def replace_lists(match):
214+
list_content = match.group(0)
215+
lines = list_content.split('\n')
216+
return parse_list_items(lines).replace('\n\n', '\n')
217+
218+
bbcode_text = re.sub(r'(?:^\s*[-*+]\s+.*\n?)+', replace_lists, bbcode_text, flags=re.MULTILINE)
219+
bbcode_text = re.sub(r'(?:^\s*\d+\.\s+.*\n?)+', replace_lists, bbcode_text, flags=re.MULTILINE)
220+
221+
# 9. Blockquotes
222+
# Convert > Quote to [quote]Quote[/quote]
223+
def replace_blockquotes(match):
224+
quote = match.group(1)
225+
return f"[quote]{quote.strip()}[/quote]"
226+
227+
bbcode_text = re.sub(r'^>\s?(.*)', replace_blockquotes, bbcode_text, flags=re.MULTILINE)
228+
229+
# 10. Horizontal Rules
230+
# Convert --- or *** or ___ to [hr]
231+
bbcode_text = re.sub(r'^(\*\*\*|---|___)$', r'[hr]', bbcode_text, flags=re.MULTILINE)
232+
233+
# 11. Line Breaks
234+
# Convert two or more spaces at the end of a line to [br]
235+
bbcode_text = re.sub(r' {2,}\n', r'[br]\n', bbcode_text)
236+
237+
return bbcode_text
238+
239+
def parse_arguments():
240+
"""
241+
Parses command-line arguments.
242+
243+
Returns:
244+
argparse.Namespace: Parsed arguments containing input file path, BBCode type, repo name, and output folder.
245+
"""
246+
parser = argparse.ArgumentParser(
247+
description='Convert a Markdown file to BBCode format.',
248+
epilog='Example usage: python markdown_to_bbcode.py input.md --type nexus --repo username/repo --output-folder ./output'
249+
)
250+
parser.add_argument('input_file', help='Path to the input Markdown file.')
251+
parser.add_argument('-t', '--type', choices=['egosoft', 'nexus', 'steam'], default='egosoft',
252+
help='Type of BBCode format to use (default: egosoft).')
253+
parser.add_argument('-r', '--repo', help='GitHub repository name (e.g., username/repo) to generate absolute image URLs.', default=None)
254+
parser.add_argument('-o', '--output-folder', help='Path to the output folder. Defaults to the current directory.', default='.')
255+
256+
return parser.parse_args()
257+
258+
def generate_output_filename(input_file, bbcode_type, output_folder):
259+
"""
260+
Generates the output file name based on the input file, BBCode type, and output folder.
261+
262+
Args:
263+
input_file (str): Path to the input Markdown file.
264+
bbcode_type (str): Type of BBCode format ('egosoft' or 'nexus').
265+
output_folder (str): Path to the output folder.
266+
267+
Returns:
268+
str: Generated output file path.
269+
"""
270+
base = os.path.splitext(os.path.basename(input_file))[0]
271+
output_filename = f"{base}.{bbcode_type}"
272+
return os.path.join(output_folder, output_filename)
273+
274+
def get_repo_name():
275+
"""
276+
Retrieves the GitHub repository name from the Git configuration.
277+
278+
Returns:
279+
str: The repository name in the format 'username/repo'.
280+
"""
281+
try:
282+
repo_url = subprocess.check_output(['git', 'config', '--get', 'remote.origin.url'], encoding='utf-8').strip()
283+
if repo_url.startswith('https://github.com/'):
284+
repo_name = repo_url[len('https://github.com/'):]
285+
return repo_name.rstrip('.git')
286+
elif repo_url.startswith('[email protected]:'):
287+
repo_name = repo_url[len('[email protected]:'):]
288+
return repo_name.rstrip('.git')
289+
else:
290+
print(f"Error: Unsupported repository URL format: {repo_url}")
291+
sys.exit(1)
292+
except subprocess.CalledProcessError as e:
293+
print(f"Error: Unable to retrieve repository URL: {e}")
294+
sys.exit(1)
295+
296+
def main():
297+
args = parse_arguments()
298+
299+
input_path = args.input_file
300+
bbcode_type = args.type
301+
repo_name = args.repo or get_repo_name()
302+
output_folder = args.output_folder
303+
304+
# Check if input file exists
305+
if not os.path.isfile(input_path):
306+
print(f"Error: The input file '{input_path}' does not exist.")
307+
sys.exit(1)
308+
309+
# Create output folder if it doesn't exist
310+
if not os.path.isdir(output_folder):
311+
try:
312+
os.makedirs(output_folder)
313+
print(f"Created output directory: '{output_folder}'")
314+
except Exception as e:
315+
print(f"Error creating output directory '{output_folder}': {e}")
316+
sys.exit(1)
317+
318+
try:
319+
with open(input_path, 'r', encoding='utf-8') as infile:
320+
markdown_content = infile.read()
321+
except Exception as e:
322+
print(f"Error reading the input file: {e}")
323+
sys.exit(1)
324+
325+
# Extract relative path part from input_path
326+
relative_path = os.path.dirname(input_path)
327+
328+
# Convert Markdown to BBCode
329+
bbcode_result = convert_markdown_to_bbcode(markdown_content, repo_name=repo_name, bbcode_type=bbcode_type, relative_path=relative_path)
330+
331+
# Generate output file name
332+
output_path = generate_output_filename(input_path, bbcode_type, output_folder)
333+
334+
try:
335+
with open(output_path, 'w', encoding='utf-8') as outfile:
336+
outfile.write(bbcode_result)
337+
print(f"Successfully converted '{input_path}' to '{output_path}'.")
338+
except Exception as e:
339+
print(f"Error writing to the output file: {e}")
340+
sys.exit(1)
341+
342+
if __name__ == "__main__":
343+
main()

0 commit comments

Comments
 (0)