!108 feat: 在「工具」菜单中增加 Markdown 预览

Merge pull request !108 from wushuzhilu/feat/markdown_preview
This commit is contained in:
爬山虎
2026-10-16 04:34:12 +00:00
committed by Gitee
8 changed files with 1522 additions and 5 deletions
+68
View File
@@ -18,6 +18,7 @@
#include "qtlangset.h"
#include "columnedit.h"
#include "langstyledefine.h"
#include "mdrenderer.h"
#include "extlexermanager.h"
#include "aboutndd.h"
#include "filelistview.h"
@@ -1701,6 +1702,14 @@ void CCNotePad::slot_dynamicLoadToolMenu()
pAct = ui.menuTools->addAction(tr("Batch Find"), this, &CCNotePad::slot_batchFind);
pAct->setData(BATCH_FIND);
// Markdown 预览:把当前编辑区内容渲染为 HTML 后单独开窗显示。
// 为什么放在主程序里而不是插件:
// 编辑控件类 ScintillaEditView 定义在本模块(scintillaeditview.h),
// 插件与本程序是两个模块,跨模块的 RTTI / dynamic_cast 不可靠,
// 插件无法安全取得 ui.editTabWidget 里的当前编辑区。
// 在源码内实现则是一行 dynamic_cast,稳定可靠。
ui.menuTools->addAction(tr("Markdown Preview"), this, &CCNotePad::slot_markdownPreview);
#ifdef NO_PLUGIN
//动态加载插件
m_pluginList.clear();
@@ -1885,6 +1894,65 @@ void CCNotePad::loadPluginProcs(QString strLibDir, QMenu* pMenu)
}
#endif
// Markdown 预览:取当前编辑区内容,渲染为 HTML 后单独开窗显示。
//
// 为什么写在主程序内而不是做成插件:
// 编辑控件类 ScintillaEditView 定义在本模块(scintillaeditview.h),
// 而插件与主程序分属两个模块,跨模块的 C++ RTTI(dynamic_cast /
// qobject_cast)不可靠:插件既拿不到 ui.editTabWidget,
// 也无法安全地把标签页控件识别成编辑区。
// 在源码内实现则只需一行 dynamic_cast,稳定可靠。
void CCNotePad::slot_markdownPreview()
{
// 1) 取当前编辑区。
// 与 getCurEditView() 相同的取法,但**不做只读拦截**:
// 只读文档同样应该允许预览内容。
QWidget* pw = ui.editTabWidget->currentWidget();
ScintillaEditView* pEditView = dynamic_cast<ScintillaEditView*>(pw);
if (pEditView == nullptr)
{
QMessageBox::information(this, tr("Markdown Preview"),
tr("No document is open.\n\nOpen a file first, then try again."));
return;
}
// 2) 取全文并渲染为 HTML(自研渲染器,支持 GFM 表格等)
const QString markdown = pEditView->text();
const QString html = MdRenderer::renderToHtml(markdown);
// 3) 复用同一个预览窗口,避免重复开窗。
// 窗口设了 WA_DeleteOnClose,用户关掉后 QPointer 自动置空,
// 下次点击会新建。
if (m_mdPreviewWin.isNull())
{
MarkdownView* pNew = new MarkdownView(this);
pNew->setAttribute(Qt::WA_DeleteOnClose);
m_mdPreviewWin = pNew;
}
MarkdownView* pView = m_mdPreviewWin.data();
if (pView == nullptr)
return;
// 4) 文件名:ndd 把路径存在控件的 "filePath" property 里
QString filePath = getFilePathProperty(pEditView);
QString shortName = filePath;
const int slash = qMax(shortName.lastIndexOf('/'), shortName.lastIndexOf('\\'));
if (slash >= 0)
shortName = shortName.mid(slash + 1);
// 5) 窗口标题(带文件名)。用 tr() 以便跟随界面语言切换。
const QString title = shortName.isEmpty()
? tr("Markdown Preview")
: tr("Markdown Preview - %1").arg(shortName);
pView->setWindowTitle(title);
pView->viewHtml(html);
pView->show();
pView->raise();
pView->activateWindow();
}
//批量查找替换
void CCNotePad::slot_batchFind()
{
+3
View File
@@ -22,6 +22,7 @@
#include "scintillaeditview.h"
#include "findwin.h"
#include "pluginGl.h"
#include "markdownview.h"
//class ScintillaEditView;
@@ -351,6 +352,7 @@ private slots:
void slot_showToolBar(bool);
void slot_dynamicLoadToolMenu();
void slot_batchFind();
void slot_markdownPreview();
void on_loadReceneFile();
void slot_pluginMgr();
#ifdef NO_PLUGIN
@@ -616,6 +618,7 @@ private:
QPointer<QMainWindow> m_batchFindWin;
QPointer<MarkdownView> m_mdPreviewWin;
int m_curIconSize;
int m_curColorIndex;
+90 -3
View File
@@ -1,9 +1,86 @@
#include "markdownview.h"
#include "markdownview.h"
#include <QTextDocument>
#include <QTextEdit>
#include <QPalette>
#include <QColor>
#include <QFont>
#include <QStringList>
#include <QApplication>
// ============================================================
// 预览用样式表
//
// 为什么需要它:
// MdRenderer 只产出 HTML 片段,没有任何样式。
// 直接 setHtml() 会用 QTextEdit 的默认样式渲染,
// 标题没有层级、代码块没有底色、表格没有边框 —— 很丑。
//
// 为什么用 setDefaultStyleSheet 而不是在 HTML 里写 <style>:
// QTextDocument 对 HTML 中 <style> 块的支持不完整,
// setDefaultStyleSheet() 是 Qt 官方推荐做法,稳定生效。
//
// 注意 Qt 的富文本引擎只支持 CSS 的一个子集:
// border-radius / overflow / transform 等无效,别写。
// 有效的主要有 color / background-color / font-* /
// margin / padding / border(表格与块) / text-decoration。
// ============================================================
static QString buildPreviewCss(const QWidget* w)
{
bool dark = false;
if (w != nullptr) {
const QColor bg = w->palette().color(QPalette::Base);
if (bg.isValid())
dark = (bg.lightness() < 128);
}
const QString fg = dark ? QStringLiteral("#dcdcdc") : QStringLiteral("#1e1e1e");
const QString muted = dark ? QStringLiteral("#9a9a9a") : QStringLiteral("#6a6a6a");
const QString codeBg = dark ? QStringLiteral("#252526") : QStringLiteral("#f5f5f5");
const QString codeFg = dark ? QStringLiteral("#e6e6e6") : QStringLiteral("#2b2b2b");
const QString border = dark ? QStringLiteral("#454545") : QStringLiteral("#d8d8d8");
const QString accent = dark ? QStringLiteral("#4fc1ff") : QStringLiteral("#0a66c2");
const QString quoteBg = dark ? QStringLiteral("#2a2a2b") : QStringLiteral("#f7f7f7");
QStringList c;
c << QStringLiteral("body { color: %1; font-size: 11pt; }").arg(fg);
c << QStringLiteral("p { margin: 8px 0; }");
c << QStringLiteral("h1 { font-size: 21pt; font-weight: bold; margin: 16px 0 8px 0; }");
c << QStringLiteral("h2 { font-size: 17pt; font-weight: bold; margin: 14px 0 7px 0; }");
c << QStringLiteral("h3 { font-size: 14pt; font-weight: bold; margin: 12px 0 6px 0; }");
c << QStringLiteral("h4 { font-size: 12pt; font-weight: bold; margin: 10px 0 5px 0; }");
c << QStringLiteral("h5 { font-size: 11pt; font-weight: bold; margin: 9px 0 4px 0; }");
c << QStringLiteral("h6 { font-size: 11pt; font-weight: bold; color: %1; margin: 9px 0 4px 0; }").arg(muted);
c << QStringLiteral("code { font-family: 'Consolas', monospace; font-size: 10pt;"
" background-color: %1; color: %2; }").arg(codeBg, codeFg);
c << QStringLiteral("pre { font-family: 'Consolas', monospace; font-size: 10pt;"
" background-color: %1; color: %2; margin: 10px 0; }").arg(codeBg, codeFg);
c << QStringLiteral("blockquote { color: %1; background-color: %2; margin: 10px 0 10px 12px; }")
.arg(muted, quoteBg);
c << QStringLiteral("ul { margin: 6px 0 6px 22px; }");
c << QStringLiteral("ol { margin: 6px 0 6px 22px; }");
c << QStringLiteral("li { margin: 3px 0; }");
c << QStringLiteral("a { color: %1; text-decoration: none; }").arg(accent);
c << QStringLiteral("hr { color: %1; }").arg(border);
c << QStringLiteral("table { border-collapse: collapse; margin: 10px 0; }");
c << QStringLiteral("th { border: 1px solid %1; padding: 5px 9px; background-color: %2;"
" font-weight: bold; }").arg(border, codeBg);
c << QStringLiteral("td { border: 1px solid %1; padding: 5px 9px; }").arg(border);
c << QStringLiteral("img { }");
c << QStringLiteral(".ndd-empty { color: %1; }").arg(muted);
return c.join(QStringLiteral("\n"));
}
MarkdownView::MarkdownView(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
// 预览区留出内边距,别让文字贴着窗口边缘
ui.textEdit->setLineWrapMode(QTextEdit::WidgetWidth);
ui.textEdit->document()->setDocumentMargin(16);
ui.textEdit->setReadOnly(true);
}
MarkdownView::~MarkdownView()
@@ -22,7 +99,17 @@ void MarkdownView::viewMarkdown(QString& text)
"%1").arg(text);
ui.textEdit->setPlainText(tips);
#else
ui.textEdit->document()->setDefaultStyleSheet(buildPreviewCss(ui.textEdit));
ui.textEdit->setMarkdown(text);
#endif
}
}
// 以 HTML 呈现:内容由自研渲染器 MdRenderer 生成
// (支持 GFM 表格、任务列表等),这里只负责套上样式。
void MarkdownView::viewHtml(const QString& html)
{
// 样式通过 setDefaultStyleSheet 注入,而不是在 HTML 里写 <style>:
// QTextDocument 对 <style> 块支持不完整,这个方式最稳。
ui.textEdit->document()->setDefaultStyleSheet(buildPreviewCss(ui.textEdit));
ui.textEdit->setHtml(html);
}
+11 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include <QMainWindow>
#include "ui_markdownview.h"
@@ -12,6 +12,16 @@ public:
~MarkdownView();
void viewMarkdown(QString& text);
// 以 HTML 呈现。
//
// 为什么需要这个入口:
// viewMarkdown() 走 QTextDocument::setMarkdown(),
// 渲染能力受 Qt 内置实现限制(GFM 表格、任务列表支持有限)。
// 本工程自带自研渲染器 MdRenderer(src/mdrenderer.*),
// 覆盖 GFM 表格 / 任务列表 / 嵌套列表等,先转成 HTML
// 再交给这里显示,效果更好。
void viewHtml(const QString& html);
private:
Ui::MarkdownViewClass ui;
};
+1296
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
// ============================================================
// mdrenderer.h —— Markdown -> HTML 渲染器
//
// 定位:给 ndd 预览窗口用的自包含渲染器。
// 不依赖任何第三方库(md4c / marked / discmark 等),
// 全部在Qt 内做,避免额外DLL 与部署复杂度。
//
// 已覆盖语法:
// ATX 标题 # ~######、Setext 标题(=== / ---)
// 段落、硬换行(行尾两空格 / 反斜杠)
// 无序列表 - * +、有序列表 1.、任务列表 - [ ] / - [x]
// 嵌套列表(按缩进递归)、引用块 >(可嵌套)
// 围栏代码块 ``` / ~~~(含语言标注)
// 缩进式代码块(4 空格)
// GFM 表格(含 :---: 对齐、行内转义竖线)
// 水平分割线 --- *** ___
// 行内:**粗体** *斜体* ***粗斜体*** `行内码`
// ~~删除线~~ [文本](链接) ![图片](链接)
// 自动链接(尖括号包裹的地址)与裸 URL
// 反转义 \` \* \_ 等
// HTML 实体透传
// ============================================================
#pragma once
#include <QString>
#include <QStringList>
namespace MdRenderer {
// 把 Markdown 文本渲染为 HTML 片段(不含 <html><head>)。
QString renderToHtml(const QString& markdown);
} // namespace MdRenderer
Binary file not shown.
+20 -1
View File
@@ -154,6 +154,25 @@
<source>BatchFindReplace</source>
<translation type="unfinished">批量查找替换</translation>
</message>
<message>
<location filename="cceditor/ccnotepad.cpp" line="1711"/>
<source>Markdown Preview</source>
<translation type="unfinished">Markdown 预览</translation>
</message>
<message>
<location filename="cceditor/ccnotepad.cpp" line="1947"/>
<source>Markdown Preview - %1</source>
<translation type="unfinished">Markdown 预览 - %1</translation>
</message>
<message>
<location filename="cceditor/ccnotepad.cpp" line="1915"/>
<source>No document is open.
Open a file first, then try again.</source>
<translation type="unfinished">当前没有打开文档。
请先打开一个文件再试。</translation>
</message>
<message>
<location filename="batchfindreplace.ui" line="37"/>
<location filename="ui_batchfindreplace.h" line="196"/>
@@ -6081,7 +6100,7 @@ Double-click a column item to modify the syntax association file.</source>
<location filename="markdownview.ui" line="14"/>
<location filename="ui_markdownview.h" line="71"/>
<source>MarkdownView</source>
<translation type="unfinished"></translation>
<translation type="unfinished">Markdown 预览</translation>
</message>
</context>
<context>