Блоки кода: отдельный компонент CodeBlock + MarkdownView (Telegram-пузырь)
- CodeBlock.qml: отдельный Rectangle #2e4d6a radius 12, header с языком и кнопкой 📋 Копировать (hover rgba 0.22/active 0.30, border, radius 11), код в rgba(0,0,0,0.22) radius 8, JetBrains Mono 13/tab 4, syntax highlight тёмной темой, selectByMouse
- MarkdownView.qml: ScrollView+Column+Repeater, parseBlocks() разбивает markdown на html/code блоки, html через Text RichText, code через CodeBlock, copyRequested → mdParser.copyToClipboard
- MdParser: parseBlocks() regex ```lang\n.*?\n``` + render для html частей, codeId/highlighted, highlightedCode(), highlight(...,true) forceDark, QTime удалён, QRegularExpression добавлен
- EditorPanel: viewArea/livePreview теперь MarkdownView (markdown property), liveDebounce/markdown, onLinkActivated → handleLink, viewMode переключает markdown
- CMake: добавлены CodeBlock.qml/MarkdownView.qml в qml модуль
- Исправлены qmllint: rgba → Qt.rgba, lineHeight убран, model.type → required property
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QImageReader>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
#include <QTime>
|
||||
#include <QUrl>
|
||||
@@ -736,4 +737,81 @@ QString MdParser::render(const QString &markdown, const QString &vaultRoot,
|
||||
cmark_node_free(doc);
|
||||
cmark_parser_free(parser);
|
||||
return html;
|
||||
}
|
||||
|
||||
QVariantList MdParser::parseBlocks(const QString &markdown, const QString &vaultRoot, const QString &fileDir)
|
||||
{
|
||||
QVariantList blocks;
|
||||
if (markdown.isEmpty()) return blocks;
|
||||
|
||||
// Сбрасываем счётчик для нового парса
|
||||
if (m) {
|
||||
m->codeBlocks.clear();
|
||||
m->codeCounter = 0;
|
||||
}
|
||||
|
||||
// Ищем fenced code blocks ```lang\ncode\n```
|
||||
static const QRegularExpression re(QStringLiteral("```([^`\\n]*?)\\n(.*?)\\n```"), QRegularExpression::DotMatchesEverythingOption);
|
||||
QRegularExpressionMatchIterator it = re.globalMatch(markdown);
|
||||
int lastPos = 0;
|
||||
while (it.hasNext()) {
|
||||
QRegularExpressionMatch match = it.next();
|
||||
int start = match.capturedStart();
|
||||
int end = match.capturedEnd();
|
||||
// Текст до блока
|
||||
if (start > lastPos) {
|
||||
QString htmlPart = markdown.mid(lastPos, start - lastPos);
|
||||
if (!htmlPart.trimmed().isEmpty()) {
|
||||
QString html = render(htmlPart, vaultRoot, fileDir);
|
||||
if (!html.trimmed().isEmpty())
|
||||
blocks.append(QVariantMap{{"type", "html"}, {"html", html}});
|
||||
}
|
||||
}
|
||||
QString langRaw = match.captured(1).trimmed();
|
||||
QString code = match.captured(2);
|
||||
QString lang = langRaw; // нормализация внутри highlight
|
||||
QString codeId;
|
||||
if (m) {
|
||||
codeId = QStringLiteral("code_%1").arg(m->codeCounter++);
|
||||
m->codeBlocks.insert(codeId, code);
|
||||
}
|
||||
CodeHighlighter hl(m ? m->m_repo : nullptr);
|
||||
QString highlighted;
|
||||
if (m && m->m_repo) {
|
||||
highlighted = hl.highlight(code, lang, true);
|
||||
} else {
|
||||
highlighted = code.toHtmlEscaped();
|
||||
}
|
||||
QVariantMap block;
|
||||
block["type"] = "code";
|
||||
block["code"] = code;
|
||||
block["language"] = lang.isEmpty() ? "text" : lang;
|
||||
block["codeId"] = codeId;
|
||||
block["highlighted"] = highlighted;
|
||||
blocks.append(block);
|
||||
lastPos = end;
|
||||
}
|
||||
// Остаток после последнего блока
|
||||
if (lastPos < markdown.length()) {
|
||||
QString htmlPart = markdown.mid(lastPos);
|
||||
if (!htmlPart.trimmed().isEmpty()) {
|
||||
QString html = render(htmlPart, vaultRoot, fileDir);
|
||||
if (!html.trimmed().isEmpty())
|
||||
blocks.append(QVariantMap{{"type", "html"}, {"html", html}});
|
||||
}
|
||||
}
|
||||
// Если вообще не нашли блоки, но markdown не пустой — вернём как html
|
||||
if (blocks.isEmpty() && !markdown.trimmed().isEmpty()) {
|
||||
QString html = render(markdown, vaultRoot, fileDir);
|
||||
blocks.append(QVariantMap{{"type", "html"}, {"html", html}});
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
QString MdParser::highlightedCode(const QString &code, const QString &language) const
|
||||
{
|
||||
QString lang = language.trimmed();
|
||||
if (!m || !m->m_repo) return code.toHtmlEscaped();
|
||||
CodeHighlighter hl(m->m_repo);
|
||||
return hl.highlight(code, lang, true);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
|
||||
// Рендер Markdown (CommonMark + GFM) в HTML для режима просмотра.
|
||||
// Задачи/ссылки/переходы кодируются ссылками schemes:
|
||||
@@ -21,8 +22,15 @@ public:
|
||||
const QString &vaultRoot,
|
||||
const QString &fileDir);
|
||||
|
||||
// Разбор на блоки для отдельного отображения кодовых блоков (CodeBlock.qml)
|
||||
// Возвращает список {type:"html"/"code", html/code/language/codeId/highlighted}
|
||||
Q_INVOKABLE QVariantList parseBlocks(const QString &markdown,
|
||||
const QString &vaultRoot,
|
||||
const QString &fileDir);
|
||||
|
||||
Q_INVOKABLE QString getCodeBlock(const QString &id) const;
|
||||
Q_INVOKABLE void copyToClipboard(const QString &text) const;
|
||||
Q_INVOKABLE QString highlightedCode(const QString &code, const QString &language) const;
|
||||
|
||||
struct Impl;
|
||||
private:
|
||||
|
||||
Reference in New Issue
Block a user