feat: v0.8.0 —— 修复围栏空行断状态,代码块改为真正的语法高亮
修复 - highlightBlock 遇到空行直接 return,把围栏状态重置了:代码块中间只要有一条 空行,后面的行就不再按代码处理(这正是 #include 之后代码变回普通文本的原因) - 空行现在会延续围栏状态;闭合围栏行也不再被误判成新的开围栏 语法高亮 - 新增 CodeHighlight:自己写的轻量 tokenizer,不依赖第三方库 - 支持 C / C++ / Python / JavaScript / TypeScript / Java / Rust / Go / JSON / Bash / SQL / YAML,按 info string 识别 - 关键字、类型、字符串、数字、行/块注释(支持跨行)、C/C++ 预处理指令分色 - 围栏行在源码模式下改成灰色标记色,不再是代码红 - 编辑模式与源代码模式共用同一套 token 着色 其它 - 主题新增 5 个代码 token 颜色(浅色 / 深色各一套) - 版本 0.8.0,README 补充支持语言
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。
|
一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。
|
||||||
|
|
||||||
当前版本 **0.7.0**。
|
当前版本 **0.8.0**。
|
||||||
|
|
||||||
## 界面
|
## 界面
|
||||||
|
|
||||||
@@ -26,6 +26,9 @@
|
|||||||
- 行号栏、当前行高亮
|
- 行号栏、当前行高亮
|
||||||
- 围栏代码块显示成带圆角边框的面板,右上角有「复制」按钮
|
- 围栏代码块显示成带圆角边框的面板,右上角有「复制」按钮
|
||||||
(源代码模式下不显示这些装饰)
|
(源代码模式下不显示这些装饰)
|
||||||
|
- 代码块按语言做语法高亮:C / C++ / Python / JavaScript / TypeScript /
|
||||||
|
Java / Rust / Go / JSON / Bash / SQL / YAML,关键字、字符串、数字、
|
||||||
|
注释、预处理指令分色;不认识的标记语言就按等宽纯文本显示
|
||||||
- `Tab` / `Shift+Tab` 整行缩进,选中多行时批量缩进
|
- `Tab` / `Shift+Tab` 整行缩进,选中多行时批量缩进
|
||||||
- 自动换行(`Alt+Z`)、`Ctrl+滚轮` 或 `Ctrl++` / `Ctrl+-` 缩放字号
|
- 自动换行(`Alt+Z`)、`Ctrl+滚轮` 或 `Ctrl++` / `Ctrl+-` 缩放字号
|
||||||
- 撤销 / 重做、剪切 / 复制 / 粘贴,菜单项会跟随可用状态自动灰显
|
- 撤销 / 重做、剪切 / 复制 / 粘贴,菜单项会跟随可用状态自动灰显
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
TEMPLATE = app
|
TEMPLATE = app
|
||||||
TARGET = VoiletMark
|
TARGET = VoiletMark
|
||||||
VERSION = 0.7.0
|
VERSION = 0.8.0
|
||||||
|
|
||||||
QT += core gui widgets printsupport
|
QT += core gui widgets printsupport
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ SOURCES += \
|
|||||||
src/MarkdownEditor.cpp \
|
src/MarkdownEditor.cpp \
|
||||||
src/MarkdownHighlighter.cpp \
|
src/MarkdownHighlighter.cpp \
|
||||||
src/MarkdownRenderer.cpp \
|
src/MarkdownRenderer.cpp \
|
||||||
|
src/CodeHighlight.cpp \
|
||||||
src/LanguageManager.cpp \
|
src/LanguageManager.cpp \
|
||||||
src/ThemeManager.cpp
|
src/ThemeManager.cpp
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ HEADERS += \
|
|||||||
src/MarkdownEditor.h \
|
src/MarkdownEditor.h \
|
||||||
src/MarkdownHighlighter.h \
|
src/MarkdownHighlighter.h \
|
||||||
src/MarkdownRenderer.h \
|
src/MarkdownRenderer.h \
|
||||||
|
src/CodeHighlight.h \
|
||||||
src/LanguageManager.h \
|
src/LanguageManager.h \
|
||||||
src/ThemeManager.h
|
src/ThemeManager.h
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 114 KiB |
@@ -0,0 +1,334 @@
|
|||||||
|
#include "CodeHighlight.h"
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QSet>
|
||||||
|
|
||||||
|
namespace CodeHighlight {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct LanguageDef
|
||||||
|
{
|
||||||
|
QSet<QString> keywords;
|
||||||
|
QSet<QString> types;
|
||||||
|
QStringList lineComments;
|
||||||
|
QString blockStart;
|
||||||
|
QString blockEnd;
|
||||||
|
QString quotes = QStringLiteral("\"'");
|
||||||
|
bool preprocessor = false;
|
||||||
|
bool caseInsensitive = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
QSet<QString> words(const char *list)
|
||||||
|
{
|
||||||
|
QSet<QString> set;
|
||||||
|
const QString text = QString::fromLatin1(list);
|
||||||
|
for (const QString &word : text.split(QLatin1Char(' '), Qt::SkipEmptyParts))
|
||||||
|
set.insert(word);
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QHash<int, LanguageDef> &definitions()
|
||||||
|
{
|
||||||
|
static const QHash<int, LanguageDef> table = [] {
|
||||||
|
QHash<int, LanguageDef> defs;
|
||||||
|
|
||||||
|
LanguageDef c;
|
||||||
|
c.keywords = words("auto break case const continue default do else enum extern for goto if "
|
||||||
|
"inline long register restrict return short signed sizeof static struct "
|
||||||
|
"switch typedef union unsigned void volatile while _Bool _Static_assert");
|
||||||
|
c.types = words("char double float int size_t ssize_t ptrdiff_t FILE int8_t int16_t int32_t "
|
||||||
|
"int64_t uint8_t uint16_t uint32_t uint64_t bool");
|
||||||
|
c.lineComments = QStringList{QStringLiteral("//")};
|
||||||
|
c.blockStart = QStringLiteral("/*");
|
||||||
|
c.blockEnd = QStringLiteral("*/");
|
||||||
|
c.preprocessor = true;
|
||||||
|
defs.insert(int(Language::C), c);
|
||||||
|
|
||||||
|
LanguageDef cpp = c;
|
||||||
|
cpp.keywords.unite(words("alignas alignof and asm bool catch class concept constexpr const_cast "
|
||||||
|
"co_await co_return co_yield decltype delete dynamic_cast explicit export "
|
||||||
|
"false friend mutable namespace new noexcept not nullptr operator or "
|
||||||
|
"override private protected public reinterpret_cast static_cast template "
|
||||||
|
"this throw true try typeid typename using virtual xor final requires"));
|
||||||
|
cpp.types.unite(words("std string vector map set unique_ptr shared_ptr size_t"));
|
||||||
|
defs.insert(int(Language::Cpp), cpp);
|
||||||
|
|
||||||
|
LanguageDef py;
|
||||||
|
py.keywords = words("and as assert async await break class continue def del elif else except False "
|
||||||
|
"finally for from global if import in is lambda None nonlocal not or pass raise "
|
||||||
|
"return True try while with yield match case");
|
||||||
|
py.types = words("int float str bytes list dict set tuple bool object");
|
||||||
|
py.lineComments = QStringList{QStringLiteral("#")};
|
||||||
|
py.quotes = QStringLiteral("\"'");
|
||||||
|
defs.insert(int(Language::Python), py);
|
||||||
|
|
||||||
|
LanguageDef js;
|
||||||
|
js.keywords = words("async await break case catch class const continue debugger default delete do "
|
||||||
|
"else export extends finally for function if import in instanceof let new of "
|
||||||
|
"return static super switch this throw try typeof var void while with yield "
|
||||||
|
"null true false undefined");
|
||||||
|
js.types = words("String Number Boolean Object Array Promise Map Set JSON Math Date RegExp");
|
||||||
|
js.lineComments = QStringList{QStringLiteral("//")};
|
||||||
|
js.blockStart = QStringLiteral("/*");
|
||||||
|
js.blockEnd = QStringLiteral("*/");
|
||||||
|
js.quotes = QStringLiteral("\"'\x60");
|
||||||
|
defs.insert(int(Language::JavaScript), js);
|
||||||
|
|
||||||
|
LanguageDef ts = js;
|
||||||
|
ts.keywords.unite(words("abstract declare enum implements interface keyof namespace never readonly "
|
||||||
|
"satisfies type unknown infer is asserts override private protected public "
|
||||||
|
"any symbol bigint"));
|
||||||
|
ts.types.unite(words("string number boolean any unknown never void"));
|
||||||
|
defs.insert(int(Language::TypeScript), ts);
|
||||||
|
|
||||||
|
LanguageDef java;
|
||||||
|
java.keywords = words("abstract assert break case catch class const continue default do else enum "
|
||||||
|
"extends final finally for goto if implements import instanceof interface "
|
||||||
|
"native new package private protected public return static strictfp super "
|
||||||
|
"switch synchronized this throw throws transient try volatile while var "
|
||||||
|
"true false null");
|
||||||
|
java.types = words("byte short int long float double char boolean void String Object List Map Set");
|
||||||
|
java.lineComments = QStringList{QStringLiteral("//")};
|
||||||
|
java.blockStart = QStringLiteral("/*");
|
||||||
|
java.blockEnd = QStringLiteral("*/");
|
||||||
|
defs.insert(int(Language::Java), java);
|
||||||
|
|
||||||
|
LanguageDef rust;
|
||||||
|
rust.keywords = words("as async await break const continue crate dyn else enum extern false fn for "
|
||||||
|
"if impl in let loop match mod move mut pub ref return self Self static "
|
||||||
|
"struct super trait true type unsafe use where while yield");
|
||||||
|
rust.types = words("i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 bool char str String Vec Option "
|
||||||
|
"Result Box");
|
||||||
|
rust.lineComments = QStringList{QStringLiteral("//")};
|
||||||
|
rust.blockStart = QStringLiteral("/*");
|
||||||
|
rust.blockEnd = QStringLiteral("*/");
|
||||||
|
defs.insert(int(Language::Rust), rust);
|
||||||
|
|
||||||
|
LanguageDef go;
|
||||||
|
go.keywords = words("break case chan const continue default defer else fallthrough for func go goto "
|
||||||
|
"if import interface map package range return select struct switch type var "
|
||||||
|
"true false nil iota");
|
||||||
|
go.types = words("bool byte complex64 complex128 error float32 float64 int int8 int16 int32 int64 "
|
||||||
|
"rune string uint uint8 uint16 uint32 uint64 uintptr any");
|
||||||
|
go.lineComments = QStringList{QStringLiteral("//")};
|
||||||
|
go.blockStart = QStringLiteral("/*");
|
||||||
|
go.blockEnd = QStringLiteral("*/");
|
||||||
|
go.quotes = QStringLiteral("\"'\x60");
|
||||||
|
defs.insert(int(Language::Go), go);
|
||||||
|
|
||||||
|
LanguageDef json;
|
||||||
|
json.keywords = words("true false null");
|
||||||
|
json.lineComments = QStringList();
|
||||||
|
json.quotes = QStringLiteral("\"");
|
||||||
|
defs.insert(int(Language::Json), json);
|
||||||
|
|
||||||
|
LanguageDef bash;
|
||||||
|
bash.keywords = words("if then else elif fi for while until do done case esac function in select "
|
||||||
|
"time return exit local export readonly declare unset echo printf cd source "
|
||||||
|
"alias set shift trap true false");
|
||||||
|
bash.lineComments = QStringList{QStringLiteral("#")};
|
||||||
|
bash.quotes = QStringLiteral("\"'\x60");
|
||||||
|
defs.insert(int(Language::Bash), bash);
|
||||||
|
|
||||||
|
LanguageDef sql;
|
||||||
|
sql.keywords = words("select from where insert into values update set delete create table drop alter "
|
||||||
|
"index view join left right inner outer full on group by order having limit "
|
||||||
|
"offset union all as and or not null is in like between distinct case when "
|
||||||
|
"then else end exists primary key foreign references default unique check");
|
||||||
|
sql.types = words("int integer bigint smallint varchar char text date time timestamp boolean float "
|
||||||
|
"double decimal numeric");
|
||||||
|
sql.lineComments = QStringList{QStringLiteral("--")};
|
||||||
|
sql.blockStart = QStringLiteral("/*");
|
||||||
|
sql.blockEnd = QStringLiteral("*/");
|
||||||
|
sql.caseInsensitive = true;
|
||||||
|
defs.insert(int(Language::Sql), sql);
|
||||||
|
|
||||||
|
LanguageDef yaml;
|
||||||
|
yaml.keywords = words("true false null yes no on off");
|
||||||
|
yaml.lineComments = QStringList{QStringLiteral("#")};
|
||||||
|
defs.insert(int(Language::Yaml), yaml);
|
||||||
|
|
||||||
|
return defs;
|
||||||
|
}();
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguageDef *definition(Language language)
|
||||||
|
{
|
||||||
|
const auto &defs = definitions();
|
||||||
|
const auto it = defs.constFind(int(language));
|
||||||
|
return it == defs.constEnd() ? nullptr : &it.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Language fromInfoString(const QString &info)
|
||||||
|
{
|
||||||
|
QString name = info.trimmed().toLower();
|
||||||
|
if (name.isEmpty())
|
||||||
|
return Language::None;
|
||||||
|
name = name.section(QLatin1Char(' '), 0, 0);
|
||||||
|
|
||||||
|
static const QHash<QString, Language> aliases = {
|
||||||
|
{QStringLiteral("c"), Language::C},
|
||||||
|
{QStringLiteral("h"), Language::C},
|
||||||
|
{QStringLiteral("cpp"), Language::Cpp},
|
||||||
|
{QStringLiteral("c++"), Language::Cpp},
|
||||||
|
{QStringLiteral("cc"), Language::Cpp},
|
||||||
|
{QStringLiteral("cxx"), Language::Cpp},
|
||||||
|
{QStringLiteral("hpp"), Language::Cpp},
|
||||||
|
{QStringLiteral("python"), Language::Python},
|
||||||
|
{QStringLiteral("py"), Language::Python},
|
||||||
|
{QStringLiteral("javascript"), Language::JavaScript},
|
||||||
|
{QStringLiteral("js"), Language::JavaScript},
|
||||||
|
{QStringLiteral("jsx"), Language::JavaScript},
|
||||||
|
{QStringLiteral("mjs"), Language::JavaScript},
|
||||||
|
{QStringLiteral("typescript"), Language::TypeScript},
|
||||||
|
{QStringLiteral("ts"), Language::TypeScript},
|
||||||
|
{QStringLiteral("tsx"), Language::TypeScript},
|
||||||
|
{QStringLiteral("java"), Language::Java},
|
||||||
|
{QStringLiteral("rust"), Language::Rust},
|
||||||
|
{QStringLiteral("rs"), Language::Rust},
|
||||||
|
{QStringLiteral("go"), Language::Go},
|
||||||
|
{QStringLiteral("golang"), Language::Go},
|
||||||
|
{QStringLiteral("json"), Language::Json},
|
||||||
|
{QStringLiteral("bash"), Language::Bash},
|
||||||
|
{QStringLiteral("sh"), Language::Bash},
|
||||||
|
{QStringLiteral("shell"), Language::Bash},
|
||||||
|
{QStringLiteral("zsh"), Language::Bash},
|
||||||
|
{QStringLiteral("sql"), Language::Sql},
|
||||||
|
{QStringLiteral("yaml"), Language::Yaml},
|
||||||
|
{QStringLiteral("yml"), Language::Yaml},
|
||||||
|
};
|
||||||
|
|
||||||
|
return aliases.value(name, Language::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<Span> tokenize(const QString &text, Language language, bool inBlockComment,
|
||||||
|
bool *newBlockComment)
|
||||||
|
{
|
||||||
|
QVector<Span> spans;
|
||||||
|
if (newBlockComment)
|
||||||
|
*newBlockComment = inBlockComment;
|
||||||
|
|
||||||
|
const LanguageDef *def = definition(language);
|
||||||
|
if (!def)
|
||||||
|
return spans;
|
||||||
|
|
||||||
|
const int length = text.size();
|
||||||
|
bool comment = inBlockComment;
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
if (def->preprocessor) {
|
||||||
|
int start = 0;
|
||||||
|
while (start < length && text.at(start).isSpace())
|
||||||
|
++start;
|
||||||
|
if (start < length && text.at(start) == QLatin1Char('#')) {
|
||||||
|
int end = start + 1;
|
||||||
|
while (end < length
|
||||||
|
&& (text.at(end).isLetterOrNumber() || text.at(end) == QLatin1Char('_')))
|
||||||
|
++end;
|
||||||
|
spans.append({start, end - start, Token::Preprocessor});
|
||||||
|
index = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (index < length) {
|
||||||
|
if (comment) {
|
||||||
|
const int end = text.indexOf(def->blockEnd, index);
|
||||||
|
if (end < 0) {
|
||||||
|
spans.append({index, length - index, Token::Comment});
|
||||||
|
index = length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
spans.append({index, end + def->blockEnd.size() - index, Token::Comment});
|
||||||
|
index = end + def->blockEnd.size();
|
||||||
|
comment = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool lineComment = false;
|
||||||
|
for (const QString &prefix : def->lineComments) {
|
||||||
|
if (text.mid(index, prefix.size()) == prefix) {
|
||||||
|
spans.append({index, length - index, Token::Comment});
|
||||||
|
index = length;
|
||||||
|
lineComment = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lineComment)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (!def->blockStart.isEmpty()
|
||||||
|
&& text.mid(index, def->blockStart.size()) == def->blockStart) {
|
||||||
|
const int end = text.indexOf(def->blockEnd, index + def->blockStart.size());
|
||||||
|
if (end < 0) {
|
||||||
|
spans.append({index, length - index, Token::Comment});
|
||||||
|
comment = true;
|
||||||
|
index = length;
|
||||||
|
} else {
|
||||||
|
spans.append({index, end + def->blockEnd.size() - index, Token::Comment});
|
||||||
|
index = end + def->blockEnd.size();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QChar current = text.at(index);
|
||||||
|
if (def->quotes.contains(current)) {
|
||||||
|
int end = index + 1;
|
||||||
|
while (end < length) {
|
||||||
|
if (text.at(end) == QLatin1Char('\\')) {
|
||||||
|
end += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (text.at(end) == current) {
|
||||||
|
++end;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++end;
|
||||||
|
}
|
||||||
|
end = qMin(end, length);
|
||||||
|
spans.append({index, end - index, Token::String});
|
||||||
|
index = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.isDigit()) {
|
||||||
|
int end = index + 1;
|
||||||
|
while (end < length) {
|
||||||
|
const QChar ch = text.at(end);
|
||||||
|
if (ch.isLetterOrNumber() || ch == QLatin1Char('.') || ch == QLatin1Char('_'))
|
||||||
|
++end;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
spans.append({index, end - index, Token::Number});
|
||||||
|
index = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.isLetter() || current == QLatin1Char('_')) {
|
||||||
|
int end = index + 1;
|
||||||
|
while (end < length
|
||||||
|
&& (text.at(end).isLetterOrNumber() || text.at(end) == QLatin1Char('_')))
|
||||||
|
++end;
|
||||||
|
const QString word = text.mid(index, end - index);
|
||||||
|
const QString probe = def->caseInsensitive ? word.toLower() : word;
|
||||||
|
if (def->types.contains(probe))
|
||||||
|
spans.append({index, end - index, Token::Type});
|
||||||
|
else if (def->keywords.contains(probe))
|
||||||
|
spans.append({index, end - index, Token::Keyword});
|
||||||
|
index = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newBlockComment)
|
||||||
|
*newBlockComment = comment;
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace CodeHighlight
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
/// 围栏代码块里的轻量语法高亮。
|
||||||
|
///
|
||||||
|
/// 不依赖任何第三方库,按语言给出一组关键字 + 注释 / 字符串规则,
|
||||||
|
/// 逐行扫描出 token,由 MarkdownHighlighter 上色。
|
||||||
|
namespace CodeHighlight {
|
||||||
|
|
||||||
|
enum class Language {
|
||||||
|
None = 0,
|
||||||
|
C,
|
||||||
|
Cpp,
|
||||||
|
Python,
|
||||||
|
JavaScript,
|
||||||
|
TypeScript,
|
||||||
|
Java,
|
||||||
|
Rust,
|
||||||
|
Go,
|
||||||
|
Json,
|
||||||
|
Bash,
|
||||||
|
Sql,
|
||||||
|
Yaml,
|
||||||
|
Count
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Token { Keyword, Type, String, Number, Comment, Preprocessor };
|
||||||
|
|
||||||
|
struct Span {
|
||||||
|
int start = 0;
|
||||||
|
int length = 0;
|
||||||
|
Token token = Token::Keyword;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 从 ```cpp 这类 info string 里识别语言(大小写不敏感)。
|
||||||
|
Language fromInfoString(const QString &info);
|
||||||
|
|
||||||
|
/// 扫描一行,返回需要着色的区间;inBlockComment 传入 / 传出跨行注释状态。
|
||||||
|
QVector<Span> tokenize(const QString &text, Language language, bool inBlockComment,
|
||||||
|
bool *newBlockComment);
|
||||||
|
|
||||||
|
} // namespace CodeHighlight
|
||||||
+95
-25
@@ -8,7 +8,22 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int StateNormal = 0;
|
constexpr int StateNormal = 0;
|
||||||
constexpr int StateInFence = 1;
|
|
||||||
|
// 围栏状态:1 + 语言编号 * 2 + 是否在块注释里。
|
||||||
|
int fenceState(int language, bool inComment)
|
||||||
|
{
|
||||||
|
return 1 + language * 2 + (inComment ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int stateLanguage(int state)
|
||||||
|
{
|
||||||
|
return (state - 1) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool stateInComment(int state)
|
||||||
|
{
|
||||||
|
return (state - 1) % 2 != 0;
|
||||||
|
}
|
||||||
|
|
||||||
QStringList monospaceFamilies()
|
QStringList monospaceFamilies()
|
||||||
{
|
{
|
||||||
@@ -103,6 +118,22 @@ void MarkdownHighlighter::rebuildFormats()
|
|||||||
m_fenceFormat.setFontFixedPitch(true);
|
m_fenceFormat.setFontFixedPitch(true);
|
||||||
m_fenceFormat.setForeground(c.editorText);
|
m_fenceFormat.setForeground(c.editorText);
|
||||||
|
|
||||||
|
m_fenceMarkerFormat = QTextCharFormat();
|
||||||
|
m_fenceMarkerFormat.setFontFamilies(monospaceFamilies());
|
||||||
|
m_fenceMarkerFormat.setFontFixedPitch(true);
|
||||||
|
m_fenceMarkerFormat.setForeground(c.placeholder);
|
||||||
|
|
||||||
|
auto tokenColor = [](const QColor &color) {
|
||||||
|
QTextCharFormat format;
|
||||||
|
format.setForeground(color);
|
||||||
|
return format;
|
||||||
|
};
|
||||||
|
m_codeKeywordFormat = tokenColor(c.codeKeyword);
|
||||||
|
m_codeStringFormat = tokenColor(c.codeString);
|
||||||
|
m_codeNumberFormat = tokenColor(c.codeNumber);
|
||||||
|
m_codeCommentFormat = tokenColor(c.codeComment);
|
||||||
|
m_codePreprocessorFormat = tokenColor(c.codePreprocessor);
|
||||||
|
|
||||||
m_linkFormat = QTextCharFormat();
|
m_linkFormat = QTextCharFormat();
|
||||||
m_linkFormat.setForeground(c.link);
|
m_linkFormat.setForeground(c.link);
|
||||||
m_linkFormat.setFontUnderline(true);
|
m_linkFormat.setFontUnderline(true);
|
||||||
@@ -141,6 +172,24 @@ QTextCharFormat MarkdownHighlighter::compose(const QTextCharFormat &specific) co
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QTextCharFormat &MarkdownHighlighter::tokenFormat(CodeHighlight::Token token) const
|
||||||
|
{
|
||||||
|
switch (token) {
|
||||||
|
case CodeHighlight::Token::Keyword:
|
||||||
|
case CodeHighlight::Token::Type:
|
||||||
|
return m_codeKeywordFormat;
|
||||||
|
case CodeHighlight::Token::String:
|
||||||
|
return m_codeStringFormat;
|
||||||
|
case CodeHighlight::Token::Number:
|
||||||
|
return m_codeNumberFormat;
|
||||||
|
case CodeHighlight::Token::Comment:
|
||||||
|
return m_codeCommentFormat;
|
||||||
|
case CodeHighlight::Token::Preprocessor:
|
||||||
|
return m_codePreprocessorFormat;
|
||||||
|
}
|
||||||
|
return m_codeKeywordFormat;
|
||||||
|
}
|
||||||
|
|
||||||
QTextCharFormat MarkdownHighlighter::hiddenFormat(const QColor &background) const
|
QTextCharFormat MarkdownHighlighter::hiddenFormat(const QColor &background) const
|
||||||
{
|
{
|
||||||
// 用“和背景同色 + 极小字号”把标记藏起来:文字仍然留在文档里,
|
// 用“和背景同色 + 极小字号”把标记藏起来:文字仍然留在文档里,
|
||||||
@@ -187,46 +236,67 @@ void MarkdownHighlighter::applyDelimited(const QRegularExpression &pattern, cons
|
|||||||
|
|
||||||
void MarkdownHighlighter::highlightBlock(const QString &text)
|
void MarkdownHighlighter::highlightBlock(const QString &text)
|
||||||
{
|
{
|
||||||
|
const int previousState = previousBlockState();
|
||||||
setCurrentBlockState(StateNormal);
|
setCurrentBlockState(StateNormal);
|
||||||
m_blockBase = QTextCharFormat();
|
m_blockBase = QTextCharFormat();
|
||||||
if (text.isEmpty())
|
|
||||||
|
const bool wasInFence = previousState > 0;
|
||||||
|
const int fenceLanguageId = wasInFence ? stateLanguage(previousState) : 0;
|
||||||
|
const auto fenceLanguage = static_cast<CodeHighlight::Language>(fenceLanguageId);
|
||||||
|
const bool fenceComment = wasInFence && stateInComment(previousState);
|
||||||
|
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
// 空行也要把围栏状态传下去,否则代码块会被一条空行截断。
|
||||||
|
if (wasInFence)
|
||||||
|
setCurrentBlockState(fenceState(fenceLanguageId, fenceComment));
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ThemeColors &c = ThemeManager::instance()->colors();
|
const ThemeColors &c = ThemeManager::instance()->colors();
|
||||||
const bool rendered = m_rendered;
|
const bool rendered = m_rendered;
|
||||||
const bool active = isActiveLine();
|
const bool active = isActiveLine();
|
||||||
|
|
||||||
static const QRegularExpression fencePattern(QStringLiteral("^\\s{0,3}(?:```|~~~)"));
|
static const QRegularExpression fencePattern(QStringLiteral("^\\s{0,3}(?:```|~~~)"));
|
||||||
const bool inFence = previousBlockState() == StateInFence;
|
|
||||||
const bool fenceLine = fencePattern.match(text).hasMatch();
|
const bool fenceLine = fencePattern.match(text).hasMatch();
|
||||||
|
|
||||||
// ---- 围栏代码块 -----------------------------------------------------
|
// ---- 围栏代码块 -----------------------------------------------------
|
||||||
if (inFence) {
|
if (wasInFence) {
|
||||||
if (!fenceLine)
|
|
||||||
setCurrentBlockState(StateInFence);
|
|
||||||
if (rendered) {
|
|
||||||
if (fenceLine && !active) {
|
|
||||||
apply(0, text.length(), hiddenFormat(c.editorBase));
|
|
||||||
} else {
|
|
||||||
m_blockBase = m_fenceFormat;
|
|
||||||
apply(0, text.length(), m_fenceFormat);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m_blockBase = m_codeFormat;
|
|
||||||
apply(0, text.length(), m_codeFormat);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (fenceLine) {
|
if (fenceLine) {
|
||||||
setCurrentBlockState(StateInFence);
|
// 闭合围栏行
|
||||||
if (rendered) {
|
if (rendered && !active)
|
||||||
if (!active)
|
|
||||||
apply(0, text.length(), hiddenFormat(c.editorBase));
|
apply(0, text.length(), hiddenFormat(c.editorBase));
|
||||||
else
|
else
|
||||||
apply(0, text.length(), m_fenceFormat);
|
apply(0, text.length(), m_fenceMarkerFormat);
|
||||||
} else {
|
return;
|
||||||
apply(0, text.length(), m_codeFormat);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool inComment = fenceComment;
|
||||||
|
bool endsInComment = inComment;
|
||||||
|
m_blockBase = m_fenceFormat;
|
||||||
|
apply(0, text.length(), m_fenceFormat);
|
||||||
|
|
||||||
|
if (fenceLanguage != CodeHighlight::Language::None) {
|
||||||
|
const QVector<CodeHighlight::Span> spans =
|
||||||
|
CodeHighlight::tokenize(text, fenceLanguage, inComment, &endsInComment);
|
||||||
|
for (const CodeHighlight::Span &span : spans)
|
||||||
|
apply(span.start, span.length, compose(tokenFormat(span.token)));
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentBlockState(fenceState(fenceLanguageId, endsInComment));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fenceLine) {
|
||||||
|
// 开围栏行:从 info string 里认语言
|
||||||
|
QString info = text.trimmed();
|
||||||
|
info.remove(QRegularExpression(QStringLiteral("^(?:```|~~~)")));
|
||||||
|
const CodeHighlight::Language language = CodeHighlight::fromInfoString(info);
|
||||||
|
setCurrentBlockState(fenceState(int(language), false));
|
||||||
|
|
||||||
|
if (rendered && !active)
|
||||||
|
apply(0, text.length(), hiddenFormat(c.editorBase));
|
||||||
|
else
|
||||||
|
apply(0, text.length(), m_fenceMarkerFormat);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "CodeHighlight.h"
|
||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QSyntaxHighlighter>
|
#include <QSyntaxHighlighter>
|
||||||
@@ -42,6 +44,7 @@ private:
|
|||||||
const QTextCharFormat &contentFormat, int contentGroup,
|
const QTextCharFormat &contentFormat, int contentGroup,
|
||||||
const QColor &hideAgainst);
|
const QColor &hideAgainst);
|
||||||
QTextCharFormat compose(const QTextCharFormat &specific) const;
|
QTextCharFormat compose(const QTextCharFormat &specific) const;
|
||||||
|
const QTextCharFormat &tokenFormat(CodeHighlight::Token token) const;
|
||||||
QTextCharFormat hiddenFormat(const QColor &background) const;
|
QTextCharFormat hiddenFormat(const QColor &background) const;
|
||||||
bool isActiveLine() const;
|
bool isActiveLine() const;
|
||||||
|
|
||||||
@@ -56,6 +59,12 @@ private:
|
|||||||
QTextCharFormat m_emphasisFormat;
|
QTextCharFormat m_emphasisFormat;
|
||||||
QTextCharFormat m_codeFormat;
|
QTextCharFormat m_codeFormat;
|
||||||
QTextCharFormat m_fenceFormat;
|
QTextCharFormat m_fenceFormat;
|
||||||
|
QTextCharFormat m_fenceMarkerFormat;
|
||||||
|
QTextCharFormat m_codeKeywordFormat;
|
||||||
|
QTextCharFormat m_codeStringFormat;
|
||||||
|
QTextCharFormat m_codeNumberFormat;
|
||||||
|
QTextCharFormat m_codeCommentFormat;
|
||||||
|
QTextCharFormat m_codePreprocessorFormat;
|
||||||
QTextCharFormat m_linkFormat;
|
QTextCharFormat m_linkFormat;
|
||||||
QTextCharFormat m_urlFormat;
|
QTextCharFormat m_urlFormat;
|
||||||
QTextCharFormat m_quoteFormat;
|
QTextCharFormat m_quoteFormat;
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ ThemeColors lightColors()
|
|||||||
c.emphasis = hex("#4b535c");
|
c.emphasis = hex("#4b535c");
|
||||||
c.code = hex("#b4295a");
|
c.code = hex("#b4295a");
|
||||||
c.codeBackground = hex("#f2f4f7");
|
c.codeBackground = hex("#f2f4f7");
|
||||||
|
c.codeKeyword = hex("#0550ae");
|
||||||
|
c.codeString = hex("#0a7d3a");
|
||||||
|
c.codeNumber = hex("#953800");
|
||||||
|
c.codeComment = hex("#6e7781");
|
||||||
|
c.codePreprocessor = hex("#8250df");
|
||||||
c.link = hex("#0969da");
|
c.link = hex("#0969da");
|
||||||
c.linkUrl = hex("#6b7480");
|
c.linkUrl = hex("#6b7480");
|
||||||
c.quote = hex("#6b7480");
|
c.quote = hex("#6b7480");
|
||||||
@@ -138,6 +143,11 @@ ThemeColors darkColors()
|
|||||||
c.emphasis = hex("#c3c9d1");
|
c.emphasis = hex("#c3c9d1");
|
||||||
c.code = hex("#ff8fbf");
|
c.code = hex("#ff8fbf");
|
||||||
c.codeBackground = hex("#292c33");
|
c.codeBackground = hex("#292c33");
|
||||||
|
c.codeKeyword = hex("#79b8ff");
|
||||||
|
c.codeString = hex("#7ee787");
|
||||||
|
c.codeNumber = hex("#ffab70");
|
||||||
|
c.codeComment = hex("#8b949e");
|
||||||
|
c.codePreprocessor = hex("#d2a8ff");
|
||||||
c.link = hex("#62a8ff");
|
c.link = hex("#62a8ff");
|
||||||
c.linkUrl = hex("#7f8791");
|
c.linkUrl = hex("#7f8791");
|
||||||
c.quote = hex("#8b939d");
|
c.quote = hex("#8b939d");
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ struct ThemeColors
|
|||||||
QColor emphasis;
|
QColor emphasis;
|
||||||
QColor code;
|
QColor code;
|
||||||
QColor codeBackground;
|
QColor codeBackground;
|
||||||
|
QColor codeKeyword;
|
||||||
|
QColor codeString;
|
||||||
|
QColor codeNumber;
|
||||||
|
QColor codeComment;
|
||||||
|
QColor codePreprocessor;
|
||||||
QColor link;
|
QColor link;
|
||||||
QColor linkUrl;
|
QColor linkUrl;
|
||||||
QColor quote;
|
QColor quote;
|
||||||
|
|||||||
Reference in New Issue
Block a user