// 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()); }