【自制】BBCode/Markdown Converter
本帖最后由 player-none 于 2025-4-17 21:45 编辑【自制】BBCode/Markdown Converter
欢迎前往 GitHub 支持!!!
上一个作品:【自制】DoublePinyin 双拼练习器 V1.0.0
欢迎支持……(没有一个人 Star 的烦恼)
这个作品估计不会长久保持更新,因为确实没啥好更的。除非我以后用这玩意写帖子的时候发现几个 bug……(暗中祈祷不会发现 bug)
最后帖子发出来一看 5 个地方出错了{:10_266:}
创作背景
[*]非常深刻的感受到 Markdown 非常简单、快速,再加上每次发帖子都要遵循同样的规范,不如就把这些规范封装到程序里。
[*]作者看到过 Twilight6 大佬发布的 MarDisTextConverter 源文本转换器,感觉代码写的有些累赘(仅个人观点),就有了自己也写一个的想法。
[*]我可以做出保证,没有任何代码是抄上一条提到的作品的。
[*]闲得无聊
具体代码
pip install PyQt5 qtawesome -i https://mirrors.aliyun.com/pypi/simple
main.py
import sys, re
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
# qtawesome.icon('iconname') 可以快速调用 qtawesome 有的图标
from qtawesome import icon
class Action(QAction):
"""用于简化 QAction 操作"""
def __init__(self, text, command, iconName='', shortcut='', checkable=False, checked=False):
"""
初始化方法
:param text: QAction 显示的文字
:param command: QAction 执行的命令
:param iconName: QAction 图标
:param shortcut: QAction 快捷键
:param checkable: 是否可以切换选中状态
:param checked: 是否被选中
"""
if iconName:
super().__init__(icon(iconName), text)
else:
super().__init__(text)
self.setShortcut(shortcut)
self.setCheckable(checkable)
self.setChecked(checked)
self.triggered.connect(command)
class PlainTextEdit(QPlainTextEdit):
"""用于简化 QPlainTextEdit 操作"""
def __init__(self, font, x, y, w, h, parent):
"""
初始化方法
:param font: QFont
:param x: 左上角 x 坐标
:param y: 左上角 y 坐标
:param w: 宽度
:param h: 高度
:param parent: parent
"""
super().__init__(parent)
self.setFont(font)
self.resize(w, h)
self.move(x, y)
def getSelection(self):
"""获取输入框的选区开始和结束位置"""
cursor = self.textCursor()
return cursor.selectionStart(), cursor.selectionEnd()
def setSelection(self, start, end=None):
"""
设置输入框的选区开始和结束位置
:param start: 开始位置
:param end: 结束位置(默认为开始位置)
"""
if end is None:
end = start
# 鬼知道为什么要单独再创建一个 QTextCursor
cursor = QTextCursor(self.document())
cursor.setPosition(start)
cursor.setPosition(end, QTextCursor.KeepAnchor)
self.setTextCursor(cursor)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowIcon(icon('mdi.autorenew'))
self.setWindowTitle('BBCode/Markdown Converter——BBCode/Markdown 转换器')
self.setFixedSize(1200, 800)
self.font8 = QFont('Microsoft YaHei UI', 8)
self.font10 = QFont('Microsoft YaHei UI', 10)
self.font11 = QFont('Microsoft YaHei UI', 11)
# 记录已打开文件的路径
self.filePath = ''
# 创建菜单栏以方便用户使用各种各样的操作
self.menubar = QMenuBar(self)
self.menubar.setFixedHeight(22)
# 菜单栏部分好像没啥需要注释的
self.fileMenu = QMenu('文件(&F)')
self.newAction = Action('新建(&N)', self.new, 'mdi.file-document', 'Ctrl+N')
self.openAction = Action('打开(&O)', self.open, 'mdi.folder-open', 'Ctrl+O')
self.saveAction = Action('保存(&S)', self.save, 'mdi.content-save', 'Ctrl+S')
self.saveAsAction = Action('另存为(&A)', lambda: self.save(True), 'mdi.content-save-all', 'Ctrl+Alt+A')
self.quitAction = Action('退出(&Q)', self.close, 'mdi.exit-to-app', 'Alt+F4')
self.fileMenu.addActions((self.newAction, self.openAction, self.saveAction, self.saveAsAction, self.quitAction))
self.editMenu = QMenu('编辑(&E)')
self.boldAction = Action('加粗(&B)', self.bold, 'mdi.format-bold', 'Ctrl+B')
self.italicAction = Action('斜体(&B)', self.italic, 'mdi.format-italic', 'Ctrl+I')
self.editMenu.addActions((self.boldAction, self.italicAction))
self.formatMenu = QMenu('格式(&O)')
self.bbcodeAction = Action('&BBCode 编辑模式', lambda: (
self.bbcodeAction.setChecked(True), self.markdownAction.setChecked(False), self.updateStatusLabel()
), '', 'Ctrl+Alt+B', True, True)
self.markdownAction = Action('&Markdown 编辑模式', lambda: (
self.bbcodeAction.setChecked(False), self.markdownAction.setChecked(True), self.updateStatusLabel()
), '', 'Ctrl+Alt+M', True)
self.fontAction = Action('显示字体(&F)', self.font_, 'mdi.format-font', 'Ctrl+Alt+F')
self.formatMenu.addActions((self.bbcodeAction, self.markdownAction, self.fontAction))
self.convertMenu = QMenu('转换(&C)')
self.convertAction = Action('转换(&C)', self.convert, 'mdi.swap-horizontal-circle', 'Ctrl+Alt+C')
self.convertMenu.addAction(self.convertAction)
self.helpMenu = QMenu('帮助(&H)')
self.aboutAction = Action('关于(&A)', self.help, 'mdi.help-circle')
self.convertMenu.addAction(self.aboutAction)
self.menubar.addMenu(self.fileMenu)
self.menubar.addMenu(self.editMenu)
self.menubar.addMenu(self.formatMenu)
self.menubar.addMenu(self.convertMenu)
self.setMenuBar(self.menubar)
# 创建工具栏
self.toolbar = QToolBar(self)
# 让工具栏的宽度和窗口宽度一样(使其“撑满”整个窗口)
self.toolbar.setFixedWidth(1200)
# 设置字体
self.toolbar.setFont(self.font8)
# 显示每个 QAction 的文字
self.toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
# 去掉工具栏左边的拖动点位
self.toolbar.setMovable(False)
# 添加各种各样的操作
self.toolbar.addActions((self.newAction, self.openAction, self.saveAction, self.quitAction,
self.boldAction, self.italicAction, self.convertAction))
# 刚好放在菜单下面
self.toolbar.move(0, self.menubar.height())
self.text = PlainTextEdit(self.font11, 0, self.menubar.height() + self.toolbar.height(),
1200, 780 - self.menubar.height() - self.toolbar.height(), self)
self.text.textChanged.connect(self.updateStatusLabel)
self.statusLabel = QLabel('编辑模式: BBCode 行数: 1 字符数: 0', self)
self.statusLabel.setFont(self.font10)
self.statusLabel.adjustSize()
self.statusLabel.move(0, 780)
def closeEvent(self, a0):
if self.windowTitle() == '*':
# 提示用户是否要保存
result = QMessageBox(QMessageBox.Question, '提示', f'是否要保存文件?',
QMessageBox.Save | QMessageBox.No, self).exec()
if result == QMessageBox.Save:
self.save()
def new(self):
"""清空输入框内容"""
self.text.clear()
self.setWindowTitle('BBCode/Markdown Converter——BBCode/Markdown 转换器')
def open(self):
"""打开一个文件"""
path = QFileDialog.getOpenFileName(self, '打开', '.')
if not path:
return
with open(path, encoding='utf-8') as f:
self.text.setPlainText(f.read())
self.filePath = path
self.setWindowTitle(f'BBCode/Markdown Converter——BBCode/Markdown 转换器 ({path})')
def save(self, saveAs=True):
"""
保存输入框里的内容
:param saveAs: 是否
"""
if saveAs:
# 询问路径
path = QFileDialog.getSaveFileName(self, '另存为', '.')
if not path:
return
else:
path = self.filePath
if not path:
# 询问路径
path = QFileDialog.getSaveFileName(self, '保存', '.')
if not path:
return
with open(path, 'w', encoding='utf-8') as f:
f.write(self.text.toPlainText())
self.filePath = path
self.setWindowTitle(f'BBCode/Markdown Converter——BBCode/Markdown 转换器 ({path})')
def help(self):
"""关于"""
top = QDialog(self)
top.setWindowTitle('关于')
top.icon = QLabel(top)
top.icon.setPixmap(self.windowIcon().pixmap(64, 64))
top.icon.move(10, 10)
top.version = QLabel('版本:V1.0.0', top)
top.version.setFont(self.font10)
top.version.adjustSize()
top.version.move(74, 10)
top.label = QLabel('BBCode/Markdown Converter 是 dddddgz(player-none)开发的一个小工具,'
'用来便捷地进行 BBCode 和 Markdown 之间的转换。', top)
top.label.setFont(self.font10)
top.label.adjustSize()
top.label.move(10, 74)
top.exec()
def updateStatusLabel(self):
"""更新 statusLabel 显示的内容"""
content = self.text.toPlainText()
mode = ['BBCode', 'Markdown']
lines = content.count('\n') + 1
length = len(content)
self.statusLabel.setText(f'编辑模式: {mode} 行数: {lines} 字符数: {length}')
# PyQt5 特性,Label 中的文本有固定宽度,无论怎么更改文本宽度都不会变,需要手动 adjustSize 适应宽度
self.statusLabel.adjustSize()
self.setWindowTitle(f'*{self.windowTitle().lstrip('*')}')
def bold(self):
"""加粗"""
start, end = self.text.getSelection()
text = self.text.toPlainText()
bbcode = self.bbcodeAction.isChecked()
# 后来发现其实根本不用分 start 等不等于 end 的情况讨论
if bbcode:
# BBCode 模式()
self.text.setPlainText(text[:end] + '' + text + '' + text)
else:
# Markdown 模式(****)
self.text.setPlainText(text[:end] + '**' + text + '**' + text)
self.text.setSelection(start + 2 + bbcode, end + 2 + bbcode)
def italic(self):
"""斜体"""
start, end = self.text.getSelection()
text = self.text.toPlainText()
bbcode = self.bbcodeAction.isChecked()
if bbcode:
# BBCode 模式()
self.text.setPlainText(text[:end] + '' + text + '' + text)
else:
# Markdown 模式(**)
self.text.setPlainText(text[:end] + '*' + text + '*' + text)
self.text.setSelection(start + 1 + bbcode * 2, end + 1 + bbcode * 2)
def font_(self):
"""字体设置界面"""
dialog = QFontDialog(self)
dialog.setFont(self.font8)
dialog.setCurrentFont(self.text.font())
dialog.accepted.connect(lambda: self.text.setFont(dialog.currentFont()))
dialog.exec()
def convert(self):
"""将输入框里的内容视情况转换为 BBCode/Markdown"""
if self.markdownAction.isChecked():
# 转换为 BBCode
self.toBBCode()
# 可以直接“触发”相关事件,省去了很多麻烦
self.bbcodeAction.trigger()
else:
# 转换为 Markdown
self.toMarkdown()
self.markdownAction.trigger()
def toBBCode(self):
"""将输入框里的内容转换为 BBCode"""
md = self.text.toPlainText().splitlines()
res = ''
# 0 表示没有特殊格式;1 表示有序列表;2 表示无序列表;3 表示代码块
flag = 0
for s in md:
if not s:
if flag in (1, 2):
# 结束有序/无序列表
res += '\n'
# 终于找到这个 bug 的源头了,结束之后程序还会尝试继续结束。。。
# 把下面这行加上就没事了
flag = 0
# 空行
res += '\n'
continue
if s[:3] == '1. ':
# 进入有序列表
res += '\n'
flag = 1
elif s[:2] in ('- ', '* ', '+ ') and flag != 2:
# 进入无序列表
res += '\n'
flag = 2
elif s[:3] == '```' and flag < 3:
# 进入代码块(不换行)
res += '[<避免被解析,请把这个删了>code]'
flag = 3
continue
if flag == 1:
# 有序列表
s = re.sub(r'^\d+\. (.*)$', r'[*]\1', s)
elif flag == 2:
# 无序列表
s = re.sub(r'^[-*+] (.*)$', r'[*]\1', s)
elif flag == 3:
# 代码块
if s == '```':
# 可以离开代码块了
s = '[<避免被解析,请把这个删了>/code]'
flag = 0
res += s + '\n'
# 代码块期间啥都不能做(其实离开代码块的那一行也啥都不能做)
continue
# 标题的格式来源于 https://fishc.com.cn/thread-146275-1-1.html
# 一级标题
s = re.sub(r'^# (.*)$', r'\1', s)
# 二级标题
s = re.sub(r'^## (.*)$', r'\1', s)
# 三级标题
s = re.sub(r'^### (.*)$', r'\1', s)
# 引用
s = re.sub(r'^> (.*)$', r'\1', s)
# 非贪婪匹配:'*1* *2*' 会替换成 '1 2' 而不是 '1* *2'
# 加粗
s = re.sub(r'\*\*(.*?)\*\*', r'\1', s)
# 斜体
s = re.sub(r'\*(.*?)\*', r'\1', s)
# 行内代码
s = re.sub(r'`(.*?)`', r'\1', s)
# 删除线
s = re.sub(r'~~(.*?)~~', r'\1', s)
# 图片
s = re.sub(r'!\[.*?]\((.*?)\)', r'\1', s)
# 链接
s = re.sub(r'\[(.*?)]\((.*?)\)', r'\1', s)
res += s + '\n'
if flag in (1, 2):
# 还没结束有序/无序列表
res += '\n'
res = re.sub(r'\n{3,}', '\n\n', res)
self.text.setPlainText(res)
def toMarkdown(self):
"""将输入框里的内容转换为 Markdown"""
bbcode = self.text.toPlainText().splitlines()
res = ''
# 0 表示没有特殊格式;1 表示有序列表;2 表示无序列表;3 表示代码块;4 表示引用
flag = 0
# 有序列表的索引
index = 0
for s in bbcode:
if not s:
# 空行
res += '\n'
continue
if s == '':
# 进入有序列表
flag = 1
continue
elif s == '':
# 进入无序列表
flag = 2
continue
elif s == '':
# 退出有序/无序列表
flag = 0
continue
elif s[:5] == '[<避免被解析,请把这个删了>code]':
# 进入代码块
res += '```\n'
flag = 3
elif s[:7] == '' or (flag == 4):
# 引用区域
s = re.sub(r'^(\)?(.*)$', r'> \2', s)
flag = 4
# 可以在一行的末尾退出代码块和引用
if s[-7:] == '[<避免被解析,请把这个删了>/code]':
s = s[:-7]
flag = 0
elif s[-8:] == '':
s = s[:-8]
flag = 0
if flag == 1:
# 有序列表
s = re.sub(r'^\[\*] (.*)$', fr'{index}. \1', s)
elif flag == 2:
# 无序列表
s = re.sub(r'^\[\*] (.*)$', r'- \1', s)
elif flag == 3:
# 代码块
res += s + '\n'
# 代码块期间啥都不能做(其实离开代码块的那一行也啥都不能做)
continue
# 不需要处理引用
# 一级标题
s = re.sub(r'^\\\(.*)\\\$', r'# \1', s)
# 二级标题
s = re.sub(r'^\\(.*)\\$', r'## \1', s)
# 三级标题
s = re.sub(r'^\\\(.*)\\\$', r'### \1', s)
# 加粗
s = re.sub(r'\(.*?)\', r'**\1**', s)
# 斜体
s = re.sub(r'\(.*?)\', r'*\1*', s)
# 行内代码
s = re.sub(r'\(.*?)\', r'`\1`', s)
# 删除线
s = re.sub(r'\(.*?)\', r'~~\1~~', s)
# 图片
s = re.sub(r'!\(.*?)\', r'', s)
# 链接
s = re.sub(r'\(.*?)\', r'[\2](\1)', s)
res += s + '\n'
res = re.sub(r'\n{3,}', '\n\n', res)
self.text.setPlainText(res)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
-- 没了 --
感谢支持!
{:10_256:}{:10_256:}{:10_256:}好项目 不二如是 发表于 2025-4-17 22:27
好项目
感谢支持{:10_298:} 不错呀! 感谢分享 {:10_324:}竟然冷了 这个新人好厉害 liuhongrun2022 发表于 2025-4-26 10:42
这个新人好厉害
一本正经的胡说八道
页:
[1]