feat: add missing KF6 framework recipes

This commit is contained in:
2026-05-07 07:53:26 +01:00
parent d8d498f831
commit a69f479b52
2374 changed files with 2610246 additions and 0 deletions
@@ -0,0 +1,387 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#include "test-config.h"
#include <KSyntaxHighlighting/AbstractHighlighter>
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Format>
#include <KSyntaxHighlighting/Repository>
#include <KSyntaxHighlighting/State>
#include <KSyntaxHighlighting/Theme>
#include <htmlhighlighter.h>
#include <QDebug>
#include <QDirIterator>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QObject>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTest>
namespace KSyntaxHighlighting
{
class FormatCollector : public AbstractHighlighter
{
public:
using AbstractHighlighter::highlightLine;
void applyFormat(int offset, int length, const Format &format) override
{
Q_UNUSED(offset);
Q_UNUSED(length);
formatMap.insert(format.name(), format);
}
QHash<QString, Format> formatMap;
};
class ThemeTest : public QObject
{
Q_OBJECT
private:
Repository m_repo;
private Q_SLOTS:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
initRepositorySearchPaths(m_repo);
}
void testThemes()
{
QVERIFY(!m_repo.themes().isEmpty());
for (const auto &theme : m_repo.themes()) {
QVERIFY(theme.isValid());
QVERIFY(!theme.name().isEmpty());
QVERIFY(!theme.filePath().isEmpty());
QVERIFY(QFileInfo::exists(theme.filePath()));
QVERIFY(m_repo.theme(theme.name()).isValid());
}
}
void testFormat_data()
{
QTest::addColumn<QString>("themeName");
QTest::newRow("default") << "Breeze Light";
QTest::newRow("dark") << "Breeze Dark";
QTest::newRow("print") << "Printing";
}
void testFormat()
{
QFETCH(QString, themeName);
// somewhat complicated way to get proper Format objects
FormatCollector collector;
collector.setDefinition(m_repo.definitionForName(QLatin1String("QML")));
const auto t = m_repo.theme(themeName);
QVERIFY(t.isValid());
collector.setTheme(t);
collector.highlightLine(u"normal + property real foo: 3.14", State());
QVERIFY(collector.formatMap.size() >= 4);
// qDebug() << collector.formatMap.keys();
// normal text
auto f = collector.formatMap.value(QLatin1String("Normal Text"));
QVERIFY(f.isValid());
QVERIFY(f.textStyle() == Theme::Normal);
QVERIFY(f.isDefaultTextStyle(t));
QVERIFY(!f.hasTextColor(t));
QVERIFY(!f.hasBackgroundColor(t));
QVERIFY(f.id() > 0);
// visually identical to normal text with Printing theme
f = collector.formatMap.value(QLatin1String("Symbol"));
QVERIFY(f.isValid());
QCOMPARE(f.textStyle(), Theme::Operator);
if (themeName == QLatin1String("Printing")) {
QVERIFY(f.isDefaultTextStyle(t));
QVERIFY(!f.hasTextColor(t));
} else {
QVERIFY(!f.isDefaultTextStyle(t));
QVERIFY(f.hasTextColor(t));
}
QVERIFY(f.id() > 0);
// visually different to normal text
f = collector.formatMap.value(QLatin1String("Keywords"));
QVERIFY(f.isValid());
QCOMPARE(f.textStyle(), Theme::Keyword);
QVERIFY(!f.isDefaultTextStyle(t));
QVERIFY(f.isBold(t));
QVERIFY(f.id() > 0);
f = collector.formatMap.value(QLatin1String("Float"));
QVERIFY(f.isValid());
QCOMPARE(f.textStyle(), Theme::Float);
QVERIFY(!f.isDefaultTextStyle(t));
QVERIFY(f.hasTextColor(t));
QVERIFY(f.id() > 0);
}
void testBreezeLightTheme()
{
Theme t = m_repo.theme(QLatin1String("Breeze Light"));
QVERIFY(t.isValid());
// Themes compiled in as resource are never writable
QVERIFY(t.isReadOnly());
// make sure all editor colors are properly read
QCOMPARE(t.editorColor(Theme::BackgroundColor), QColor("#ffffff").rgba());
QCOMPARE(t.editorColor(Theme::TextSelection), QColor("#94caef").rgba());
QCOMPARE(t.editorColor(Theme::CurrentLine), QColor("#f8f7f6").rgba());
QCOMPARE(t.editorColor(Theme::SearchHighlight), QColor("#ffff00").rgba());
QCOMPARE(t.editorColor(Theme::ReplaceHighlight), QColor("#00ff00").rgba());
QCOMPARE(t.editorColor(Theme::BracketMatching), QColor("#ffff00").rgba());
QCOMPARE(t.editorColor(Theme::TabMarker), QColor("#d2d2d2").rgba());
QCOMPARE(t.editorColor(Theme::SpellChecking), QColor("#bf0303").rgba());
QCOMPARE(t.editorColor(Theme::IndentationLine), QColor("#d2d2d2").rgba());
QCOMPARE(t.editorColor(Theme::IconBorder), QColor("#f0f0f0").rgba());
QCOMPARE(t.editorColor(Theme::CodeFolding), QColor("#94caef").rgba());
QCOMPARE(t.editorColor(Theme::LineNumbers), QColor("#a0a0a0").rgba());
QCOMPARE(t.editorColor(Theme::CurrentLineNumber), QColor("#1e1e1e").rgba());
QCOMPARE(t.editorColor(Theme::WordWrapMarker), QColor("#ededed").rgba());
QCOMPARE(t.editorColor(Theme::ModifiedLines), QColor("#fdbc4b").rgba());
QCOMPARE(t.editorColor(Theme::SavedLines), QColor("#2ecc71").rgba());
QCOMPARE(t.editorColor(Theme::Separator), QColor("#d5d5d5").rgba());
QCOMPARE(t.editorColor(Theme::MarkBookmark), QColor("#0000ff").rgba());
QCOMPARE(t.editorColor(Theme::MarkBreakpointActive), QColor("#ff0000").rgba());
QCOMPARE(t.editorColor(Theme::MarkBreakpointReached), QColor("#ffff00").rgba());
QCOMPARE(t.editorColor(Theme::MarkBreakpointDisabled), QColor("#ff00ff").rgba());
QCOMPARE(t.editorColor(Theme::MarkExecution), QColor("#a0a0a4").rgba());
QCOMPARE(t.editorColor(Theme::MarkWarning), QColor("#00ff00").rgba());
QCOMPARE(t.editorColor(Theme::MarkError), QColor("#ff0000").rgba());
QCOMPARE(t.editorColor(Theme::TemplateBackground), QColor("#d6d2d0").rgba());
QCOMPARE(t.editorColor(Theme::TemplatePlaceholder), QColor("#baf8ce").rgba());
QCOMPARE(t.editorColor(Theme::TemplateFocusedPlaceholder), QColor("#76da98").rgba());
QCOMPARE(t.editorColor(Theme::TemplateReadOnlyPlaceholder), QColor("#f6e6e6").rgba());
}
void testInvalidTheme()
{
// somewhat complicated way to get proper Format objects
FormatCollector collector;
collector.setDefinition(m_repo.definitionForName(QLatin1String("QML")));
collector.highlightLine(u"normal + property real foo: 3.14", State());
QVERIFY(collector.formatMap.size() >= 4);
auto f = collector.formatMap.value(QLatin1String("Normal Text"));
QVERIFY(f.isValid());
QVERIFY(f.isDefaultTextStyle(Theme()));
QVERIFY(!f.hasTextColor(Theme()));
QVERIFY(!f.hasBackgroundColor(Theme()));
}
void testThemeIntegrity_data()
{
// cleanup before we test
QDir(QStringLiteral(TESTBUILDDIR "/theme.html.output/")).removeRecursively();
QDir().mkpath(QStringLiteral(TESTBUILDDIR "/theme.html.output/"));
QTest::addColumn<QString>("themeFileName");
QDirIterator it(QStringLiteral(":/org.kde.syntax-highlighting/themes"), QStringList() << QLatin1String("*.theme"), QDir::Files);
while (it.hasNext()) {
const QString fileName = it.next();
QTest::newRow(fileName.toLatin1().data()) << fileName;
}
}
static bool isColorEntry(const QString &entry)
{
static const QLatin1String predefColorEntries[] = {QLatin1String("text-color"),
QLatin1String("selected-text-color"),
QLatin1String("background-color"),
QLatin1String("selected-background-color")};
return std::find(std::begin(predefColorEntries), std::end(predefColorEntries), entry) != std::end(predefColorEntries);
}
static bool isFontStyleEntry(const QString &entry)
{
static const QLatin1String predefColorEntries[] = {QLatin1String("bold"),
QLatin1String("italic"),
QLatin1String("underline"),
QLatin1String("strike-through")};
return std::find(std::begin(predefColorEntries), std::end(predefColorEntries), entry) != std::end(predefColorEntries);
}
void verifyStyle(const QJsonObject &textStyle, const QString &textStyleName, QList<QString> &unknown)
{
const QStringList definedColors = textStyle.keys();
for (const auto &key : definedColors) {
const QString context = textStyleName + QLatin1Char('/') + key + QLatin1Char('=') + textStyle.value(key).toString();
if (isColorEntry(key)) {
QVERIFY2(QColor::isValidColorName(textStyle.value(key).toString()), context.toLatin1().data());
} else if (isFontStyleEntry(key)) {
QVERIFY2(textStyle.value(key).isBool(), context.toLatin1().data());
} else {
unknown.append(textStyleName + QLatin1Char('/') + key);
}
}
}
void testThemeIntegrity()
{
QFETCH(QString, themeFileName);
QFile loadFile(themeFileName);
QVERIFY(loadFile.open(QIODevice::ReadOnly));
const QByteArray jsonData = loadFile.readAll();
QJsonParseError parseError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Failed to parse theme file:" << parseError.errorString();
QVERIFY(false);
}
QJsonObject obj = jsonDoc.object();
// verify metadata
QVERIFY(obj.contains(QLatin1String("metadata")));
const QJsonObject metadata = obj.value(QLatin1String("metadata")).toObject();
QVERIFY(metadata.contains(QLatin1String("name")));
const auto themeName = metadata.value(QLatin1String("name")).toString();
QVERIFY(!themeName.isEmpty());
QVERIFY(metadata.contains(QLatin1String("revision")));
QVERIFY(metadata.value(QLatin1String("revision")).toInt() > 0);
// verify licensing part of the metadata
// ensure we have some copyright text attributes like "SPDX-FileCopyrightText: 2020 Christoph Cullmann <cullmann@kde.org>"
QVERIFY(metadata.contains(QLatin1String("copyright")));
const auto copyrights = metadata.value(QLatin1String("copyright")).toArray();
QVERIFY(!copyrights.empty());
for (const auto &copyright : copyrights) {
static const QRegularExpression copyrightRegex(QLatin1String("SPDX-FileCopyrightText: \\d{4} "));
QVERIFY(copyright.toString().indexOf(copyrightRegex) == 0);
}
// ensure the theme is MIT licensed with a proper SPDX identifier
// we always compile all themes into the library as resources, we want to have a "pure" MIT licensed library!
QVERIFY(metadata.contains(QLatin1String("license")));
QVERIFY(metadata.value(QLatin1String("license")).toString() == QLatin1String("SPDX-License-Identifier: MIT"));
// verify completeness of text styles
const auto metaEnum = QMetaEnum::fromType<Theme::TextStyle>();
QVERIFY(obj.contains(QLatin1String("text-styles")));
const QJsonObject textStyles = obj.value(QLatin1String("text-styles")).toObject();
for (int i = 0; i < metaEnum.keyCount(); ++i) {
QCOMPARE(i, metaEnum.value(i));
const QString textStyleName = QLatin1String(metaEnum.key(i));
QVERIFY(textStyles.contains(textStyleName));
const QJsonObject textStyle = textStyles.value(textStyleName).toObject();
QVERIFY(textStyle.contains(QLatin1String("text-color")));
// verify valid entry
QList<QString> unknown;
verifyStyle(textStyle, textStyleName, unknown);
if (!unknown.isEmpty()) {
qWarning() << "Unknown entries found in text-styles: " << unknown;
}
QVERIFY(unknown.isEmpty());
}
// editor area colors
const auto metaEnumColor = QMetaEnum::fromType<Theme::EditorColorRole>();
QStringList requiredEditorColors;
for (int i = 0; i < metaEnumColor.keyCount(); ++i) {
Q_ASSERT(i == metaEnumColor.value(i));
requiredEditorColors.append(QLatin1String(metaEnumColor.key(i)));
}
std::sort(requiredEditorColors.begin(), requiredEditorColors.end());
// verify all editor colors are defined - not more, not less
QVERIFY(obj.contains(QLatin1String("editor-colors")));
const QJsonObject editorColors = obj.value(QLatin1String("editor-colors")).toObject();
QStringList definedEditorColors = editorColors.keys();
std::sort(definedEditorColors.begin(), definedEditorColors.end());
QCOMPARE(definedEditorColors, requiredEditorColors);
// verify all editor colors are valid
for (const auto &key : requiredEditorColors) {
auto color = editorColors.value(key).toString();
QVERIFY2(QColor::isValidColorName(color), color.toStdString().c_str());
}
// verify custom-styles if any
{
QList<QPair<QString, QString>> invalidCustomStyles;
const auto customStyles = obj.value(QLatin1String("custom-styles")).toObject();
for (auto it = customStyles.constBegin(); it != customStyles.constEnd(); ++it) {
// get definitions for this language
const auto &lang = it.key();
const auto def = m_repo.definitionForName(lang);
QVERIFY2(def.isValid(), qPrintable(QStringLiteral("Definition %1 does not exist").arg(lang)));
const QList<Format> fmts = def.formats();
QSet<QString> fmtNames;
fmtNames.reserve(fmts.size());
for (const auto &fmt : fmts) {
fmtNames.insert(fmt.name());
}
// get custom style names for `lang` in this theme
// and make sure the language definition contain that name
const auto customStylesForLang = it.value().toObject();
for (auto csIt = customStylesForLang.constBegin(); csIt != customStylesForLang.constEnd(); ++csIt) {
// make sure the text style is present in language definition formats
const auto &textStyleName = csIt.key();
// wasn't found, append it to the vector
// we will later print this and fail the test
if (!fmtNames.contains(textStyleName)) {
invalidCustomStyles.append({lang, textStyleName});
continue;
}
// now verify this text style
const auto entry = csIt.value().toObject();
QList<QString> unknown;
verifyStyle(entry, textStyleName, unknown);
if (!unknown.isEmpty()) {
qWarning() << "Unknown entries found in custom-styles for " << lang << ": " << unknown;
}
QVERIFY(unknown.isEmpty());
}
}
if (!invalidCustomStyles.isEmpty()) {
qWarning() << "Unknown styles found: " << invalidCustomStyles;
}
QVERIFY(invalidCustomStyles.isEmpty());
}
// the theme must be available in our repository, too
const auto theme = m_repo.theme(themeName);
QVERIFY(theme.isValid());
QVERIFY(theme.name() == themeName);
// we have one fixed theme showcase
const QString inFile(QStringLiteral(TESTSRCDIR "/input/themes/showcase.cpp"));
// render some example HTML for the theme, we use that e.g. to show-case the themes on our website
const QString outFile(QStringLiteral(TESTBUILDDIR "/theme.html.output/") + QFileInfo(theme.filePath()).baseName() + QStringLiteral(".html"));
HtmlHighlighter highlighter;
highlighter.setTheme(theme);
QVERIFY(highlighter.theme().isValid());
highlighter.setDefinition(m_repo.definitionForFileName(inFile));
highlighter.setOutputFile(outFile);
highlighter.highlightFile(inFile);
}
};
}
QTEST_GUILESS_MAIN(KSyntaxHighlighting::ThemeTest)
#include "theme_test.moc"