鱼C论坛

 找回密码
 立即注册
查看: 65|回复: 3

[作品展示] 【自制】BBCode/Markdown Converter

[复制链接]
发表于 3 天前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 player-none 于 2025-4-17 21:45 编辑

【自制】BBCode/Markdown Converter


欢迎前往 GitHub 支持!!!


上一个作品:【自制】DoublePinyin 双拼练习器 V1.0.0

欢迎支持……(没有一个人 Star 的烦恼

这个作品估计不会长久保持更新,因为确实没啥好更的。除非我以后用这玩意写帖子的时候发现几个 bug……(暗中祈祷不会发现 bug

最后帖子发出来一看 5 个地方出错了

1.gif

创作背景

  • 非常深刻的感受到 Markdown 非常简单、快速,再加上每次发帖子都要遵循同样的规范,不如就把这些规范封装到程序里。
  • 作者看到过 Twilight6 大佬发布的 MarDisTextConverter 源文本转换器,感觉代码写的有些累赘(仅个人观点),就有了自己也写一个的想法。
  • 我可以做出保证,没有任何代码是抄上一条提到的作品的。
  • 闲得无聊


具体代码

  1. pip install PyQt5 qtawesome -i https://mirrors.aliyun.com/pypi/simple
复制代码


main.py

  1. import sys, re
  2. from PyQt5.QtCore import *
  3. from PyQt5.QtGui import *
  4. from PyQt5.QtWidgets import *
  5. # qtawesome.icon('iconname') 可以快速调用 qtawesome 有的图标
  6. from qtawesome import icon

  7. class Action(QAction):
  8.     """用于简化 QAction 操作"""

  9.     def __init__(self, text, command, iconName='', shortcut='', checkable=False, checked=False):
  10.         """
  11.         初始化方法
  12.         :param text: QAction 显示的文字
  13.         :param command: QAction 执行的命令
  14.         :param iconName: QAction 图标
  15.         :param shortcut: QAction 快捷键
  16.         :param checkable: 是否可以切换选中状态
  17.         :param checked: 是否被选中
  18.         """
  19.         if iconName:
  20.             super().__init__(icon(iconName), text)
  21.         else:
  22.             super().__init__(text)
  23.         self.setShortcut(shortcut)
  24.         self.setCheckable(checkable)
  25.         self.setChecked(checked)
  26.         self.triggered.connect(command)

  27. class PlainTextEdit(QPlainTextEdit):
  28.     """用于简化 QPlainTextEdit 操作"""

  29.     def __init__(self, font, x, y, w, h, parent):
  30.         """
  31.         初始化方法
  32.         :param font: QFont
  33.         :param x: 左上角 x 坐标
  34.         :param y: 左上角 y 坐标
  35.         :param w: 宽度
  36.         :param h: 高度
  37.         :param parent: parent
  38.         """
  39.         super().__init__(parent)
  40.         self.setFont(font)
  41.         self.resize(w, h)
  42.         self.move(x, y)

  43.     def getSelection(self):
  44.         """获取输入框的选区开始和结束位置"""
  45.         cursor = self.textCursor()
  46.         return cursor.selectionStart(), cursor.selectionEnd()

  47.     def setSelection(self, start, end=None):
  48.         """
  49.         设置输入框的选区开始和结束位置
  50.         :param start: 开始位置
  51.         :param end: 结束位置(默认为开始位置)
  52.         """
  53.         if end is None:
  54.             end = start
  55.         # 鬼知道为什么要单独再创建一个 QTextCursor
  56.         cursor = QTextCursor(self.document())
  57.         cursor.setPosition(start)
  58.         cursor.setPosition(end, QTextCursor.KeepAnchor)
  59.         self.setTextCursor(cursor)

  60. class MainWindow(QMainWindow):
  61.     def __init__(self):
  62.         super().__init__()
  63.         self.setWindowIcon(icon('mdi.autorenew'))
  64.         self.setWindowTitle('BBCode/Markdown Converter——BBCode/Markdown 转换器')
  65.         self.setFixedSize(1200, 800)
  66.         self.font8 = QFont('Microsoft YaHei UI', 8)
  67.         self.font10 = QFont('Microsoft YaHei UI', 10)
  68.         self.font11 = QFont('Microsoft YaHei UI', 11)
  69.         # 记录已打开文件的路径
  70.         self.filePath = ''
  71.         # 创建菜单栏以方便用户使用各种各样的操作
  72.         self.menubar = QMenuBar(self)
  73.         self.menubar.setFixedHeight(22)

  74.         # 菜单栏部分好像没啥需要注释的
  75.         self.fileMenu = QMenu('文件(&F)')
  76.         self.newAction = Action('新建(&N)', self.new, 'mdi.file-document', 'Ctrl+N')
  77.         self.openAction = Action('打开(&O)', self.open, 'mdi.folder-open', 'Ctrl+O')
  78.         self.saveAction = Action('保存(&S)', self.save, 'mdi.content-save', 'Ctrl+S')
  79.         self.saveAsAction = Action('另存为(&A)', lambda: self.save(True), 'mdi.content-save-all', 'Ctrl+Alt+A')
  80.         self.quitAction = Action('退出(&Q)', self.close, 'mdi.exit-to-app', 'Alt+F4')
  81.         self.fileMenu.addActions((self.newAction, self.openAction, self.saveAction, self.saveAsAction, self.quitAction))

  82.         self.editMenu = QMenu('编辑(&E)')
  83.         self.boldAction = Action('加粗(&B)', self.bold, 'mdi.format-bold', 'Ctrl+B')
  84.         self.italicAction = Action('斜体(&B)', self.italic, 'mdi.format-italic', 'Ctrl+I')
  85.         self.editMenu.addActions((self.boldAction, self.italicAction))

  86.         self.formatMenu = QMenu('格式(&O)')
  87.         self.bbcodeAction = Action('&BBCode 编辑模式', lambda: (
  88.             self.bbcodeAction.setChecked(True), self.markdownAction.setChecked(False), self.updateStatusLabel()
  89.         ), '', 'Ctrl+Alt+B', True, True)
  90.         self.markdownAction = Action('&Markdown 编辑模式', lambda: (
  91.             self.bbcodeAction.setChecked(False), self.markdownAction.setChecked(True), self.updateStatusLabel()
  92.         ), '', 'Ctrl+Alt+M', True)
  93.         self.fontAction = Action('显示字体(&F)', self.font_, 'mdi.format-font', 'Ctrl+Alt+F')
  94.         self.formatMenu.addActions((self.bbcodeAction, self.markdownAction, self.fontAction))

  95.         self.convertMenu = QMenu('转换(&C)')
  96.         self.convertAction = Action('转换(&C)', self.convert, 'mdi.swap-horizontal-circle', 'Ctrl+Alt+C')
  97.         self.convertMenu.addAction(self.convertAction)

  98.         self.helpMenu = QMenu('帮助(&H)')
  99.         self.aboutAction = Action('关于(&A)', self.help, 'mdi.help-circle')
  100.         self.convertMenu.addAction(self.aboutAction)

  101.         self.menubar.addMenu(self.fileMenu)
  102.         self.menubar.addMenu(self.editMenu)
  103.         self.menubar.addMenu(self.formatMenu)
  104.         self.menubar.addMenu(self.convertMenu)
  105.         self.setMenuBar(self.menubar)

  106.         # 创建工具栏
  107.         self.toolbar = QToolBar(self)
  108.         # 让工具栏的宽度和窗口宽度一样(使其“撑满”整个窗口)
  109.         self.toolbar.setFixedWidth(1200)
  110.         # 设置字体
  111.         self.toolbar.setFont(self.font8)
  112.         # 显示每个 QAction 的文字
  113.         self.toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
  114.         # 去掉工具栏左边的拖动点位
  115.         self.toolbar.setMovable(False)
  116.         # 添加各种各样的操作
  117.         self.toolbar.addActions((self.newAction, self.openAction, self.saveAction, self.quitAction,
  118.                                  self.boldAction, self.italicAction, self.convertAction))
  119.         # 刚好放在菜单下面
  120.         self.toolbar.move(0, self.menubar.height())

  121.         self.text = PlainTextEdit(self.font11, 0, self.menubar.height() + self.toolbar.height(),
  122.                                   1200, 780 - self.menubar.height() - self.toolbar.height(), self)
  123.         self.text.textChanged.connect(self.updateStatusLabel)
  124.         self.statusLabel = QLabel('编辑模式: BBCode 行数: 1 字符数: 0', self)
  125.         self.statusLabel.setFont(self.font10)
  126.         self.statusLabel.adjustSize()
  127.         self.statusLabel.move(0, 780)

  128.     def closeEvent(self, a0):
  129.         if self.windowTitle()[0] == '*':
  130.             # 提示用户是否要保存
  131.             result = QMessageBox(QMessageBox.Question, '提示', f'是否要保存文件?',
  132.                                  QMessageBox.Save | QMessageBox.No, self).exec()
  133.             if result == QMessageBox.Save:
  134.                 self.save()

  135.     def new(self):
  136.         """清空输入框内容"""
  137.         self.text.clear()
  138.         self.setWindowTitle('BBCode/Markdown Converter——BBCode/Markdown 转换器')

  139.     def open(self):
  140.         """打开一个文件"""
  141.         path = QFileDialog.getOpenFileName(self, '打开', '.')[0]
  142.         if not path:
  143.             return
  144.         with open(path, encoding='utf-8') as f:
  145.             self.text.setPlainText(f.read())
  146.             self.filePath = path
  147.             self.setWindowTitle(f'BBCode/Markdown Converter——BBCode/Markdown 转换器 ({path})')

  148.     def save(self, saveAs=True):
  149.         """
  150.         保存输入框里的内容
  151.         :param saveAs: 是否
  152.         """
  153.         if saveAs:
  154.             # 询问路径
  155.             path = QFileDialog.getSaveFileName(self, '另存为', '.')[0]
  156.             if not path:
  157.                 return
  158.         else:
  159.             path = self.filePath
  160.             if not path:
  161.                 # 询问路径
  162.                 path = QFileDialog.getSaveFileName(self, '保存', '.')[0]
  163.                 if not path:
  164.                     return
  165.         with open(path, 'w', encoding='utf-8') as f:
  166.             f.write(self.text.toPlainText())
  167.         self.filePath = path
  168.         self.setWindowTitle(f'BBCode/Markdown Converter——BBCode/Markdown 转换器 ({path})')

  169.     def help(self):
  170.         """关于"""
  171.         top = QDialog(self)
  172.         top.setWindowTitle('关于')
  173.         top.icon = QLabel(top)
  174.         top.icon.setPixmap(self.windowIcon().pixmap(64, 64))
  175.         top.icon.move(10, 10)
  176.         top.version = QLabel('版本:V1.0.0', top)
  177.         top.version.setFont(self.font10)
  178.         top.version.adjustSize()
  179.         top.version.move(74, 10)
  180.         top.label = QLabel('BBCode/Markdown Converter 是 dddddgz(player-none)开发的一个小工具,'
  181.                            '用来便捷地进行 BBCode 和 Markdown 之间的转换。', top)
  182.         top.label.setFont(self.font10)
  183.         top.label.adjustSize()
  184.         top.label.move(10, 74)
  185.         top.exec()

  186.     def updateStatusLabel(self):
  187.         """更新 statusLabel 显示的内容"""
  188.         content = self.text.toPlainText()
  189.         mode = ['BBCode', 'Markdown'][self.markdownAction.isChecked()]
  190.         lines = content.count('\n') + 1
  191.         length = len(content)
  192.         self.statusLabel.setText(f'编辑模式: {mode} 行数: {lines} 字符数: {length}')
  193.         # PyQt5 特性,Label 中的文本有固定宽度,无论怎么更改文本宽度都不会变,需要手动 adjustSize 适应宽度
  194.         self.statusLabel.adjustSize()
  195.         self.setWindowTitle(f'*{self.windowTitle().lstrip('*')}')

  196.     def bold(self):
  197.         """加粗"""
  198.         start, end = self.text.getSelection()
  199.         text = self.text.toPlainText()
  200.         bbcode = self.bbcodeAction.isChecked()
  201.         # 后来发现其实根本不用分 start 等不等于 end 的情况讨论
  202.         if bbcode:
  203.             # BBCode 模式([b][/b])
  204.             self.text.setPlainText(text[:end] + '[b]' + text[start:end] + '[/b]' + text[end:])
  205.         else:
  206.             # Markdown 模式(****)
  207.             self.text.setPlainText(text[:end] + '**' + text[start:end] + '**' + text[end:])
  208.         self.text.setSelection(start + 2 + bbcode, end + 2 + bbcode)

  209.     def italic(self):
  210.         """斜体"""
  211.         start, end = self.text.getSelection()
  212.         text = self.text.toPlainText()
  213.         bbcode = self.bbcodeAction.isChecked()
  214.         if bbcode:
  215.             # BBCode 模式([i][/i])
  216.             self.text.setPlainText(text[:end] + '[i]' + text[start:end] + '[/i]' + text[end:])
  217.         else:
  218.             # Markdown 模式(**)
  219.             self.text.setPlainText(text[:end] + '*' + text[start:end] + '*' + text[end:])
  220.         self.text.setSelection(start + 1 + bbcode * 2, end + 1 + bbcode * 2)

  221.     def font_(self):
  222.         """字体设置界面"""
  223.         dialog = QFontDialog(self)
  224.         dialog.setFont(self.font8)
  225.         dialog.setCurrentFont(self.text.font())
  226.         dialog.accepted.connect(lambda: self.text.setFont(dialog.currentFont()))
  227.         dialog.exec()

  228.     def convert(self):
  229.         """将输入框里的内容视情况转换为 BBCode/Markdown"""
  230.         if self.markdownAction.isChecked():
  231.             # 转换为 BBCode
  232.             self.toBBCode()
  233.             # 可以直接“触发”相关事件,省去了很多麻烦
  234.             self.bbcodeAction.trigger()
  235.         else:
  236.             # 转换为 Markdown
  237.             self.toMarkdown()
  238.             self.markdownAction.trigger()

  239.     def toBBCode(self):
  240.         """将输入框里的内容转换为 BBCode"""
  241.         md = self.text.toPlainText().splitlines()
  242.         res = ''
  243.         # 0 表示没有特殊格式;1 表示有序列表;2 表示无序列表;3 表示代码块
  244.         flag = 0
  245.         for s in md:
  246.             if not s:
  247.                 if flag in (1, 2):
  248.                     # 结束有序/无序列表
  249.                     res += '[/list]\n'
  250.                     # 终于找到这个 bug 的源头了,结束之后程序还会尝试继续结束。。。
  251.                     # 把下面这行加上就没事了
  252.                     flag = 0
  253.                 # 空行
  254.                 res += '\n'
  255.                 continue
  256.             if s[:3] == '1. ':
  257.                 # 进入有序列表
  258.                 res += '[list=1]\n'
  259.                 flag = 1
  260.             elif s[:2] in ('- ', '* ', '+ ') and flag != 2:
  261.                 # 进入无序列表
  262.                 res += '[list]\n'
  263.                 flag = 2
  264.             elif s[:3] == '```' and flag < 3:
  265.                 # 进入代码块(不换行)
  266.                 res += '[<避免被解析,请把这个删了>code]'
  267.                 flag = 3
  268.                 continue
  269.             if flag == 1:
  270.                 # 有序列表
  271.                 s = re.sub(r'^\d+\. (.*)$', r'[*]\1', s)
  272.             elif flag == 2:
  273.                 # 无序列表
  274.                 s = re.sub(r'^[-*+] (.*)$', r'[*]\1', s)
  275.             elif flag == 3:
  276.                 # 代码块
  277.                 if s == '```':
  278.                     # 可以离开代码块了
  279.                     s = '[<避免被解析,请把这个删了>/code]'
  280.                     flag = 0
  281.                 res += s + '\n'
  282.                 # 代码块期间啥都不能做(其实离开代码块的那一行也啥都不能做)
  283.                 continue
  284.             # 标题的格式来源于 [url]https://fishc.com.cn/thread-146275-1-1.html[/url]
  285.             # 一级标题
  286.             s = re.sub(r'^# (.*)$', r'[align=center][size=5][b]\1[/b][/size][/align]', s)
  287.             # 二级标题
  288.             s = re.sub(r'^## (.*)$', r'[size=4][b]\1[/b][/size]', s)
  289.             # 三级标题
  290.             s = re.sub(r'^### (.*)$', r'[size=3][backcolor=DarkSlateGray][color=White]\1[/color][/backcolor][/size]', s)
  291.             # 引用
  292.             s = re.sub(r'^> (.*)$', r'[quote]\1[/quote]', s)
  293.             # 非贪婪匹配:'*1* *2*' 会替换成 '[i]1[/i] [i]2[/i]' 而不是 '[i]1* *2[/i]'
  294.             # 加粗
  295.             s = re.sub(r'\*\*(.*?)\*\*', r'[b]\1[/b]', s)
  296.             # 斜体
  297.             s = re.sub(r'\*(.*?)\*', r'[i]\1[/i]', s)
  298.             # 行内代码
  299.             s = re.sub(r'`(.*?)`', r'[backcolor=LightGray]\1[/backcolor]', s)
  300.             # 删除线
  301.             s = re.sub(r'~~(.*?)~~', r'[s]\1[/s]', s)
  302.             # 图片
  303.             s = re.sub(r'!\[.*?]\((.*?)\)', r'[img]\1[/img]', s)
  304.             # 链接
  305.             s = re.sub(r'\[(.*?)]\((.*?)\)', r'[url=\2]\1[/url]', s)
  306.             res += s + '\n'
  307.         if flag in (1, 2):
  308.             # 还没结束有序/无序列表
  309.             res += '[/list]\n'
  310.         res = re.sub(r'\n{3,}', '\n\n', res)
  311.         self.text.setPlainText(res)

  312.     def toMarkdown(self):
  313.         """将输入框里的内容转换为 Markdown"""
  314.         bbcode = self.text.toPlainText().splitlines()
  315.         res = ''
  316.         # 0 表示没有特殊格式;1 表示有序列表;2 表示无序列表;3 表示代码块;4 表示引用
  317.         flag = 0
  318.         # 有序列表的索引
  319.         index = 0
  320.         for s in bbcode:
  321.             if not s:
  322.                 # 空行
  323.                 res += '\n'
  324.                 continue
  325.             if s == '[list=1]':
  326.                 # 进入有序列表
  327.                 flag = 1
  328.                 continue
  329.             elif s == '[list]':
  330.                 # 进入无序列表
  331.                 flag = 2
  332.                 continue
  333.             elif s == '[/list]':
  334.                 # 退出有序/无序列表
  335.                 flag = 0
  336.                 continue
  337.             elif s[:5] == '[<避免被解析,请把这个删了>code]':
  338.                 # 进入代码块
  339.                 res += '```\n'
  340.                 flag = 3
  341.             elif s[:7] == '[quote]' or (flag == 4):
  342.                 # 引用区域
  343.                 s = re.sub(r'^(\[quote])?(.*)$', r'> \2', s)
  344.                 flag = 4
  345.             # 可以在一行的末尾退出代码块和引用
  346.             if s[-7:] == '[<避免被解析,请把这个删了>/code]':
  347.                 s = s[:-7]
  348.                 flag = 0
  349.             elif s[-8:] == '[/quote]':
  350.                 s = s[:-8]
  351.                 flag = 0
  352.             if flag == 1:
  353.                 # 有序列表
  354.                 s = re.sub(r'^\[\*] (.*)$', fr'{index}. \1', s)
  355.             elif flag == 2:
  356.                 # 无序列表
  357.                 s = re.sub(r'^\[\*] (.*)$', r'- \1', s)
  358.             elif flag == 3:
  359.                 # 代码块
  360.                 res += s + '\n'
  361.                 # 代码块期间啥都不能做(其实离开代码块的那一行也啥都不能做)
  362.                 continue
  363.             # 不需要处理引用
  364.             # 一级标题
  365.             s = re.sub(r'^\[align=center]\[size=5]\[b](.*)\[/b]\[/size]\[/align]$', r'# \1', s)
  366.             # 二级标题
  367.             s = re.sub(r'^\[size=4]\[b](.*)\[/b]\[/size]$', r'## \1', s)
  368.             # 三级标题
  369.             s = re.sub(r'^\[size=3]\[backcolor=DarkSlateGray]\[color=White](.*)\[/color]\[/backcolor]\[/size]$', r'### \1', s)
  370.             # 加粗
  371.             s = re.sub(r'\[b](.*?)\[/b]', r'**\1**', s)
  372.             # 斜体
  373.             s = re.sub(r'\[i](.*?)\[/i]', r'*\1*', s)
  374.             # 行内代码
  375.             s = re.sub(r'\[backcolor=LightGray](.*?)\[/backcolor]', r'`\1`', s)
  376.             # 删除线
  377.             s = re.sub(r'\[s](.*?)\[/s]', r'~~\1~~', s)
  378.             # 图片
  379.             s = re.sub(r'!\[img](.*?)\[/img]', r'![](\1)', s)
  380.             # 链接
  381.             s = re.sub(r'\[url=(.*?)](.*?)\[/url]', r'[\2](\1)', s)
  382.             res += s + '\n'
  383.         res = re.sub(r'\n{3,}', '\n\n', res)
  384.         self.text.setPlainText(res)

  385. if __name__ == '__main__':
  386.     app = QApplication(sys.argv)
  387.     window = MainWindow()
  388.     window.show()
  389.     sys.exit(app.exec())
复制代码


-- 没了 --

感谢支持!

评分.gif

评分

参与人数 2荣誉 +12 鱼币 +12 贡献 +12 收起 理由
小甲鱼 + 6 + 6 + 6 鱼C有你更精彩^_^
不二如是 + 6 + 6 + 6 鱼C有你更精彩^_^

查看全部评分

小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层
好项目
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 3 天前 | 显示全部楼层
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 前天 02:02 | 显示全部楼层
不错呀!
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2025-4-20 00:01

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表