فهرست منبع

MarkdownViewWindow: use web to highlight code block in editor

Le Tan 3 سال پیش
والد
کامیت
eff1a81125

+ 1 - 1
libs/vtextedit

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

+ 1 - 1
src/core/configmgr.cpp

@@ -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";

+ 2 - 0
src/core/mainconfig.cpp

@@ -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;
 }

+ 1 - 1
src/core/markdowneditorconfig.h

@@ -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;
     };
 }
 

+ 5 - 1
src/core/texteditorconfig.h

@@ -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;
 

+ 1 - 0
src/core/theme.h

@@ -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;
         };
 

+ 2 - 2
src/data/core/vnotex.json

@@ -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" : [

+ 8 - 0
src/data/extra/web/js/markdownviewer.js

@@ -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);
         });

+ 43 - 0
src/data/extra/web/js/vnotex.js

@@ -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);
     }

+ 2 - 18
src/fakeaccessible.cpp

@@ -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);
     }
 

+ 29 - 0
src/utils/callbackpool.cpp

@@ -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 - 0
src/utils/callbackpool.h

@@ -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

+ 12 - 0
src/utils/utils.cpp

@@ -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);
+}

+ 2 - 0
src/utils/utils.h

@@ -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
 

+ 2 - 0
src/utils/utils.pri

@@ -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 \

+ 1 - 1
src/widgets/editors/markdowneditor.cpp

@@ -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);
 

+ 75 - 1
src/widgets/editors/markdownvieweradapter.cpp

@@ -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);
+        });
+    }
+}

+ 37 - 0
src/widgets/editors/markdownvieweradapter.h

@@ -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;
     };
 }
 

+ 5 - 1
src/widgets/mainwindow.cpp

@@ -383,7 +383,11 @@ 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)) {
-            showNormal();
+            if (m_windowOldState & Qt::WindowMaximized) {
+                showMaximized();
+            } else {
+                showNormal();
+            }
         }
         saveStateAndGeometry();
     }

+ 55 - 0
src/widgets/markdownviewwindow.cpp

@@ -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);
+}

+ 2 - 0
src/widgets/markdownviewwindow.h

@@ -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);