MarkdownViewWindow: use web to highlight code block in editor

This commit is contained in:
Le Tan 2022-01-07 20:41:31 +08:00
parent 727fade948
commit eff1a81125
21 changed files with 315 additions and 27 deletions

@ -1 +1 @@
Subproject commit 064a434202096f703cbed9162742bbc55911f974
Subproject commit 43ed95437369d48ff0428661db9ebae711d725e9

View File

@ -25,7 +25,7 @@
using namespace vnotex;
#ifndef QT_NO_DEBUG
// #define VX_DEBUG_WEB
#define VX_DEBUG_WEB
#endif
const QString ConfigMgr::c_orgName = "VNote";

View File

@ -7,6 +7,7 @@
#include "coreconfig.h"
#include "editorconfig.h"
#include "widgetconfig.h"
#include "texteditorconfig.h"
#include "markdowneditorconfig.h"
using namespace vnotex;
@ -118,4 +119,5 @@ QString MainConfig::getVersion(const QJsonObject &p_jobj)
void MainConfig::doVersionSpecificOverride()
{
// In a new version, we may want to change one value by force.
m_editorConfig->getTextEditorConfig().m_highlightWhitespace = false;
}

View File

@ -241,7 +241,7 @@ namespace vnotex
InplacePreviewSources m_inplacePreviewSources = InplacePreviewSource::NoInplacePreview;
// View mode in edit mode.
EditViewMode m_editViewMode = EditViewMode::EditPreview;
EditViewMode m_editViewMode = EditViewMode::EditOnly;
};
}

View File

@ -5,6 +5,8 @@
namespace vnotex
{
class MainConfig;
class TextEditorConfig : public IConfig
{
public:
@ -73,6 +75,8 @@ namespace vnotex
void setSpellCheckEnabled(bool p_enabled);
private:
friend class MainConfig;
QString lineNumberTypeToString(LineNumberType p_type) const;
LineNumberType stringToLineNumberType(const QString &p_str) const;
@ -99,7 +103,7 @@ namespace vnotex
int m_tabStopWidth = 4;
bool m_highlightWhitespace = true;
bool m_highlightWhitespace = false;
int m_zoomDelta = 0;

View File

@ -67,6 +67,7 @@ namespace vnotex
// Use for MarkdownEditor code block highlight.
// If not specified, will use m_editorHighlightTheme.
// Valid only when KSyntaxCodeBlockHighlighter is used.
QString m_markdownEditorHighlightTheme;
};

View File

@ -141,7 +141,7 @@
"wrap_mode": "word_anywhere",
"expand_tab": true,
"tab_stop_width": 4,
"highlight_whitespace": true,
"highlight_whitespace": false,
"//comment" : "Positive to zoom in and negative to zoom out",
"zoom_delta": 0,
"spell_check": false
@ -360,7 +360,7 @@
"//comment" : "imagelink/codeblock/math",
"inplace_preview_sources" : "imagelink;codeblock;math",
"//comment" : "view mode of edit mode: editonly/editpreview",
"edit_view_mode" : "editpreview"
"edit_view_mode" : "editonly"
},
"image_host" : {
"hosts" : [

View File

@ -35,6 +35,14 @@ new QWebChannel(qt.webChannelTransport,
window.vnotex.htmlToMarkdown(p_id, p_timeStamp, p_html);
});
adapter.highlightCodeBlockRequested.connect(function(p_idx, p_timeStamp, p_text) {
window.vnotex.highlightCodeBlock(p_idx, p_timeStamp, p_text);
});
adapter.parseStyleSheetRequested.connect(function(p_id, p_styleSheet) {
window.vnotex.parseStyleSheet(p_id, p_styleSheet);
});
adapter.crossCopyRequested.connect(function(p_id, p_timeStamp, p_target, p_baseUrl, p_html) {
window.vnotex.crossCopy(p_id, p_timeStamp, p_target, p_baseUrl, p_html);
});

View File

@ -270,6 +270,49 @@ class VNoteX extends EventEmitter {
window.vxMarkdownAdapter.setMarkdownFromHtml(p_id, p_timeStamp, markdown);
}
highlightCodeBlock(p_idx, p_timeStamp, p_text) {
let match = /^```[^\S\n]*(\S+)?\s*\n([\s\S]+)\n```\s*$/.exec(p_text);
if (!match || !match[1] || !match[2]) {
window.vxMarkdownAdapter.setCodeBlockHighlightHtml(p_idx, p_timeStamp, '');
return;
}
let lang = match[1];
let body = match[2];
if (Prism && Prism.languages[lang]) {
let html = Prism.highlight(body, Prism.languages[lang], lang);
window.vxMarkdownAdapter.setCodeBlockHighlightHtml(p_idx, p_timeStamp, html);
} else {
window.vxMarkdownAdapter.setCodeBlockHighlightHtml(p_idx, p_timeStamp, '');
}
}
parseStyleSheet(p_id, p_styleSheet) {
let doc = document.implementation.createHTMLDocument('');
let styleEle = document.createElement('style');
styleEle.textContent = p_styleSheet;
doc.body.appendChild(styleEle);
let styles = [];
for (let i = 0; i < styleEle.sheet.cssRules.length; ++i) {
let rule = styleEle.sheet.cssRules[i];
if (rule.type != CSSRule.STYLE_RULE) {
continue;
}
styles.push({
selector: rule.selectorText,
color: rule.style.color,
backgroundColor: rule.style.backgroundColor,
fontWeight: rule.style.fontWeight,
fontStyle: rule.style.fontStyle
});
}
window.vxMarkdownAdapter.setStyleSheetStyles(p_id, styles);
}
setCrossCopyTargets(p_targets) {
window.vxMarkdownAdapter.setCrossCopyTargets(p_targets);
}

View File

@ -8,24 +8,8 @@ using namespace vnotex;
QAccessibleInterface *FakeAccessible::accessibleFactory(const QString &p_className, QObject *p_obj)
{
// Try to fix non-responsible issue caused by Youdao Dict.
if (p_className == QLatin1String("vnotex::LineEdit")
|| p_className == QLatin1String("vnotex::TitleBar")
|| p_className == QLatin1String("vnotex::NotebookSelector")
|| p_className == QLatin1String("vnotex::TagExplorer")
|| p_className == QLatin1String("vnotex::SearchPanel")
|| p_className == QLatin1String("vnotex::SnippetPanel")
|| p_className == QLatin1String("vnotex::OutlineViewer")
|| p_className == QLatin1String("vnotex::TitleToolBar")
|| p_className == QLatin1String("vnotex::MainWindow")
|| p_className == QLatin1String("vnotex::ViewArea")
|| p_className == QLatin1String("vte::VTextEdit")
|| p_className == QLatin1String("vte::IndicatorsBorder")
|| p_className == QLatin1String("vte::MarkdownEditor")
|| p_className == QLatin1String("vte::VMarkdownEditor")
|| p_className == QLatin1String("vte::VTextEditor")
|| p_className == QLatin1String("vte::ViStatusBar")
|| p_className == QLatin1String("vte::StatusIndicator")
|| p_className == QLatin1String("vte::ScrollBar")) {
if (p_className.startsWith(QStringLiteral("vnotex::"))
|| p_className.startsWith(QStringLiteral("vte::"))) {
return new FakeAccessibleInterface(p_obj);
}

View File

@ -0,0 +1,29 @@
#include "callbackpool.h"
#include <QDebug>
using namespace vnotex;
quint64 CallbackPool::add(const Callback &p_callback)
{
static quint64 nextId = 0;
quint64 id = nextId++;
m_pool.insert(id, p_callback);
return id;
}
void CallbackPool::call(quint64 p_id, void *p_data)
{
auto it = m_pool.find(p_id);
if (it != m_pool.end()) {
it.value()(p_data);
m_pool.erase(it);
} else {
qWarning() << "failed to locate callback in pool with id" << p_id;
}
}
void CallbackPool::clear()
{
m_pool.clear();
}

29
src/utils/callbackpool.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef CALLBACKPOOL_H
#define CALLBACKPOOL_H
#include <functional>
#include <QMap>
namespace vnotex
{
// Manage callbacks with id.
class CallbackPool
{
public:
typedef std::function<void(void *)> Callback;
CallbackPool() = default;
quint64 add(const Callback &p_callback);
void call(quint64 p_id, void *p_data);
void clear();
private:
QMap<quint64, Callback> m_pool;
};
}
#endif // CALLBACKPOOL_H

View File

@ -192,3 +192,15 @@ QJsonValue Utils::parseAndReadJson(const QJsonObject &p_obj, const QString &p_ex
return val;
}
QColor Utils::toColor(const QString &p_color)
{
// rgb(123, 123, 123).
QRegularExpression rgbTripleRegExp(R"(^rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)$)", QRegularExpression::CaseInsensitiveOption);
auto match = rgbTripleRegExp.match(p_color);
if (match.hasMatch()) {
return QColor(match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt());
}
return QColor(p_color);
}

View File

@ -61,6 +61,8 @@ namespace vnotex
// Parse @p_exp into tokens and read the target value from @p_obj.
// Format: obj1.obj2.arr[2].obj3.
static QJsonValue parseAndReadJson(const QJsonObject &p_obj, const QString &p_exp);
static QColor toColor(const QString &p_color);
};
} // ns vnotex

View File

@ -1,6 +1,7 @@
QT += widgets svg
SOURCES += \
$$PWD/callbackpool.cpp \
$$PWD/contentmediautils.cpp \
$$PWD/docsutils.cpp \
$$PWD/htmlutils.cpp \
@ -17,6 +18,7 @@ SOURCES += \
$$PWD/clipboardutils.cpp
HEADERS += \
$$PWD/callbackpool.h \
$$PWD/contentmediautils.h \
$$PWD/docsutils.h \
$$PWD/htmlutils.h \

View File

@ -1057,7 +1057,7 @@ void MarkdownEditor::parseToMarkdownAndPaste()
void MarkdownEditor::handleHtmlToMarkdownData(quint64 p_id, TimeStamp p_timeStamp, const QString &p_text)
{
Q_UNUSED(p_id);
qDebug() << "htmlToMarkdownData" << p_timeStamp;
qDebug() << "htmlToMarkdownData" << p_timeStamp << p_text;
if (m_timeStamp == p_timeStamp && !p_text.isEmpty()) {
QString text(p_text);

View File

@ -1,6 +1,5 @@
#include "markdownvieweradapter.h"
#include <QDebug>
#include <QMap>
#include "../outlineprovider.h"
@ -61,6 +60,35 @@ QJsonObject MarkdownViewerAdapter::FindOption::toJson() const
return obj;
}
MarkdownViewerAdapter::CssRuleStyle MarkdownViewerAdapter::CssRuleStyle::fromJson(const QJsonObject &p_obj)
{
CssRuleStyle style;
style.m_selector = p_obj[QStringLiteral("selector")].toString();
style.m_color = p_obj[QStringLiteral("color")].toString();
style.m_backgroundColor = p_obj[QStringLiteral("backgroundColor")].toString();
style.m_fontWeight = p_obj[QStringLiteral("fontWeight")].toString();
style.m_fontStyle = p_obj[QStringLiteral("fontStyle")].toString();
return style;
}
QTextCharFormat MarkdownViewerAdapter::CssRuleStyle::toTextCharFormat() const
{
QTextCharFormat fmt;
if (!m_color.isEmpty()) {
fmt.setForeground(Utils::toColor(m_color));
}
if (!m_backgroundColor.isEmpty()) {
fmt.setBackground(QColor(m_color));
}
if (m_fontWeight.contains(QStringLiteral("bold"))) {
fmt.setFontWeight(QFont::Bold);
}
if (m_fontStyle.contains(QStringLiteral("italic"))) {
fmt.setFontItalic(true);
}
return fmt;
}
MarkdownViewerAdapter::MarkdownViewerAdapter(QObject *p_parent)
: QObject(p_parent)
{
@ -269,6 +297,11 @@ void MarkdownViewerAdapter::setMarkdownFromHtml(quint64 p_id, quint64 p_timeStam
emit htmlToMarkdownReady(p_id, p_timeStamp, p_text);
}
void MarkdownViewerAdapter::setCodeBlockHighlightHtml(int p_idx, quint64 p_timeStamp, const QString &p_html)
{
emit highlightCodeBlockReady(p_idx, p_timeStamp, p_html);
}
void MarkdownViewerAdapter::setCrossCopyTargets(const QJsonArray &p_targets)
{
m_crossCopyTargets.clear();
@ -401,3 +434,44 @@ void MarkdownViewerAdapter::renderGraph(quint64 p_id,
Q_ASSERT(false);
}
}
void MarkdownViewerAdapter::highlightCodeBlock(int p_idx, quint64 p_timeStamp, const QString &p_text)
{
if (m_viewerReady) {
emit highlightCodeBlockRequested(p_idx, p_timeStamp, p_text);
} else {
m_pendingActions.append([this, p_idx, p_timeStamp, p_text]() {
emit highlightCodeBlockRequested(p_idx, p_timeStamp, p_text);
});
}
}
void MarkdownViewerAdapter::setStyleSheetStyles(quint64 p_id, const QJsonArray &p_styles)
{
QVector<CssRuleStyle> ruleStyles;
ruleStyles.reserve(p_styles.size());
for (int i = 0; i < p_styles.size(); ++i) {
ruleStyles.push_back(CssRuleStyle::fromJson(p_styles[i].toObject()));
}
m_callbackPool.call(p_id, &ruleStyles);
}
void MarkdownViewerAdapter::fetchStylesFromStyleSheet(const QString &p_styleSheet,
const std::function<void(const QVector<CssRuleStyle> *)> &p_callback)
{
if (p_styleSheet.isEmpty()) {
return;
}
const quint64 id = m_callbackPool.add([p_callback](void *data) {
p_callback(reinterpret_cast<const QVector<CssRuleStyle> *>(data));
});
if (m_viewerReady) {
emit parseStyleSheetRequested(id, p_styleSheet);
} else {
m_pendingActions.append([this, p_styleSheet, id]() {
emit parseStyleSheetRequested(id, p_styleSheet);
});
}
}

View File

@ -6,8 +6,10 @@
#include <QJsonObject>
#include <QScopedPointer>
#include <QJsonArray>
#include <QTextCharFormat>
#include <core/global.h>
#include <utils/callbackpool.h>
namespace vnotex
{
@ -78,6 +80,23 @@ namespace vnotex
bool m_regularExpression = false;
};
struct CssRuleStyle
{
QTextCharFormat toTextCharFormat() const;
static CssRuleStyle fromJson(const QJsonObject &p_obj);
QString m_selector;
QString m_color;
QString m_backgroundColor;
QString m_fontWeight;
QString m_fontStyle;
};
explicit MarkdownViewerAdapter(QObject *p_parent = nullptr);
virtual ~MarkdownViewerAdapter();
@ -114,6 +133,12 @@ namespace vnotex
// Should be called before WebViewer.setHtml().
void reset();
void highlightCodeBlock(int p_idx, quint64 p_timeStamp, const QString &p_text);
// Parse style sheet and fetch the styles.
void fetchStylesFromStyleSheet(const QString &p_styleSheet,
const std::function<void(const QVector<CssRuleStyle> *)> &p_callback);
// Functions to be called from web side.
public slots:
void setReady(bool p_ready);
@ -152,6 +177,8 @@ namespace vnotex
// Set back the result of htmlToMarkdown() call.
void setMarkdownFromHtml(quint64 p_id, quint64 p_timeStamp, const QString &p_text);
void setCodeBlockHighlightHtml(int p_idx, quint64 p_timeStamp, const QString &p_html);
void setCrossCopyTargets(const QJsonArray &p_targets);
void setCrossCopyResult(quint64 p_id, quint64 p_timeStamp, const QString &p_html);
@ -167,6 +194,8 @@ namespace vnotex
const QString &p_lang,
const QString &p_text);
void setStyleSheetStyles(quint64 p_id, const QJsonArray &p_styles);
// Signals to be connected at web side.
signals:
// Current Markdown text is updated.
@ -208,6 +237,10 @@ namespace vnotex
const QString &p_format,
const QString &p_data);
void highlightCodeBlockRequested(int p_idx, quint64 p_timeStamp, const QString &p_text);
void parseStyleSheetRequested(quint64 p_id, const QString &p_styleSheet);
// Signals to be connected at cpp side.
signals:
void graphPreviewDataReady(const PreviewData &p_data);
@ -238,6 +271,8 @@ namespace vnotex
const QString &p_content,
const QString &p_bodyClassList);
void highlightCodeBlockReady(int p_idx, quint64 p_timeStamp, const QString &p_html);
private:
void scrollToLine(int p_lineNumber);
@ -261,6 +296,8 @@ namespace vnotex
// Targets supported by cross copy. Set by web.
QStringList m_crossCopyTargets;
CallbackPool m_callbackPool;
};
}

View File

@ -383,8 +383,12 @@ void MainWindow::closeEvent(QCloseEvent *p_event)
// Avoid geometry corruption caused by fullscreen or minimized window.
const auto state = windowState();
if (state & (Qt::WindowMinimized | Qt::WindowFullScreen)) {
if (m_windowOldState & Qt::WindowMaximized) {
showMaximized();
} else {
showNormal();
}
}
saveStateAndGeometry();
}

View File

@ -17,12 +17,14 @@
#include <core/fileopenparameters.h>
#include <core/editorconfig.h>
#include <core/htmltemplatehelper.h>
#include <core/exception.h>
#include <vtextedit/vtextedit.h>
#include <vtextedit/pegmarkdownhighlighter.h>
#include <vtextedit/markdowneditorconfig.h>
#include <utils/pathutils.h>
#include <utils/widgetutils.h>
#include <utils/printutils.h>
#include <utils/fileutils.h>
#include <buffer/markdownbuffer.h>
#include <core/vnotex.h>
#include <core/thememgr.h>
@ -359,6 +361,10 @@ void MarkdownViewWindow::setupTextEditor()
adapter(), &MarkdownViewerAdapter::htmlToMarkdownRequested);
connect(adapter(), &MarkdownViewerAdapter::htmlToMarkdownReady,
m_editor, &MarkdownEditor::handleHtmlToMarkdownData);
connect(m_editor, &vte::VMarkdownEditor::externalCodeBlockHighlightRequested,
this, &MarkdownViewWindow::handleExternalCodeBlockHighlightRequest);
connect(adapter(), &MarkdownViewerAdapter::highlightCodeBlockReady,
m_editor, &vte::VMarkdownEditor::handleExternalCodeBlockHighlightData);
// Connect outline pipeline.
connect(m_editor, &MarkdownEditor::headingsChanged,
@ -1383,3 +1389,52 @@ void MarkdownViewWindow::print()
});
}
}
void MarkdownViewWindow::handleExternalCodeBlockHighlightRequest(int p_idx, quint64 p_timeStamp, const QString &p_text)
{
static bool stylesInitialized = false;
if (!stylesInitialized) {
stylesInitialized = true;
const auto file = VNoteX::getInst().getThemeMgr().getFile(Theme::File::HighlightStyleSheet);
if (file.isEmpty()) {
qWarning() << "no highlight style sheet specified for external code block highlight";
} else {
QString content;
try {
content = FileUtils::readTextFile(file);
} catch (Exception &e) {
qWarning() << "failed to read highlight style sheet for external code block highlight" << file << e.what();
}
adapter()->fetchStylesFromStyleSheet(content, [this](const QVector<MarkdownViewerAdapter::CssRuleStyle> *rules) {
MarkdownEditor::ExternalCodeBlockHighlightStyles styles;
const QString prefix(".token.");
for (const auto &rule : *rules) {
bool isFirst = true;
QTextCharFormat fmt;
// Just fetch `.token.attr` styles.
auto selects = rule.m_selector.split(QLatin1Char(','));
for (const auto &sel : selects) {
const auto ts = sel.trimmed();
if (!ts.startsWith(prefix)) {
continue;
}
auto classList = ts.mid(prefix.size()).split(QLatin1Char('.'));
for (const auto &cla : classList) {
if (isFirst) {
fmt = rule.toTextCharFormat();
isFirst = false;
}
styles.insert(cla, fmt);
}
}
}
MarkdownEditor::setExternalCodeBlockHighlihgtStyles(styles);
});
}
}
adapter()->highlightCodeBlock(p_idx, p_timeStamp, p_text);
}

View File

@ -177,6 +177,8 @@ namespace vnotex
void syncEditorPositionToPreview();
void handleExternalCodeBlockHighlightRequest(int p_idx, quint64 p_timeStamp, const QString &p_text);
template <class T>
static QSharedPointer<Outline> headingsToOutline(const QVector<T> &p_headings);