From 29cc585b064bfddb3b6f89a2858e6f4d22e84145 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Tue, 18 Oct 2022 19:26:32 +0200 Subject: [PATCH] PoC: syntax highlighting Signed-off-by: Carl Schwan --- CMakeLists.txt | 2 +- src/CMakeLists.txt | 3 +- src/main.cpp | 9 ++ src/messageformatter.cpp | 133 +++++++++++++++++++++++ src/messageformatter.h | 13 +++ src/qml/Component/Timeline/RichLabel.qml | 6 + 6 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/messageformatter.cpp create mode 100644 src/messageformatter.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d7399555..491c71cad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES TYPE REQUIRED PURPOSE "Basic application components" ) -find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels) +find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels SyntaxHighlighting) set_package_properties(KF${QT_MAJOR_VERSION} PROPERTIES TYPE REQUIRED PURPOSE "Basic application components" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1c1742d98..63b7d316d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -136,6 +136,7 @@ add_library(neochat STATIC mediasizehelper.h eventhandler.cpp enums/delegatetype.h + messageformatter.cpp ) ecm_qt_declare_logging_category(neochat @@ -199,7 +200,7 @@ else() endif() target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR}) -target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark QCoro::Core) +target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 Qt::Xml KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels KF${QT_MAJOR_VERSION}::SyntaxHighlighting Quotient${QUOTIENT_SUFFIX} cmark::cmark QCoro::Core) kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc) diff --git a/src/main.cpp b/src/main.cpp index 0005d910e..e0b2eef04 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -107,6 +107,8 @@ #include #endif +#include "messageformatter.h" + using namespace Quotient; class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory @@ -228,6 +230,12 @@ int main(int argc, char *argv[]) Login *login = new Login(); UrlHelper urlHelper; + MessageFormatter formatter; + // formatter.formatInternal("

hrrejoire

\n
var i = 0; i++; function\n
\n

rekore

\n", new + // QTextDocument); + + // return 0; + #ifdef HAVE_COLORSCHEME ColorSchemer colorScheme; qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "ColorSchemer", &colorScheme); @@ -236,6 +244,7 @@ int main(int argc, char *argv[]) } #endif + qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "MessageFormatter", &formatter); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance()); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "NotificationsManager", &NotificationsManager::instance()); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard); diff --git a/src/messageformatter.cpp b/src/messageformatter.cpp new file mode 100644 index 000000000..06f3ab7f0 --- /dev/null +++ b/src/messageformatter.cpp @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2021 Carson Black +// SPDX-FileCopyrightText: 2022 Carl Schwan +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "messageformatter.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +QTextDocumentFragment copyTextLayoutFrom(QTextDocument *document) +{ + QTextCursor sourceCursor(document); + sourceCursor.select(QTextCursor::Document); + + QTextDocument helper; + + // copy the content fragment from the source document into our helper document + QTextCursor curs(&helper); + curs.insertFragment(sourceCursor.selection()); + curs.select(QTextCursor::Document); + + // not sure why, but fonts get lost. since this is for codeblocks, we can + // just force the mono font. anyone copying this code would probably want + // to fix the problem proper if it's not also for codeblocks. + const auto fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); + + const int docStart = sourceCursor.selectionStart(); + const int docEnd = helper.characterCount() - 1; + + // since the copied fragment above lost the qsyntaxhighlighter stuff, + // we gotta go through the qtextlayout and apply those styles to the + // document + const auto end = document->findBlock(sourceCursor.selectionEnd()).next(); + for (auto current = document->findBlock(docStart); current.isValid() && current != end; current = current.next()) { + const auto layout = current.layout(); + + // iterate through the formats, applying them to our document + for (const auto &span : layout->formats()) { + const int start = current.position() + span.start - docStart; + const int end = start + span.length; + + curs.setPosition(qMax(start, 0)); + curs.setPosition(qMin(end, docEnd), QTextCursor::KeepAnchor); + + auto fmt = span.format; + fmt.setFont(fixedFont); + + curs.setCharFormat(fmt); + } + } + + return QTextDocumentFragment(&helper); +} + +QTextDocumentFragment highlight(const QString &code, const QString &language) +{ + using namespace KSyntaxHighlighting; + + static Repository repo; + + auto theme = repo.themeForPalette(QGuiApplication::palette()); + auto definition = repo.definitionForFileName(QLatin1String("file.") + language); + + QTextDocument doku(code); + + QScopedPointer highlighter(new SyntaxHighlighter(&doku)); + highlighter->setTheme(theme); + highlighter->setDefinition(definition); + + return copyTextLayoutFrom(&doku); +} + +bool extractCodeBlock(QTextCursor cursor, QDomElement element) +{ + const auto codeNode = element.firstChild(); + if (!codeNode.isNull()) { + const auto code = codeNode.toElement(); + if (!code.isNull() && code.tagName() == QLatin1String("code")) { + QString lang; + auto langClass = code.attribute(QLatin1String("class"), QLatin1String("none")); + if (langClass != QLatin1String("none") && langClass.startsWith(QLatin1String("language-"))) { + lang = langClass.remove(0, 9); + } + + if (!lang.isNull()) { + cursor.insertFragment(highlight(code.text(), lang)); + return true; + } + } + } + return false; +} + +QString MessageFormatter::formatInternal(const QString &messageBody, QTextDocument *document) +{ + QTextCursor curs(document); + QDomDocument doc(QLatin1String("htmlement")); + doc.setContent(QStringLiteral("
%1
").arg(messageBody)); + QDomElement docElem = doc.documentElement(); + QDomNode n = docElem.firstChild(); + while (!n.isNull()) { + QDomElement e = n.toElement(); + if (!e.isNull()) { + if (e.tagName() != QLatin1String("pre") || !extractCodeBlock(curs, e)) { + QString outText; + QTextStream out(&outText); + e.save(out, 0); + curs.insertHtml(outText); + } + } + n = n.nextSibling(); + } + + Q_EMIT document->contentsChanged(); + return document->toHtml(); +} + +QString MessageFormatter::format(const QString &messageBody, QQuickTextDocument *doc, QQuickItem *item) +{ + QColor linkColor = QQmlProperty(item, QLatin1String("Kirigami.Theme.linkColor"), qmlContext(item)).read().value(); + + return formatInternal(messageBody, doc->textDocument()); +} diff --git a/src/messageformatter.h b/src/messageformatter.h new file mode 100644 index 000000000..c70fa649a --- /dev/null +++ b/src/messageformatter.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2022 Carl Schwan +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +class MessageFormatter : public QObject +{ + Q_OBJECT +public: + Q_INVOKABLE QString format(const QString &messageBody, QQuickTextDocument *doc, QQuickItem *item); + Q_INVOKABLE QString formatInternal(const QString &messageBody, QTextDocument *doc); +}; \ No newline at end of file diff --git a/src/qml/Component/Timeline/RichLabel.qml b/src/qml/Component/Timeline/RichLabel.qml index a9de8c5d4..0b03d2bbc 100644 --- a/src/qml/Component/Timeline/RichLabel.qml +++ b/src/qml/Component/Timeline/RichLabel.qml @@ -44,6 +44,7 @@ TextEdit { property bool spoilerRevealed: !hasSpoiler.test(textMessage) ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage)) + onTextMessageChanged: text = MessageFormatter.format(textMessage, contentLabel.textDocument, contentLabel) persistentSelection: true @@ -52,6 +53,7 @@ TextEdit { Controller.forceRefreshTextDocument(root.textDocument, root) } + /* text: "" + textMessage +======= +" + (isEmote ? "* " + author.displayName + " " : "") + textMessage + (isEdited ? (" " + "" + i18n(" (edited)") + "") : "") + */ color: Kirigami.Theme.textColor selectedTextColor: Kirigami.Theme.highlightedTextColor