diff --git a/README.md b/README.md
index 95205e4..d5573dc 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。
-当前版本 **0.2.0**。
+当前版本 **0.3.0**。
## 界面
@@ -20,6 +20,7 @@
链接与图片、引用、有序 / 无序列表、任务列表、分隔线
- `Ctrl+1` ~ `Ctrl+6` 把选中行设成 1–6 级标题,再按一次变回正文
- `Ctrl+/` 把选中内容包成围栏代码块,再按一次拆掉
+- 默认「编辑模式」隐藏 Markdown 标记、标题按级别放大,光标所在行露出原始标记
- 行号栏、当前行高亮
- `Tab` / `Shift+Tab` 整行缩进,选中多行时批量缩进
- 自动换行(`Alt+Z`)、`Ctrl+滚轮` 或 `Ctrl++` / `Ctrl+-` 缩放字号
@@ -30,15 +31,19 @@
- **文件 → 打开文件夹…**(`Ctrl+Shift+O`)选一个目录,递归扫描
`*.md` / `*.markdown` / `*.mdown` / `*.mkd`
-- `Ctrl+B` 显示 / 隐藏侧栏
-- 点文件名即打开,当前文件在列表里高亮;右上角「刷新」重新扫描
+- 状态栏左下角的「侧栏」按钮或 `Ctrl+B` 显示 / 隐藏
+- 点文件名即打开,当前文件在列表里高亮
+- 每 5 秒自动扫描一次:目录里新增 / 删除的 Markdown 会自动出现在列表里
- 上次打开的文件夹会被记住,下次启动自动恢复
-### 源代码模式
+### 编辑模式 / 源代码模式
-左下角状态栏的 **``** 按钮(或 `Ctrl+Shift+M`)切到源代码模式:
-关掉语法高亮、行号与当前行高亮,只显示文件原始内容,适合核对格式或复制纯文本。
-默认是编辑模式,按钮会显示成 `` 表示当前处于源码模式。
+默认是**编辑模式**:Markdown 标记不显示——标题按级别放大加粗、粗体真加粗、
+行内代码带底色、链接只留文字,看到的就是排版后的内容。光标移到哪一行,
+那一行会露出 `#`、`**` 这些原始标记,可以直接修改。
+
+点左下角的 **``**(或 `Ctrl+Shift+M`)切到**源代码模式**:所有标记原样显示,
+只做着色,适合核对格式或复制纯文本;按钮会变成 ``。
### 文件
diff --git a/VoiletMark.pro b/VoiletMark.pro
index 7638386..efb8c0d 100644
--- a/VoiletMark.pro
+++ b/VoiletMark.pro
@@ -1,16 +1,18 @@
TEMPLATE = app
TARGET = VoiletMark
-VERSION = 0.2.0
+VERSION = 0.3.0
QT += core gui widgets
CONFIG += c++17 warn_on
-# 让 main.cpp 里的 QStringLiteral(VM_VERSION) 拿到 "0.1.0"
+# 让 main.cpp 里的 QStringLiteral(VM_VERSION) 拿到版本号字符串
DEFINES += VM_VERSION=\\\"$$VERSION\\\"
INCLUDEPATH += $$PWD/src
+RESOURCES += resources.qrc
+
SOURCES += \
src/main.cpp \
src/MainWindow.cpp \
diff --git a/docs/screenshot-dark.png b/docs/screenshot-dark.png
index 64a947d..3bdb14b 100644
Binary files a/docs/screenshot-dark.png and b/docs/screenshot-dark.png differ
diff --git a/docs/screenshot-light.png b/docs/screenshot-light.png
index a35150e..f20163b 100644
Binary files a/docs/screenshot-light.png and b/docs/screenshot-light.png differ
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..d0374c9
Binary files /dev/null and b/logo.png differ
diff --git a/resources.qrc b/resources.qrc
new file mode 100644
index 0000000..90517ff
--- /dev/null
+++ b/resources.qrc
@@ -0,0 +1,5 @@
+
+
+ logo.png
+
+
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index c9f6900..ff0b30b 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -193,6 +193,10 @@ void MainWindow::createActions()
connect(m_sidebarAction, &QAction::toggled, this, [this](bool visible) {
if (m_sidebar)
m_sidebar->setVisible(visible);
+ if (m_sidebarButton) {
+ const QSignalBlocker blocker(m_sidebarButton);
+ m_sidebarButton->setChecked(visible);
+ }
QSettings().setValue(QStringLiteral("window/sidebarVisible"), visible);
});
@@ -230,16 +234,9 @@ void MainWindow::createSidebar()
layout->setContentsMargins(8, 8, 8, 8);
layout->setSpacing(6);
- QHBoxLayout *header = new QHBoxLayout();
- header->setSpacing(4);
m_folderLabel = new QLabel(tr("未打开文件夹"), content);
m_folderLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
- QToolButton *refreshButton = new QToolButton(content);
- refreshButton->setText(tr("刷新"));
- refreshButton->setToolTip(tr("重新扫描文件夹里的 Markdown 文件"));
- header->addWidget(m_folderLabel, 1);
- header->addWidget(refreshButton);
- layout->addLayout(header);
+ layout->addWidget(m_folderLabel);
m_fileList = new QListWidget(content);
m_fileList->setFrameShape(QFrame::NoFrame);
@@ -252,8 +249,13 @@ void MainWindow::createSidebar()
addDockWidget(Qt::LeftDockWidgetArea, m_sidebar);
resizeDocks({m_sidebar}, {250}, Qt::Horizontal);
- connect(refreshButton, &QToolButton::clicked, this, &MainWindow::refreshFolder);
connect(m_fileList, &QListWidget::itemClicked, this, &MainWindow::openSidebarItem);
+
+ // 列表自己刷新:没有“刷新”按钮,每 5 秒比对一次文件集合,变了才重建。
+ m_folderRefreshTimer = new QTimer(this);
+ m_folderRefreshTimer->setInterval(5000);
+ connect(m_folderRefreshTimer, &QTimer::timeout, this, &MainWindow::refreshFolder);
+ m_folderRefreshTimer->start();
}
void MainWindow::createMenus()
@@ -330,7 +332,16 @@ void MainWindow::createMenus()
void MainWindow::createStatusBar()
{
- // 左下角的 开关:切到源代码模式。
+ // 左下角:侧栏开关 + 源代码模式开关。
+ m_sidebarButton = new QToolButton(this);
+ m_sidebarButton->setText(tr("侧栏"));
+ m_sidebarButton->setCheckable(true);
+ m_sidebarButton->setChecked(true);
+ m_sidebarButton->setCursor(Qt::PointingHandCursor);
+ m_sidebarButton->setToolTip(tr("显示 / 隐藏文件夹侧栏(Ctrl+B)"));
+ statusBar()->addWidget(m_sidebarButton);
+ connect(m_sidebarButton, &QToolButton::toggled, m_sidebarAction, &QAction::setChecked);
+
m_sourceButton = new QToolButton(this);
m_sourceButton->setText(QStringLiteral(""));
m_sourceButton->setCheckable(true);
@@ -719,24 +730,13 @@ void MainWindow::openFolder(const QString &path)
m_sidebarAction->setChecked(true);
}
-void MainWindow::refreshFolder()
+QStringList MainWindow::scanFolder() const
{
- if (!m_fileList)
- return;
-
- m_fileList->clear();
-
- if (m_folderPath.isEmpty() || !QFileInfo(m_folderPath).isDir()) {
- m_folderLabel->setText(tr("未打开文件夹"));
- m_folderLabel->setToolTip(tr("文件 → 打开文件夹…"));
- return;
- }
+ QStringList files;
+ if (m_folderPath.isEmpty() || !QFileInfo(m_folderPath).isDir())
+ return files;
const QDir root(m_folderPath);
- m_folderLabel->setText(root.dirName().isEmpty() ? m_folderPath : root.dirName());
- m_folderLabel->setToolTip(QDir::toNativeSeparators(m_folderPath));
-
- QStringList files;
QDirIterator iterator(m_folderPath,
{QStringLiteral("*.md"), QStringLiteral("*.markdown"),
QStringLiteral("*.mdown"), QStringLiteral("*.mkd")},
@@ -753,6 +753,47 @@ void MainWindow::refreshFolder()
return root.relativeFilePath(left).compare(root.relativeFilePath(right),
Qt::CaseInsensitive) < 0;
});
+ return files;
+}
+
+void MainWindow::refreshFolder()
+{
+ if (!m_fileList)
+ return;
+
+ if (m_folderPath.isEmpty() || !QFileInfo(m_folderPath).isDir()) {
+ if (m_fileList->count() > 0 || m_displayedFolder != m_folderPath) {
+ m_fileList->clear();
+ m_displayedFolder = m_folderPath;
+ m_folderLabel->setText(tr("未打开文件夹"));
+ m_folderLabel->setToolTip(tr("文件 → 打开文件夹…"));
+ }
+ return;
+ }
+
+ const QDir root(m_folderPath);
+ if (m_displayedFolder != m_folderPath) {
+ m_displayedFolder = m_folderPath;
+ m_fileList->clear();
+ m_folderLabel->setText(root.dirName().isEmpty() ? m_folderPath : root.dirName());
+ m_folderLabel->setToolTip(QDir::toNativeSeparators(m_folderPath));
+ }
+
+ const QStringList files = scanFolder();
+
+ // 和当前显示的内容一致就什么都不做,避免每 5 秒闪一下、丢失选中项。
+ QStringList shown;
+ shown.reserve(m_fileList->count());
+ for (int index = 0; index < m_fileList->count(); ++index) {
+ const QString path = m_fileList->item(index)->data(Qt::UserRole).toString();
+ if (!path.isEmpty())
+ shown.append(path);
+ }
+ if (files == shown)
+ return;
+
+ const QString selectedPath = m_currentPath;
+ m_fileList->clear();
for (const QString &filePath : files) {
QListWidgetItem *item = new QListWidgetItem(root.relativeFilePath(filePath), m_fileList);
@@ -765,7 +806,7 @@ void MainWindow::refreshFolder()
empty->setFlags(Qt::NoItemFlags);
}
- selectInSidebar(m_currentPath);
+ selectInSidebar(selectedPath);
}
void MainWindow::openSidebarItem(QListWidgetItem *item)
diff --git a/src/MainWindow.h b/src/MainWindow.h
index e5091bd..f6f8ac7 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -54,6 +54,7 @@ private:
void refreshFolder();
void openSidebarItem(QListWidgetItem *item);
void selectInSidebar(const QString &path);
+ QStringList scanFolder() const;
void setSourceModeEnabled(bool enabled);
void changeZoom(int direction);
@@ -76,7 +77,10 @@ private:
QListWidget *m_fileList = nullptr;
QLabel *m_folderLabel = nullptr;
QToolButton *m_sourceButton = nullptr;
+ QToolButton *m_sidebarButton = nullptr;
+ QTimer *m_folderRefreshTimer = nullptr;
QString m_folderPath;
+ QString m_displayedFolder;
QString m_currentPath;
Eol m_eol = Eol::Lf;
diff --git a/src/MarkdownEditor.cpp b/src/MarkdownEditor.cpp
index 555a4ac..58fac33 100644
--- a/src/MarkdownEditor.cpp
+++ b/src/MarkdownEditor.cpp
@@ -74,6 +74,8 @@ MarkdownEditor::MarkdownEditor(QWidget *parent)
m_lineNumberArea = new LineNumberArea(this);
m_highlighter = new MarkdownHighlighter(document());
+ m_activeBlock = textCursor().blockNumber();
+ m_highlighter->setActiveBlock(m_activeBlock);
connect(this, &QPlainTextEdit::blockCountChanged,
this, &MarkdownEditor::updateLineNumberAreaWidth);
@@ -81,6 +83,8 @@ MarkdownEditor::MarkdownEditor(QWidget *parent)
this, &MarkdownEditor::updateLineNumberArea);
connect(this, &QPlainTextEdit::cursorPositionChanged,
this, &MarkdownEditor::updateCurrentLineHighlight);
+ connect(this, &QPlainTextEdit::cursorPositionChanged,
+ this, &MarkdownEditor::updateActiveBlock);
connect(ThemeManager::instance(), &ThemeManager::schemeChanged,
this, &MarkdownEditor::applyTheme);
@@ -159,11 +163,15 @@ void MarkdownEditor::applyEditorFont()
document()->setDefaultFont(font);
setTabStopDistance(QFontMetricsF(font).horizontalAdvance(QLatin1Char(' ')) * 4.0);
updateLineNumberAreaWidth(0);
+
+ // 标题字号是根据正文大小算的,缩放后要让高亮层重建格式。
+ if (m_highlighter)
+ m_highlighter->refreshAppearance();
}
void MarkdownEditor::updateLineNumberAreaWidth(int)
{
- setViewportMargins(m_sourceMode ? 0 : lineNumberAreaWidth(), 0, 0, 0);
+ setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
}
void MarkdownEditor::updateLineNumberArea(const QRect &rect, int dy)
@@ -179,11 +187,6 @@ void MarkdownEditor::updateLineNumberArea(const QRect &rect, int dy)
void MarkdownEditor::updateCurrentLineHighlight()
{
- if (m_sourceMode) {
- setExtraSelections({});
- return;
- }
-
QTextEdit::ExtraSelection selection;
selection.format.setBackground(ThemeManager::instance()->colors().currentLine);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
@@ -205,24 +208,34 @@ void MarkdownEditor::setSourceMode(bool enabled)
return;
m_sourceMode = enabled;
- m_highlighter->setHighlightingEnabled(!enabled);
-
- if (enabled)
- setExtraSelections({});
- else
- updateCurrentLineHighlight();
-
- m_lineNumberArea->setVisible(!enabled);
- updateLineNumberAreaWidth(0);
-
- const QRect rect = contentsRect();
- m_lineNumberArea->setGeometry(QRect(rect.left(), rect.top(),
- lineNumberAreaWidth(), rect.height()));
+ m_activeBlock = textCursor().blockNumber();
+ m_highlighter->setActiveBlock(m_activeBlock);
+ m_highlighter->setRendered(!enabled);
+ updateCurrentLineHighlight();
viewport()->update();
emit sourceModeChanged(enabled);
}
+void MarkdownEditor::updateActiveBlock()
+{
+ if (m_sourceMode)
+ return;
+
+ const int blockNumber = textCursor().blockNumber();
+ if (blockNumber == m_activeBlock)
+ return;
+
+ const QTextBlock previous = document()->findBlockByNumber(m_activeBlock);
+ m_activeBlock = blockNumber;
+ m_highlighter->setActiveBlock(blockNumber);
+
+ // 只重排两行:离开的那行重新隐藏标记,进入的那行露出来。
+ if (previous.isValid())
+ m_highlighter->rehighlightBlock(previous);
+ m_highlighter->rehighlightBlock(textCursor().block());
+}
+
void MarkdownEditor::selectedBlockRange(int *firstBlock, int *lastBlock) const
{
const QTextCursor cursor = textCursor();
diff --git a/src/MarkdownEditor.h b/src/MarkdownEditor.h
index 51d2996..13e754f 100644
--- a/src/MarkdownEditor.h
+++ b/src/MarkdownEditor.h
@@ -48,6 +48,7 @@ private slots:
void updateLineNumberAreaWidth(int newBlockCount);
void updateLineNumberArea(const QRect &rect, int dy);
void updateCurrentLineHighlight();
+ void updateActiveBlock();
void applyTheme();
private:
@@ -63,4 +64,5 @@ private:
MarkdownHighlighter *m_highlighter = nullptr;
int m_fontPointSize = 12;
bool m_sourceMode = false;
+ int m_activeBlock = -1;
};
diff --git a/src/MarkdownHighlighter.cpp b/src/MarkdownHighlighter.cpp
index 4b6fa82..62eb268 100644
--- a/src/MarkdownHighlighter.cpp
+++ b/src/MarkdownHighlighter.cpp
@@ -2,7 +2,9 @@
#include "ThemeManager.h"
+#include
#include
+#include
namespace {
constexpr int StateNormal = 0;
@@ -19,13 +21,45 @@ MarkdownHighlighter::MarkdownHighlighter(QTextDocument *document)
});
}
+void MarkdownHighlighter::setRendered(bool rendered)
+{
+ if (m_rendered == rendered)
+ return;
+ m_rendered = rendered;
+ rehighlight();
+}
+
+void MarkdownHighlighter::setActiveBlock(int blockNumber)
+{
+ m_activeBlock = blockNumber;
+}
+
+void MarkdownHighlighter::refreshAppearance()
+{
+ rebuildFormats();
+ rehighlight();
+}
+
+bool MarkdownHighlighter::isActiveLine() const
+{
+ return m_rendered && currentBlock().blockNumber() == m_activeBlock;
+}
+
void MarkdownHighlighter::rebuildFormats()
{
const ThemeColors &c = ThemeManager::instance()->colors();
- m_headingFormat = QTextCharFormat();
- m_headingFormat.setForeground(c.heading);
- m_headingFormat.setFontWeight(QFont::DemiBold);
+ qreal base = 12.0;
+ if (document() && document()->defaultFont().pointSizeF() > 0.5)
+ base = document()->defaultFont().pointSizeF();
+
+ static const qreal scales[6] = {1.75, 1.50, 1.30, 1.14, 1.04, 1.0};
+ for (int level = 0; level < 6; ++level) {
+ m_headingFormats[level] = QTextCharFormat();
+ m_headingFormats[level].setForeground(c.heading);
+ m_headingFormats[level].setFontWeight(QFont::Bold);
+ m_headingFormats[level].setFontPointSize(base * scales[level]);
+ }
m_headingMarkerFormat = QTextCharFormat();
m_headingMarkerFormat.setForeground(c.headingMarker);
@@ -34,6 +68,11 @@ void MarkdownHighlighter::rebuildFormats()
m_strongFormat.setForeground(c.strong);
m_strongFormat.setFontWeight(QFont::Bold);
+ m_boldItalicFormat = QTextCharFormat();
+ m_boldItalicFormat.setForeground(c.strong);
+ m_boldItalicFormat.setFontWeight(QFont::Bold);
+ m_boldItalicFormat.setFontItalic(true);
+
m_emphasisFormat = QTextCharFormat();
m_emphasisFormat.setForeground(c.emphasis);
m_emphasisFormat.setFontItalic(true);
@@ -46,6 +85,7 @@ void MarkdownHighlighter::rebuildFormats()
m_linkFormat = QTextCharFormat();
m_linkFormat.setForeground(c.link);
+ m_linkFormat.setFontUnderline(true);
m_urlFormat = QTextCharFormat();
m_urlFormat.setForeground(c.linkUrl);
@@ -74,125 +114,234 @@ void MarkdownHighlighter::rebuildFormats()
m_strikeFormat.setForeground(c.quote);
}
+QTextCharFormat MarkdownHighlighter::compose(const QTextCharFormat &specific) const
+{
+ QTextCharFormat result = m_blockBase;
+ result.merge(specific);
+ return result;
+}
+
+QTextCharFormat MarkdownHighlighter::hiddenFormat(const QColor &background) const
+{
+ // 用“和背景同色 + 极小字号”把标记藏起来:文字仍然留在文档里,
+ // 光标移回来时重新显示,所以不影响编辑和撤销。
+ QTextCharFormat format;
+ format.setForeground(background);
+ format.setFontPointSize(0.5);
+ return format;
+}
+
void MarkdownHighlighter::apply(int start, int length, const QTextCharFormat &format)
{
if (length > 0)
setFormat(start, length, format);
}
-void MarkdownHighlighter::applyPattern(const QRegularExpression &pattern, const QString &text,
- const QTextCharFormat &format)
+void MarkdownHighlighter::applyDelimited(const QRegularExpression &pattern, const QString &text,
+ const QTextCharFormat &contentFormat, int contentGroup,
+ const QColor &hideAgainst)
{
- auto it = pattern.globalMatch(text);
- while (it.hasNext()) {
- const QRegularExpressionMatch match = it.next();
- apply(match.capturedStart(), match.capturedLength(), format);
- }
-}
+ auto iterator = pattern.globalMatch(text);
+ while (iterator.hasNext()) {
+ const QRegularExpressionMatch match = iterator.next();
+ const int start = match.capturedStart(contentGroup);
+ const int length = match.capturedLength(contentGroup);
+ if (length <= 0)
+ continue;
-void MarkdownHighlighter::setHighlightingEnabled(bool enabled)
-{
- if (m_enabled == enabled)
- return;
- m_enabled = enabled;
- rehighlight();
+ if (m_rendered && !isActiveLine()) {
+ apply(start, length, compose(contentFormat));
+ const int matchStart = match.capturedStart();
+ const int matchEnd = matchStart + match.capturedLength();
+ if (start > matchStart)
+ apply(matchStart, start - matchStart, compose(hiddenFormat(hideAgainst)));
+ if (matchEnd > start + length)
+ apply(start + length, matchEnd - start - length,
+ compose(hiddenFormat(hideAgainst)));
+ } else {
+ // 源码模式,或光标正停在这一行:整段按内容样式显示,标记保留。
+ apply(match.capturedStart(), match.capturedLength(), compose(contentFormat));
+ }
+ }
}
void MarkdownHighlighter::highlightBlock(const QString &text)
{
setCurrentBlockState(StateNormal);
- if (!m_enabled || text.isEmpty())
+ m_blockBase = QTextCharFormat();
+ if (text.isEmpty())
return;
+ const ThemeColors &c = ThemeManager::instance()->colors();
+ const bool rendered = m_rendered;
+ const bool active = isActiveLine();
+
static const QRegularExpression fencePattern(QStringLiteral("^\\s{0,3}(?:```|~~~)"));
+ const bool inFence = previousBlockState() == StateInFence;
+ const bool fenceLine = fencePattern.match(text).hasMatch();
- // ---- 围栏代码块:跨行状态机 ----------------------------------------
- if (previousBlockState() == StateInFence) {
- apply(0, text.length(), m_codeFormat);
- if (!fencePattern.match(text).hasMatch())
+ // ---- 围栏代码块 -----------------------------------------------------
+ if (inFence) {
+ if (!fenceLine)
setCurrentBlockState(StateInFence);
+ if (rendered && fenceLine && !active) {
+ apply(0, text.length(), hiddenFormat(c.editorBase));
+ } else {
+ m_blockBase = m_codeFormat;
+ apply(0, text.length(), m_codeFormat);
+ }
return;
}
- if (fencePattern.match(text).hasMatch()) {
- apply(0, text.length(), m_codeFormat);
+ if (fenceLine) {
setCurrentBlockState(StateInFence);
+ if (rendered && !active)
+ apply(0, text.length(), hiddenFormat(c.editorBase));
+ else
+ apply(0, text.length(), m_codeFormat);
return;
}
- // ---- 块级语法 ------------------------------------------------------
- static const QRegularExpression headingPattern(
- QStringLiteral("^\\s{0,3}(#{1,6})(?:\\s+|$)"));
+ // ---- 标题 -----------------------------------------------------------
+ static const QRegularExpression headingPattern(QStringLiteral("^\\s{0,3}(#{1,6})(\\s+)"));
const QRegularExpressionMatch heading = headingPattern.match(text);
if (heading.hasMatch()) {
- apply(0, text.length(), m_headingFormat);
- apply(heading.capturedStart(1), heading.capturedLength(1), m_headingMarkerFormat);
+ const int level = qBound(1, heading.capturedLength(1), 6);
+ m_blockBase.merge(m_headingFormats[level - 1]);
+ apply(0, text.length(), m_blockBase);
+
+ const int markerStart = heading.capturedStart(1);
+ const int markerLength = heading.capturedLength(1) + heading.capturedLength(2);
+ if (rendered && !active)
+ apply(markerStart, markerLength, compose(hiddenFormat(c.editorBase)));
+ else
+ apply(markerStart, markerLength, compose(m_headingMarkerFormat));
}
+ // ---- 引用 -----------------------------------------------------------
static const QRegularExpression quotePattern(QStringLiteral("^\\s{0,3}(>+)"));
const QRegularExpressionMatch quote = quotePattern.match(text);
if (quote.hasMatch()) {
- apply(0, text.length(), m_quoteFormat);
- apply(quote.capturedStart(1), quote.capturedLength(1), m_quoteMarkerFormat);
+ m_blockBase.merge(m_quoteFormat);
+ apply(0, text.length(), m_blockBase);
+ if (rendered && !active)
+ apply(quote.capturedStart(1), quote.capturedLength(1),
+ compose(hiddenFormat(c.editorBase)));
+ else
+ apply(quote.capturedStart(1), quote.capturedLength(1), compose(m_quoteMarkerFormat));
}
+ // ---- 分隔线 / 列表 ---------------------------------------------------
static const QRegularExpression rulePattern(
QStringLiteral("^\\s{0,3}(?:(?:\\*\\s*){3,}|(?:-\\s*){3,}|(?:_\\s*){3,})$"));
- const QRegularExpressionMatch rule = rulePattern.match(text);
- if (rule.hasMatch()) {
- apply(0, text.length(), m_ruleFormat);
+ if (rulePattern.match(text).hasMatch()) {
+ apply(0, text.length(), compose(m_ruleFormat));
} else {
static const QRegularExpression taskPattern(
QStringLiteral("^\\s*[-+*]\\s+\\[([ xX])\\]"));
const QRegularExpressionMatch task = taskPattern.match(text);
if (task.hasMatch()) {
- apply(task.capturedStart(), task.capturedLength(), m_listMarkerFormat);
+ apply(task.capturedStart(), task.capturedLength(), compose(m_listMarkerFormat));
if (task.capturedLength(1) > 0) {
- const bool done = task.captured(1).compare(QLatin1String("x"), Qt::CaseInsensitive) == 0;
+ const bool done = task.captured(1).compare(QLatin1String("x"),
+ Qt::CaseInsensitive) == 0;
apply(task.capturedStart(1), task.capturedLength(1),
- done ? m_taskDoneFormat : m_listMarkerFormat);
+ compose(done ? m_taskDoneFormat : m_listMarkerFormat));
}
} else {
static const QRegularExpression listPattern(
QStringLiteral("^(\\s*)([-+*]|\\d{1,9}[.)])(\\s+)"));
const QRegularExpressionMatch list = listPattern.match(text);
if (list.hasMatch())
- apply(list.capturedStart(2), list.capturedLength(2), m_listMarkerFormat);
+ apply(list.capturedStart(2), list.capturedLength(2), compose(m_listMarkerFormat));
}
}
- // ---- 行内语法 ------------------------------------------------------
+ // ---- 链接 / 图片 -----------------------------------------------------
static const QRegularExpression linkPattern(
QStringLiteral("!?\\[([^\\]]*)\\]\\(([^)\\s]*)(?:\\s+\"([^\"]*)\")?\\)"));
auto links = linkPattern.globalMatch(text);
while (links.hasNext()) {
const QRegularExpressionMatch match = links.next();
- apply(match.capturedStart(1), match.capturedLength(1), m_linkFormat);
- apply(match.capturedStart(2), match.capturedLength(2), m_urlFormat);
+ const int labelStart = match.capturedStart(1);
+ const int labelLength = match.capturedLength(1);
+ const int matchEnd = match.capturedStart() + match.capturedLength();
+
+ apply(labelStart, labelLength, compose(m_linkFormat));
+
+ if (rendered && !active) {
+ if (labelStart > match.capturedStart())
+ apply(match.capturedStart(), labelStart - match.capturedStart(),
+ compose(hiddenFormat(c.editorBase)));
+ const int suffixStart = labelStart + labelLength;
+ if (matchEnd > suffixStart)
+ apply(suffixStart, matchEnd - suffixStart,
+ compose(hiddenFormat(c.editorBase)));
+ } else if (match.capturedLength(2) > 0) {
+ apply(match.capturedStart(2), match.capturedLength(2), compose(m_urlFormat));
+ }
}
static const QRegularExpression autoLinkPattern(QStringLiteral("<(https?://[^>]+)>"));
- auto autolinks = autoLinkPattern.globalMatch(text);
- while (autolinks.hasNext()) {
- const QRegularExpressionMatch match = autolinks.next();
- apply(match.capturedStart(1), match.capturedLength(1), m_urlFormat);
+ auto autoLinks = autoLinkPattern.globalMatch(text);
+ while (autoLinks.hasNext()) {
+ const QRegularExpressionMatch match = autoLinks.next();
+ apply(match.capturedStart(1), match.capturedLength(1), compose(m_urlFormat));
+ if (rendered && !active) {
+ apply(match.capturedStart(), 1, compose(hiddenFormat(c.editorBase)));
+ apply(match.capturedStart() + match.capturedLength() - 1, 1,
+ compose(hiddenFormat(c.editorBase)));
+ }
}
+ // ---- 行内强调 --------------------------------------------------------
+ // 内容里禁止出现定界符,否则 **粗体** 会被更宽泛的 *斜体* 规则先吃掉。
+ static const QRegularExpression boldAsterisk(
+ QStringLiteral("\\*\\*(?=\\S)([^*\\n]+?)(?<=\\S)\\*\\*"));
+ applyDelimited(boldAsterisk, text, m_strongFormat, 1, c.editorBase);
+
+ static const QRegularExpression boldUnderscore(
+ QStringLiteral("__(?=\\S)([^_\\n]+?)(?<=\\S)__"));
+ applyDelimited(boldUnderscore, text, m_strongFormat, 1, c.editorBase);
+
+ static const QRegularExpression italicAsterisk(
+ QStringLiteral("(? matchStart)
+ apply(matchStart, contentStart - matchStart, hidden);
+ const int suffixStart = contentStart + contentLength;
+ if (matchEnd > suffixStart)
+ apply(suffixStart, matchEnd - suffixStart, hidden);
+ }
+ }
}
diff --git a/src/MarkdownHighlighter.h b/src/MarkdownHighlighter.h
index 5d3d2fb..3ffa107 100644
--- a/src/MarkdownHighlighter.h
+++ b/src/MarkdownHighlighter.h
@@ -1,14 +1,20 @@
#pragma once
+#include
#include
#include
#include
-/// Markdown 语法着色。
+/// Markdown 着色与“就地渲染”。
///
-/// 只做“逐行着色”,不改动文档内容:标题、引用、列表、行内样式、
-/// 链接以及跨行的围栏代码块。颜色全部取自 ThemeManager,
-/// 主题切换后自动重建格式并重新着色。
+/// 两种形态:
+/// - 渲染模式(默认,对应界面的“编辑器模式”):隐藏 #、**、反引号等标记,
+/// 标题按级别放大、加粗文本真加粗、链接只留文字;光标所在行会露出原始标记,
+/// 方便直接修改。
+/// - 源码模式:原样显示所有标记,只做着色。
+///
+/// 注意:QSyntaxHighlighter::setFormat 是“整段替换”而不是合并,所以这里
+/// 每一段都先和当前行的基础格式(标题 / 引用 / 代码块)合并后再设置。
class MarkdownHighlighter : public QSyntaxHighlighter
{
Q_OBJECT
@@ -16,8 +22,15 @@ class MarkdownHighlighter : public QSyntaxHighlighter
public:
explicit MarkdownHighlighter(QTextDocument *document = nullptr);
- /// 源代码模式会关掉着色,但不改文档内容(rehighlight 会清掉旧格式)。
- void setHighlightingEnabled(bool enabled);
+ void setRendered(bool rendered);
+ bool isRendered() const { return m_rendered; }
+
+ /// 光标所在行;渲染模式下这一行不隐藏标记。
+ void setActiveBlock(int blockNumber);
+ int activeBlock() const { return m_activeBlock; }
+
+ /// 主题或字号变化后重建格式并重新着色。
+ void refreshAppearance();
protected:
void highlightBlock(const QString &text) override;
@@ -25,12 +38,21 @@ protected:
private:
void rebuildFormats();
void apply(int start, int length, const QTextCharFormat &format);
- void applyPattern(const QRegularExpression &pattern, const QString &text,
- const QTextCharFormat &format);
+ void applyDelimited(const QRegularExpression &pattern, const QString &text,
+ const QTextCharFormat &contentFormat, int contentGroup,
+ const QColor &hideAgainst);
+ QTextCharFormat compose(const QTextCharFormat &specific) const;
+ QTextCharFormat hiddenFormat(const QColor &background) const;
+ bool isActiveLine() const;
- QTextCharFormat m_headingFormat;
+ bool m_rendered = true;
+ int m_activeBlock = -1;
+ QTextCharFormat m_blockBase;
+
+ QTextCharFormat m_headingFormats[6];
QTextCharFormat m_headingMarkerFormat;
QTextCharFormat m_strongFormat;
+ QTextCharFormat m_boldItalicFormat;
QTextCharFormat m_emphasisFormat;
QTextCharFormat m_codeFormat;
QTextCharFormat m_linkFormat;
@@ -41,5 +63,4 @@ private:
QTextCharFormat m_ruleFormat;
QTextCharFormat m_taskDoneFormat;
QTextCharFormat m_strikeFormat;
- bool m_enabled = true;
};
diff --git a/src/main.cpp b/src/main.cpp
index a7dd5ea..3f2fa73 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -4,6 +4,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -15,6 +16,7 @@ int main(int argc, char *argv[])
QApplication app(argc, argv);
QApplication::setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
+ QApplication::setWindowIcon(QIcon(QStringLiteral(":/logo.png")));
QCoreApplication::setOrganizationName(QStringLiteral("VoiletMark"));
QCoreApplication::setApplicationName(QStringLiteral("VoiletMark"));