|
马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
本帖最后由 player-none 于 2025-4-17 21:45 编辑
【自制】BBCode/Markdown Converter
上一个作品:【自制】DoublePinyin 双拼练习器 V1.0.0
欢迎支持……(没有一个人 Star 的烦恼)
这个作品估计不会长久保持更新,因为确实没啥好更的。除非我以后用这玩意写帖子的时候发现几个 bug……(暗中祈祷不会发现 bug)
最后帖子发出来一看 5 个地方出错了
创作背景
- 非常深刻的感受到 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()[0] == '*':
- # 提示用户是否要保存
- 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, '打开', '.')[0]
- 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, '另存为', '.')[0]
- if not path:
- return
- else:
- path = self.filePath
- if not path:
- # 询问路径
- path = QFileDialog.getSaveFileName(self, '保存', '.')[0]
- 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'][self.markdownAction.isChecked()]
- 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 模式([b][/b])
- self.text.setPlainText(text[:end] + '[b]' + text[start:end] + '[/b]' + text[end:])
- else:
- # Markdown 模式(****)
- self.text.setPlainText(text[:end] + '**' + text[start:end] + '**' + text[end:])
- 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 模式([i][/i])
- self.text.setPlainText(text[:end] + '[i]' + text[start:end] + '[/i]' + text[end:])
- else:
- # Markdown 模式(**)
- self.text.setPlainText(text[:end] + '*' + text[start:end] + '*' + text[end:])
- 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 += '[/list]\n'
- # 终于找到这个 bug 的源头了,结束之后程序还会尝试继续结束。。。
- # 把下面这行加上就没事了
- flag = 0
- # 空行
- res += '\n'
- continue
- if s[:3] == '1. ':
- # 进入有序列表
- res += '[list=1]\n'
- flag = 1
- elif s[:2] in ('- ', '* ', '+ ') and flag != 2:
- # 进入无序列表
- res += '[list]\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
- # 标题的格式来源于 [url]https://fishc.com.cn/thread-146275-1-1.html[/url]
- # 一级标题
- s = re.sub(r'^# (.*)$', r'[align=center][size=5][b]\1[/b][/size][/align]', s)
- # 二级标题
- s = re.sub(r'^## (.*)$', r'[size=4][b]\1[/b][/size]', s)
- # 三级标题
- s = re.sub(r'^### (.*)$', r'[size=3][backcolor=DarkSlateGray][color=White]\1[/color][/backcolor][/size]', s)
- # 引用
- s = re.sub(r'^> (.*)$', r'[quote]\1[/quote]', s)
- # 非贪婪匹配:'*1* *2*' 会替换成 '[i]1[/i] [i]2[/i]' 而不是 '[i]1* *2[/i]'
- # 加粗
- s = re.sub(r'\*\*(.*?)\*\*', r'[b]\1[/b]', s)
- # 斜体
- s = re.sub(r'\*(.*?)\*', r'[i]\1[/i]', s)
- # 行内代码
- s = re.sub(r'`(.*?)`', r'[backcolor=LightGray]\1[/backcolor]', s)
- # 删除线
- s = re.sub(r'~~(.*?)~~', r'[s]\1[/s]', s)
- # 图片
- s = re.sub(r'!\[.*?]\((.*?)\)', r'[img]\1[/img]', s)
- # 链接
- s = re.sub(r'\[(.*?)]\((.*?)\)', r'[url=\2]\1[/url]', s)
- res += s + '\n'
- if flag in (1, 2):
- # 还没结束有序/无序列表
- res += '[/list]\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 == '[list=1]':
- # 进入有序列表
- flag = 1
- continue
- elif s == '[list]':
- # 进入无序列表
- flag = 2
- continue
- elif s == '[/list]':
- # 退出有序/无序列表
- flag = 0
- continue
- elif s[:5] == '[<避免被解析,请把这个删了>code]':
- # 进入代码块
- res += '```\n'
- flag = 3
- elif s[:7] == '[quote]' or (flag == 4):
- # 引用区域
- s = re.sub(r'^(\[quote])?(.*)$', r'> \2', s)
- flag = 4
- # 可以在一行的末尾退出代码块和引用
- if s[-7:] == '[<避免被解析,请把这个删了>/code]':
- s = s[:-7]
- flag = 0
- elif s[-8:] == '[/quote]':
- 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'^\[align=center]\[size=5]\[b](.*)\[/b]\[/size]\[/align]$', r'# \1', s)
- # 二级标题
- s = re.sub(r'^\[size=4]\[b](.*)\[/b]\[/size]$', r'## \1', s)
- # 三级标题
- s = re.sub(r'^\[size=3]\[backcolor=DarkSlateGray]\[color=White](.*)\[/color]\[/backcolor]\[/size]$', r'### \1', s)
- # 加粗
- s = re.sub(r'\[b](.*?)\[/b]', r'**\1**', s)
- # 斜体
- s = re.sub(r'\[i](.*?)\[/i]', r'*\1*', s)
- # 行内代码
- s = re.sub(r'\[backcolor=LightGray](.*?)\[/backcolor]', r'`\1`', s)
- # 删除线
- s = re.sub(r'\[s](.*?)\[/s]', r'~~\1~~', s)
- # 图片
- s = re.sub(r'!\[img](.*?)\[/img]', r'', s)
- # 链接
- s = re.sub(r'\[url=(.*?)](.*?)\[/url]', 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())
复制代码
-- 没了 --
感谢支持!
|
评分
-
参与人数 2 | 荣誉 +12 |
鱼币 +12 |
贡献 +12 |
收起
理由
|
小甲鱼
| + 6 |
+ 6 |
+ 6 |
鱼C有你更精彩^_^ |
不二如是
| + 6 |
+ 6 |
+ 6 |
鱼C有你更精彩^_^ |
查看全部评分
|