Compare commits

...

1 Commits

Author SHA1 Message Date
Carl Schwan
29cc585b06 PoC: syntax highlighting
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2023-09-12 17:52:18 +02:00
6 changed files with 164 additions and 2 deletions

View File

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

View File

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

View File

@@ -107,6 +107,8 @@
#include <Windows.h>
#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("<p>hrrejoire</p>\n<pre><code class=\"language-js\">var i = 0; i++; function\n</code></pre>\n<p>rekore</p>\n", new
// QTextDocument);
// return 0;
#ifdef HAVE_COLORSCHEME
ColorSchemer colorScheme;
qmlRegisterSingletonInstance<ColorSchemer>("org.kde.neochat", 1, 0, "ColorSchemer", &colorScheme);
@@ -236,6 +244,7 @@ int main(int argc, char *argv[])
}
#endif
qmlRegisterSingletonInstance<MessageFormatter>("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);

133
src/messageformatter.cpp Normal file
View File

@@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "messageformatter.h"
#include <QDomDocument>
#include <QGuiApplication>
#include <QPalette>
#include <QQmlContext>
#include <QQmlProperty>
#include <QTextCursor>
#include <QTextDocumentFragment>
#include <KSyntaxHighlighting/definition.h>
#include <KSyntaxHighlighting/repository.h>
#include <KSyntaxHighlighting/syntaxhighlighter.h>
#include <KSyntaxHighlighting/theme.h>
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<SyntaxHighlighter> 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("<div>%1</div>").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<QColor>();
return formatInternal(messageBody, doc->textDocument());
}

13
src/messageformatter.h Normal file
View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QQuickItem>
#include <QQuickTextDocument>
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);
};

View File

@@ -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: "<style>
table {
width:100%;
@@ -84,7 +86,11 @@ a{
background: " + Kirigami.Theme.textColor + ";
}
" : "") + "
<<<<<<< HEAD:src/qml/Component/Timeline/RichLabel.qml
</style>" + textMessage
=======
</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + textMessage + (isEdited ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + "<span style='font-size: " + Kirigami.Theme.defaultFont.pixelSize +"px'>" + i18n(" (edited)") + "</span>") : "")
*/
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor