diff --git a/autotests/foldingtest.cpp b/autotests/foldingtest.cpp index 5d10166..6417564 100644 --- a/autotests/foldingtest.cpp +++ b/autotests/foldingtest.cpp @@ -1,191 +1,191 @@ /* Copyright (C) 2016 Volker Krause Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "test-config.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KSyntaxHighlighting; class FoldingHighlighter : public AbstractHighlighter { public: void highlightFile(const QString &inFileName, const QString &outFileName) { QFile outFile(outFileName); if (!outFile.open(QFile::WriteOnly | QFile::Truncate)) { qWarning() << "Failed to open output file" << outFileName << ":" << outFile.errorString(); return; } m_out.setDevice(&outFile); m_out.setCodec("UTF-8"); QFile f(inFileName); if (!f.open(QFile::ReadOnly)) { qWarning() << "Failed to open input file" << inFileName << ":" << f.errorString(); return; } QTextStream in(&f); in.setCodec("UTF-8"); State state; bool indentationFoldEnabled = definition().indentationBasedFoldingEnabled(); if (indentationFoldEnabled) m_out << ""; while (!in.atEnd()) { const auto currentLine = in.readLine(); state = highlightLine(currentLine, state); if (indentationFoldEnabled != state.indentationBasedFoldingEnabled()) { indentationFoldEnabled = state.indentationBasedFoldingEnabled(); if (indentationFoldEnabled) m_out << ""; else m_out << ""; } int offset = 0; - foreach (const auto &fold, m_folds) { + for (const auto &fold : qAsConst(m_folds)) { m_out << currentLine.mid(offset, fold.offset - offset); if (fold.region.type() == FoldingRegion::Begin) m_out << ""; else m_out << ""; m_out << currentLine.mid(fold.offset, fold.length); if (fold.region.type() == FoldingRegion::Begin) m_out << ""; else m_out << ""; offset = fold.offset + fold.length; } m_out << currentLine.mid(offset) << '\n'; m_folds.clear(); } m_out.flush(); } protected: void applyFormat(int offset, int length, const Format &format) Q_DECL_OVERRIDE { Q_UNUSED(offset); Q_UNUSED(length); Q_UNUSED(format); } void applyFolding(int offset, int length, FoldingRegion region) Q_DECL_OVERRIDE { Q_ASSERT(region.isValid()); m_folds.push_back({offset, length, region}); } private: QTextStream m_out; struct Fold { int offset; int length; FoldingRegion region; }; QVector m_folds; }; class FoldingTest : public QObject { Q_OBJECT public: explicit FoldingTest(QObject *parent = nullptr) : QObject(parent) {} private: private Q_SLOTS: void initTestCase() { QStandardPaths::enableTestMode(true); } void testFolding_data() { QTest::addColumn("inFile"); QTest::addColumn("outFile"); QTest::addColumn("refFile"); QTest::addColumn("syntax"); const QDir dir(QStringLiteral(TESTSRCDIR "/input")); - foreach (const auto &fileName, dir.entryList(QDir::Files | QDir::NoSymLinks | QDir::Readable, QDir::Name)) { + for (const auto &fileName : dir.entryList(QDir::Files | QDir::NoSymLinks | QDir::Readable, QDir::Name)) { const auto inFile = dir.absoluteFilePath(fileName); if (inFile.endsWith(QLatin1String(".syntax"))) continue; QString syntax; QFile syntaxOverride(inFile + QStringLiteral(".syntax")); if (syntaxOverride.exists() && syntaxOverride.open(QFile::ReadOnly)) syntax = QString::fromUtf8(syntaxOverride.readAll()).trimmed(); QTest::newRow(fileName.toUtf8().constData()) << inFile << (QStringLiteral(TESTBUILDDIR "/folding.out/") + fileName + QStringLiteral(".fold")) << (QStringLiteral(TESTSRCDIR "/folding/") + fileName + QStringLiteral(".fold")) << syntax; } QDir().mkpath(QStringLiteral(TESTBUILDDIR "/folding.out/")); } void testFolding() { QFETCH(QString, inFile); QFETCH(QString, outFile); QFETCH(QString, refFile); QFETCH(QString, syntax); Repository m_repo; initRepositorySearchPaths(m_repo); auto def = m_repo.definitionForFileName(inFile); if (!syntax.isEmpty()) def = m_repo.definitionForName(syntax); FoldingHighlighter highlighter; QVERIFY(def.isValid()); highlighter.setDefinition(def); highlighter.highlightFile(inFile, outFile); /** * compare results */ compareFiles(refFile, outFile); } }; QTEST_GUILESS_MAIN(FoldingTest) #include "foldingtest.moc" diff --git a/autotests/highlighter_benchmark.cpp b/autotests/highlighter_benchmark.cpp index 0b945ff..2a4068a 100644 --- a/autotests/highlighter_benchmark.cpp +++ b/autotests/highlighter_benchmark.cpp @@ -1,145 +1,145 @@ /* Copyright (C) 2016 Volker Krause Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "test-config.h" #include #include #include #include #include #include #include #include using namespace KSyntaxHighlighting; class NullHighlighter : public AbstractHighlighter { public: /** * Read in the given file and cache it for the highlighting benchmarking * @param inFileName file to read */ NullHighlighter(const QString &inFileName) { QFile f(inFileName); if (!f.open(QFile::ReadOnly)) { qWarning() << "Failed to open input file" << inFileName << ":" << f.errorString(); return; } QTextStream in(&f); in.setCodec("UTF-8"); while (!in.atEnd()) m_fileContents.append(in.readLine()); } /** * highlight the in-memory stored file * @return number of highlighted lines */ int highlightFile() { State state; for (const auto &line : qAsConst(m_fileContents)) state = highlightLine(line, state); return m_fileContents.size(); } protected: void applyFormat(int, int, const Format&) Q_DECL_OVERRIDE {} QStringList m_fileContents; }; class HighlighterBenchmark : public QObject { Q_OBJECT public: explicit HighlighterBenchmark(QObject *parent = nullptr) : QObject(parent) {} private: Repository m_repo; private Q_SLOTS: void initTestCase() { initRepositorySearchPaths(m_repo); } void cleanupTestCase() { } void benchmarkHighlight_data() { QTest::addColumn("inFile"); QTest::addColumn("syntax"); const QDir dir(QStringLiteral(TESTSRCDIR "/input")); - foreach (const auto &fileName, dir.entryList(QDir::Files | QDir::NoSymLinks | QDir::Readable, QDir::Name)) { + for (const auto &fileName : dir.entryList(QDir::Files | QDir::NoSymLinks | QDir::Readable, QDir::Name)) { const auto inFile = dir.absoluteFilePath(fileName); if (inFile.endsWith(QLatin1String(".syntax"))) continue; QString syntax; QFile syntaxOverride(inFile + QStringLiteral(".syntax")); if (syntaxOverride.exists() && syntaxOverride.open(QFile::ReadOnly)) syntax = QString::fromUtf8(syntaxOverride.readAll()).trimmed(); QTest::newRow(fileName.toUtf8().constData()) << inFile << syntax; } } void benchmarkHighlight() { QFETCH(QString, inFile); QFETCH(QString, syntax); NullHighlighter highlighter(inFile); auto def = m_repo.definitionForFileName(inFile); if (!syntax.isEmpty()) def = m_repo.definitionForName(syntax); QVERIFY(def.isValid()); highlighter.setDefinition(def); // trigger loading of definition per benchmarking loop QVERIFY(!def.formats().isEmpty()); // benchmark the highlighting // try to highlight ~ 20000 lines per file // bail out, if file is empty, else we are stuck for(int i = 0; i <= 20000;) { int lines = highlighter.highlightFile(); if (lines <= 0) break; i += lines; } } }; QTEST_GUILESS_MAIN(HighlighterBenchmark) #include "highlighter_benchmark.moc" diff --git a/autotests/htmlhighlighter_test.cpp b/autotests/htmlhighlighter_test.cpp index f7cc30a..789ab0a 100644 --- a/autotests/htmlhighlighter_test.cpp +++ b/autotests/htmlhighlighter_test.cpp @@ -1,121 +1,121 @@ /* Copyright (C) 2016 Volker Krause Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "test-config.h" #include #include #include #include #include #include #include #include #include using namespace KSyntaxHighlighting; class HTMLHighlighterTest : public QObject { Q_OBJECT public: explicit HTMLHighlighterTest(QObject *parent = nullptr) : QObject(parent), m_repo(nullptr) {} private: Repository *m_repo; private Q_SLOTS: void initTestCase() { QStandardPaths::enableTestMode(true); m_repo = new Repository; initRepositorySearchPaths(*m_repo); } void cleanupTestCase() { delete m_repo; m_repo = nullptr; } void testHighlight_data() { QTest::addColumn("inFile"); QTest::addColumn("outFile"); QTest::addColumn("refFile"); QTest::addColumn("syntax"); const QDir dir(QStringLiteral(TESTSRCDIR "/input")); - foreach (const auto &fileName, dir.entryList(QDir::Files | QDir::NoSymLinks | QDir::Readable, QDir::Name)) { + for (const auto &fileName : dir.entryList(QDir::Files | QDir::NoSymLinks | QDir::Readable, QDir::Name)) { const auto inFile = dir.absoluteFilePath(fileName); if (inFile.endsWith(QLatin1String(".syntax"))) continue; QString syntax; QFile syntaxOverride(inFile + QStringLiteral(".syntax")); if (syntaxOverride.exists() && syntaxOverride.open(QFile::ReadOnly)) syntax = QString::fromUtf8(syntaxOverride.readAll()).trimmed(); QTest::newRow(fileName.toUtf8().constData()) << inFile << (QStringLiteral(TESTBUILDDIR "/html.output/") + fileName + QStringLiteral(".html")) << (QStringLiteral(TESTSRCDIR "/html/") + fileName + QStringLiteral(".html")) << syntax; } QDir().mkpath(QStringLiteral(TESTBUILDDIR "/html.output/")); } void testHighlight() { QFETCH(QString, inFile); QFETCH(QString, outFile); QFETCH(QString, refFile); QFETCH(QString, syntax); QVERIFY(m_repo); HtmlHighlighter highlighter; highlighter.setTheme(m_repo->defaultTheme()); QVERIFY(highlighter.theme().isValid()); auto def = m_repo->definitionForFileName(inFile); if (!syntax.isEmpty()) def = m_repo->definitionForName(syntax); QVERIFY(def.isValid()); highlighter.setDefinition(def); highlighter.setOutputFile(outFile); highlighter.highlightFile(inFile); /** * compare results */ compareFiles(refFile, outFile); } }; QTEST_GUILESS_MAIN(HTMLHighlighterTest) #include "htmlhighlighter_test.moc" diff --git a/autotests/syntaxrepository_test.cpp b/autotests/syntaxrepository_test.cpp index 5c945ac..2ad0e97 100644 --- a/autotests/syntaxrepository_test.cpp +++ b/autotests/syntaxrepository_test.cpp @@ -1,496 +1,496 @@ /* Copyright (C) 2016 Volker Krause Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "test-config.h" #include #include #include #include #include #include #include #include #include #include namespace KSyntaxHighlighting { class NullHighlighter : public AbstractHighlighter { public: using AbstractHighlighter::highlightLine; void applyFormat(int offset, int length, const Format &format) Q_DECL_OVERRIDE { Q_UNUSED(offset); Q_UNUSED(length); // only here to ensure we don't crash format.isDefaultTextStyle(theme()); format.textColor(theme()); } }; class RepositoryTest : public QObject { Q_OBJECT private: Repository m_repo; private Q_SLOTS: void initTestCase() { QStandardPaths::enableTestMode(true); initRepositorySearchPaths(m_repo); } void testDefinitionByExtension_data() { QTest::addColumn("fileName"); QTest::addColumn("defName"); QTest::newRow("empty") << QString() << QString(); QTest::newRow("qml") << QStringLiteral("/bla/foo.qml") << QStringLiteral("QML"); QTest::newRow("glsl") << QStringLiteral("flat.frag") << QStringLiteral("GLSL"); // the following ones are defined in multiple syntax definitions QTest::newRow("c") << QStringLiteral("test.c") << QStringLiteral("C"); QTest::newRow("c++") << QStringLiteral("test.cpp") << QStringLiteral("C++"); QTest::newRow("markdown") << QStringLiteral("test.md") << QStringLiteral("Markdown"); QTest::newRow("Makefile 1") << QStringLiteral("Makefile") << QStringLiteral("Makefile"); QTest::newRow("Makefile 2") << QStringLiteral("/some/path/to/Makefile") << QStringLiteral("Makefile"); QTest::newRow("Makefile 3") << QStringLiteral("Makefile.am") << QStringLiteral("Makefile"); } void testDefinitionByExtension() { QFETCH(QString, fileName); QFETCH(QString, defName); auto def = m_repo.definitionForFileName(fileName); if (defName.isEmpty()) { QVERIFY(!def.isValid()); } else { QVERIFY(def.isValid()); QCOMPARE(def.name(), defName); } } void testLoadAll() { - foreach (const auto &def, m_repo.definitions()) { + for (const auto &def : m_repo.definitions()) { QVERIFY(!def.name().isEmpty()); QVERIFY(!def.translatedName().isEmpty()); QVERIFY(!def.isValid() || !def.section().isEmpty()); QVERIFY(!def.isValid() || !def.translatedSection().isEmpty()); // indirectly trigger loading, as we can't reach that from public API // if the loading fails the highlighter will produce empty states NullHighlighter hl; State initialState; hl.setDefinition(def); const auto state = hl.highlightLine(QLatin1String("This should not crash } ] ) !"), initialState); QVERIFY(!def.isValid() || state != initialState); } } void testMetaData() { auto def = m_repo.definitionForName(QLatin1String("Alerts")); QVERIFY(def.isValid()); QVERIFY(def.extensions().isEmpty()); QVERIFY(def.mimeTypes().isEmpty()); QVERIFY(def.version() >= 1.11f); QVERIFY(def.isHidden()); QCOMPARE(def.section(), QLatin1String("Other")); QCOMPARE(def.license(), QLatin1String("MIT")); QVERIFY(def.author().contains(QLatin1String("Dominik"))); QFileInfo fi(def.filePath()); QVERIFY(fi.isAbsolute()); QVERIFY(def.filePath().endsWith(QLatin1String("alert.xml"))); def = m_repo.definitionForName(QLatin1String("C++")); QVERIFY(def.isValid()); QCOMPARE(def.section(), QLatin1String("Sources")); QCOMPARE(def.indenter(), QLatin1String("cstyle")); QCOMPARE(def.style(), QLatin1String("C++")); QVERIFY(def.mimeTypes().contains(QLatin1String("text/x-c++hdr"))); QVERIFY(def.extensions().contains(QLatin1String("*.h"))); QCOMPARE(def.priority(), 9); def = m_repo.definitionForName(QLatin1String("Apache Configuration")); QVERIFY(def.isValid()); QVERIFY(def.extensions().contains(QLatin1String("httpd.conf"))); QVERIFY(def.extensions().contains(QLatin1String(".htaccess*"))); } void testGeneralMetaData() { auto def = m_repo.definitionForName(QLatin1String("C++")); QVERIFY(def.isValid()); QVERIFY(!def.indentationBasedFoldingEnabled()); // comment markers QCOMPARE(def.singleLineCommentMarker(), QLatin1String("//")); QCOMPARE(def.singleLineCommentPosition(), KSyntaxHighlighting::CommentPosition::StartOfLine); const auto cppMultiLineCommentMarker = QPair(QLatin1String("/*"), QLatin1String("*/")); QCOMPARE(def.multiLineCommentMarker(), cppMultiLineCommentMarker); def = m_repo.definitionForName(QLatin1String("Python")); QVERIFY(def.isValid()); // indentation QVERIFY(def.indentationBasedFoldingEnabled()); QCOMPARE(def.foldingIgnoreList(), QStringList() << QLatin1String("(?:\\s+|\\s*#.*)")); // keyword lists QVERIFY(!def.keywordLists().isEmpty()); QVERIFY(def.keywordList(QLatin1String("operators")).contains(QLatin1String("and"))); QVERIFY(!def.keywordList(QLatin1String("does not exits")).contains(QLatin1String("and"))); } void testFormatData() { auto def = m_repo.definitionForName(QLatin1String("ChangeLog")); QVERIFY(def.isValid()); auto formats = def.formats(); QVERIFY(!formats.isEmpty()); // verify that the formats are sorted, such that the order matches the order of the itemDatas in the xml files. auto sortComparator = [](const KSyntaxHighlighting::Format & lhs, const KSyntaxHighlighting::Format & rhs) { return lhs.id() < rhs.id(); }; QVERIFY(std::is_sorted(formats.begin(), formats.end(), sortComparator)); // check all names are listed QStringList formatNames; - foreach (const auto & format, formats) { + for (const auto & format : qAsConst(formats)) { formatNames.append(format.name()); } const QStringList expectedItemDatas = { QStringLiteral("Normal Text"), QStringLiteral("Name"), QStringLiteral("E-Mail"), QStringLiteral("Date"), QStringLiteral("Entry") }; QCOMPARE(formatNames, expectedItemDatas); } void testIncludedDefinitions() { auto def = m_repo.definitionForName(QLatin1String("PHP (HTML)")); QVERIFY(def.isValid()); auto defs = def.includedDefinitions(); const QStringList expectedDefinitionNames = { QStringLiteral("PHP/PHP"), QStringLiteral("Alerts"), QStringLiteral("CSS/PHP"), QStringLiteral("JavaScript/PHP"), QStringLiteral("Doxygen"), QStringLiteral("Modelines"), QStringLiteral("HTML"), QStringLiteral("CSS"), QStringLiteral("SQL (MySQL)"), QStringLiteral("JavaScript") }; QStringList definitionNames; for (auto d : defs) { QVERIFY(d.isValid()); definitionNames.push_back(d.name()); } QCOMPARE(definitionNames, expectedDefinitionNames); } void testIncludedFormats() { QStringList definitionNames; - foreach (const auto &def, m_repo.definitions()) { + for (const auto &def : m_repo.definitions()) { definitionNames.push_back(def.name()); } - foreach (const QString & name, definitionNames) { + for (const QString & name : qAsConst(definitionNames)) { Repository repo; initRepositorySearchPaths(repo); auto def = repo.definitionForName(name); QCOMPARE(m_repo.definitionForName(name).isValid(), def.isValid()); auto includedDefs = def.includedDefinitions(); includedDefs.push_front(def); // collect all formats, shall be numbered from 1.. QSet formatIds; for (auto d : qAsConst(includedDefs)) { const auto formats = d.formats(); for (const auto format : formats) { // no duplicates QVERIFY(!formatIds.contains(format.id())); formatIds.insert(format.id()); } } QVERIFY(!def.isValid() || !formatIds.isEmpty()); // ensure all ids are there from 1..size for (int i = 1; i <= formatIds.size(); ++i) { QVERIFY(formatIds.contains(i)); } } } void testReload() { auto def = m_repo.definitionForName(QLatin1String("QML")); QVERIFY(!m_repo.definitions().isEmpty()); QVERIFY(def.isValid()); NullHighlighter hl; hl.setDefinition(def); auto oldState = hl.highlightLine(QLatin1String("/* TODO this should not crash */"), State()); m_repo.reload(); QVERIFY(!m_repo.definitions().isEmpty()); QVERIFY(!def.isValid()); hl.highlightLine(QLatin1String("/* TODO this should not crash */"), State()); hl.highlightLine(QLatin1String("/* FIXME neither should this crash */"), oldState); QVERIFY(hl.definition().isValid()); QCOMPARE(hl.definition().name(), QLatin1String("QML")); } void testLifetime() { // common mistake with value-type like Repo API, make sure this doesn'T crash NullHighlighter hl; { Repository repo; hl.setDefinition(repo.definitionForName(QLatin1String("C++"))); hl.setTheme(repo.defaultTheme()); } hl.highlightLine(QLatin1String("/**! @todo this should not crash .*/"), State()); } void testCustomPath() { QString testInputPath = QStringLiteral(TESTSRCDIR "/input"); Repository repo; QVERIFY(repo.customSearchPaths().empty()); repo.addCustomSearchPath(testInputPath); QCOMPARE(repo.customSearchPaths().size(), 1); QCOMPARE(repo.customSearchPaths()[0], testInputPath); auto customDefinition = repo.definitionForName(QLatin1String("Test Syntax")); QVERIFY(customDefinition.isValid()); auto customTheme = repo.theme(QLatin1String("Test Theme")); QVERIFY(customTheme.isValid()); } void testInvalidDefinition() { Definition def; QVERIFY(!def.isValid()); QVERIFY(def.filePath().isEmpty()); QCOMPARE(def.name(), QLatin1String("None")); QVERIFY(def.section().isEmpty()); QVERIFY(def.translatedSection().isEmpty()); QVERIFY(def.mimeTypes().isEmpty()); QVERIFY(def.extensions().isEmpty()); QCOMPARE(def.version(), 0); QCOMPARE(def.priority(), 0); QVERIFY(!def.isHidden()); QVERIFY(def.style().isEmpty()); QVERIFY(def.indenter().isEmpty()); QVERIFY(def.author().isEmpty()); QVERIFY(def.license().isEmpty()); QVERIFY(!def.foldingEnabled()); QVERIFY(!def.indentationBasedFoldingEnabled()); QVERIFY(def.foldingIgnoreList().isEmpty()); QVERIFY(def.keywordLists().isEmpty()); QVERIFY(def.formats().isEmpty()); QVERIFY(def.includedDefinitions().isEmpty()); QVERIFY(def.singleLineCommentMarker().isEmpty()); QCOMPARE(def.singleLineCommentPosition(), KSyntaxHighlighting::CommentPosition::StartOfLine); const auto emptyPair = QPair(); QCOMPARE(def.multiLineCommentMarker(), emptyPair); QVERIFY(def.characterEncodings().isEmpty()); for (QChar c : QStringLiteral("\t !%&()*+,-./:;<=>?[\\]^{|}~")) { QVERIFY(def.isWordDelimiter(c)); QVERIFY(def.isWordWrapDelimiter(c)); } } void testDelimiters() { auto def = m_repo.definitionForName(QLatin1String("LaTeX")); QVERIFY(def.isValid()); // check that backslash '\' is removed for (QChar c : QStringLiteral("\t !%&()*+,-./:;<=>?[]^{|}~")) QVERIFY(def.isWordDelimiter(c)); QVERIFY(!def.isWordDelimiter(QLatin1Char('\\'))); // check where breaking a line is valid for (QChar c : QStringLiteral(",{}[]")) QVERIFY(def.isWordWrapDelimiter(c)); } void testFoldingEnabled() { // test invalid folding Definition def; QVERIFY(!def.isValid()); QVERIFY(!def.foldingEnabled()); QVERIFY(!def.indentationBasedFoldingEnabled()); // test no folding def = m_repo.definitionForName(QLatin1String("ChangeLog")); QVERIFY(def.isValid()); QVERIFY(!def.foldingEnabled()); QVERIFY(!def.indentationBasedFoldingEnabled()); // C++ itself has no regions, but it includes ISO C++ def = m_repo.definitionForName(QLatin1String("C++")); QVERIFY(def.isValid()); QVERIFY(def.foldingEnabled()); QVERIFY(!def.indentationBasedFoldingEnabled()); // ISO C++ itself has folding regions def = m_repo.definitionForName(QLatin1String("ISO C++")); QVERIFY(def.isValid()); QVERIFY(def.foldingEnabled()); QVERIFY(!def.indentationBasedFoldingEnabled()); // Python has indentation based folding def = m_repo.definitionForName(QLatin1String("Python")); QVERIFY(def.isValid()); QVERIFY(def.foldingEnabled()); QVERIFY(def.indentationBasedFoldingEnabled()); } void testCharacterEncodings() { auto def = m_repo.definitionForName(QLatin1String("LaTeX")); QVERIFY(def.isValid()); const auto encodings = def.characterEncodings(); QVERIFY(!encodings.isEmpty()); QVERIFY(encodings.contains({ QChar(196), QLatin1String("\\\"{A}") })); QVERIFY(encodings.contains({ QChar(227), QLatin1String("\\~{a}") })); } void testIncludeKeywordLists() { Repository repo; QTemporaryDir dir; // forge a syntax file { QVERIFY(QDir(dir.path()).mkpath(QLatin1String("syntax"))); const char syntax[] = R"xml( c a b a c b d e##AAA e f f )xml"; QFile file(dir.path() + QLatin1String("/syntax/a.xml")); QVERIFY(file.open(QIODevice::WriteOnly)); QTextStream stream(&file); stream << syntax; } repo.addCustomSearchPath(dir.path()); auto def = repo.definitionForName(QLatin1String("AAA")); QCOMPARE(def.name(), QLatin1String("AAA")); auto klist1 = def.keywordList(QLatin1String("a")); auto klist2 = def.keywordList(QLatin1String("b")); auto klist3 = def.keywordList(QLatin1String("c")); // internal QHash is arbitrarily ordered and undeterministic auto& klist = klist1.size() == 3 ? klist1 : klist2.size() == 3 ? klist2 : klist3; QCOMPARE(klist.size(), 3); QVERIFY(klist.contains(QLatin1String("a"))); QVERIFY(klist.contains(QLatin1String("b"))); QVERIFY(klist.contains(QLatin1String("c"))); klist = def.keywordList(QLatin1String("d")); QCOMPARE(klist.size(), 3); QVERIFY(klist.contains(QLatin1String("d"))); QVERIFY(klist.contains(QLatin1String("e"))); QVERIFY(klist.contains(QLatin1String("f"))); klist = def.keywordList(QLatin1String("e")); QCOMPARE(klist.size(), 2); QVERIFY(klist.contains(QLatin1String("e"))); QVERIFY(klist.contains(QLatin1String("f"))); klist = def.keywordList(QLatin1String("f")); QCOMPARE(klist.size(), 1); QVERIFY(klist.contains(QLatin1String("f"))); } }; } QTEST_GUILESS_MAIN(KSyntaxHighlighting::RepositoryTest) #include "syntaxrepository_test.moc" diff --git a/autotests/testhighlighter.cpp b/autotests/testhighlighter.cpp index fad0300..508fdb5 100644 --- a/autotests/testhighlighter.cpp +++ b/autotests/testhighlighter.cpp @@ -1,189 +1,189 @@ /* Copyright (C) 2016 Volker Krause Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "test-config.h" #include #include #include #include #include #include #include #include #include #include #include using namespace KSyntaxHighlighting; class TestHighlighter : public AbstractHighlighter { public: void highlightFile(const QString &inFileName, const QString &outFileName) { QFile outFile(outFileName); if (!outFile.open(QFile::WriteOnly | QFile::Truncate)) { qWarning() << "Failed to open output file" << outFileName << ":" << outFile.errorString(); return; } m_out.setDevice(&outFile); m_out.setCodec("UTF-8"); QFile f(inFileName); if (!f.open(QFile::ReadOnly)) { qWarning() << "Failed to open input file" << inFileName << ":" << f.errorString(); return; } QTextStream in(&f); in.setCodec("UTF-8"); State state; while (!in.atEnd()) { m_currentLine = in.readLine(); state = highlightLine(m_currentLine, state); m_out << "
\n"; } m_out.flush(); } protected: void applyFormat(int offset, int length, const Format &format) Q_DECL_OVERRIDE { if (format.name().isEmpty()) m_out << "" << m_currentLine.midRef(offset, length) << ""; else m_out << "<" << format.name() << ">" << m_currentLine.midRef(offset, length) << ""; } private: QTextStream m_out; QString m_currentLine; }; class TestHighlighterTest : public QObject { Q_OBJECT public: explicit TestHighlighterTest(QObject *parent = nullptr) : QObject(parent), m_repo(nullptr) {} private: Repository *m_repo; QSet m_coveredDefinitions; private Q_SLOTS: void initTestCase() { QStandardPaths::enableTestMode(true); m_repo = new Repository; initRepositorySearchPaths(*m_repo); } void cleanupTestCase() { QFile coveredList(QLatin1String(TESTBUILDDIR "/covered-definitions.txt")); QFile uncoveredList(QLatin1String(TESTBUILDDIR "/uncovered-definition.txt")); QVERIFY(coveredList.open(QFile::WriteOnly)); QVERIFY(uncoveredList.open(QFile::WriteOnly)); int count = 0; - foreach (const auto &def, m_repo->definitions()) { + for (const auto &def : m_repo->definitions()) { if (def.isHidden() || !def.isValid()) continue; ++count; if (m_coveredDefinitions.contains(def.name())) coveredList.write(def.name().toUtf8() + '\n'); else uncoveredList.write(def.name().toUtf8() + '\n'); } qDebug() << "Syntax definitions with test coverage:" << ((float)m_coveredDefinitions.size() * 100.0f / (float)count) << "%"; delete m_repo; m_repo = nullptr; } void testHighlight_data() { QTest::addColumn("inFile"); QTest::addColumn("outFile"); QTest::addColumn("refFile"); QTest::addColumn("syntax"); const QDir dir(QStringLiteral(TESTSRCDIR "/input")); - foreach (const auto &fileName, dir.entryList(QDir::Files | QDir::NoSymLinks | QDir::Readable, QDir::Name)) { + for (const auto &fileName : dir.entryList(QDir::Files | QDir::NoSymLinks | QDir::Readable, QDir::Name)) { const auto inFile = dir.absoluteFilePath(fileName); if (inFile.endsWith(QLatin1String(".syntax"))) continue; QString syntax; QFile syntaxOverride(inFile + QStringLiteral(".syntax")); if (syntaxOverride.exists() && syntaxOverride.open(QFile::ReadOnly)) syntax = QString::fromUtf8(syntaxOverride.readAll()).trimmed(); QTest::newRow(fileName.toUtf8().constData()) << inFile << (QStringLiteral(TESTBUILDDIR "/output/") + fileName + QStringLiteral(".ref")) << (QStringLiteral(TESTSRCDIR "/reference/") + fileName + QStringLiteral(".ref")) << syntax; } QVERIFY(QDir().mkpath(QStringLiteral(TESTBUILDDIR "/output/"))); } void testHighlight() { QFETCH(QString, inFile); QFETCH(QString, outFile); QFETCH(QString, refFile); QFETCH(QString, syntax); QVERIFY(m_repo); auto def = m_repo->definitionForFileName(inFile); if (!syntax.isEmpty()) def = m_repo->definitionForName(syntax); TestHighlighter highlighter; highlighter.setTheme(m_repo->defaultTheme()); QVERIFY(highlighter.theme().isValid()); QVERIFY(def.isValid()); qDebug() << "Using syntax" << def.name(); m_coveredDefinitions.insert(def.name()); highlighter.setDefinition(def); highlighter.highlightFile(inFile, outFile); /** * compare results */ compareFiles(refFile, outFile); } }; QTEST_GUILESS_MAIN(TestHighlighterTest) #include "testhighlighter.moc" diff --git a/examples/codeeditor.cpp b/examples/codeeditor.cpp index 88f3154..603c61e 100644 --- a/examples/codeeditor.cpp +++ b/examples/codeeditor.cpp @@ -1,358 +1,358 @@ /* Copyright (C) 2016 Volker Krause Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "codeeditor.h" #include #include #include #include #include #include #include #include #include #include #include #include class CodeEditorSidebar : public QWidget { Q_OBJECT public: explicit CodeEditorSidebar(CodeEditor *editor); QSize sizeHint() const Q_DECL_OVERRIDE; protected: void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; void mouseReleaseEvent(QMouseEvent *event) Q_DECL_OVERRIDE; private: CodeEditor *m_codeEditor; }; CodeEditorSidebar::CodeEditorSidebar(CodeEditor *editor) : QWidget(editor), m_codeEditor(editor) { } QSize CodeEditorSidebar::sizeHint() const { return QSize(m_codeEditor->sidebarWidth(), 0); } void CodeEditorSidebar::paintEvent(QPaintEvent *event) { m_codeEditor->sidebarPaintEvent(event); } void CodeEditorSidebar::mouseReleaseEvent(QMouseEvent *event) { if (event->x() >= width() - m_codeEditor->fontMetrics().lineSpacing()) { auto block = m_codeEditor->blockAtPosition(event->y()); if (!block.isValid() || !m_codeEditor->isFoldable(block)) return; m_codeEditor->toggleFold(block); } QWidget::mouseReleaseEvent(event); } CodeEditor::CodeEditor(QWidget *parent) : QPlainTextEdit(parent), m_highlighter(new KSyntaxHighlighting::SyntaxHighlighter(document())), m_sideBar(new CodeEditorSidebar(this)) { setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); setTheme((palette().color(QPalette::Base).lightness() < 128) ? m_repository.defaultTheme(KSyntaxHighlighting::Repository::DarkTheme) : m_repository.defaultTheme(KSyntaxHighlighting::Repository::LightTheme)); connect(this, &QPlainTextEdit::blockCountChanged, this, &CodeEditor::updateSidebarGeometry); connect(this, &QPlainTextEdit::updateRequest, this, &CodeEditor::updateSidebarArea); connect(this, &QPlainTextEdit::cursorPositionChanged, this, &CodeEditor::highlightCurrentLine); updateSidebarGeometry(); highlightCurrentLine(); } CodeEditor::~CodeEditor() { } void CodeEditor::openFile(const QString& fileName) { QFile f(fileName); if (!f.open(QFile::ReadOnly)) { qWarning() << "Failed to open" << fileName << ":" << f.errorString(); return; } clear(); const auto def = m_repository.definitionForFileName(fileName); m_highlighter->setDefinition(def); setWindowTitle(fileName); setPlainText(QString::fromUtf8(f.readAll())); } void CodeEditor::contextMenuEvent(QContextMenuEvent *event) { auto menu = createStandardContextMenu(event->pos()); menu->addSeparator(); auto openAction = menu->addAction(QStringLiteral("Open File...")); connect(openAction, &QAction::triggered, this, [this]() { const auto fileName = QFileDialog::getOpenFileName(this, QStringLiteral("Open File")); if (!fileName.isEmpty()) openFile(fileName); }); // syntax selection auto hlActionGroup = new QActionGroup(menu); hlActionGroup->setExclusive(true); auto hlGroupMenu = menu->addMenu(QStringLiteral("Syntax")); QMenu *hlSubMenu = hlGroupMenu; QString currentGroup; - foreach (const auto &def, m_repository.definitions()) { + for (const auto &def : m_repository.definitions()) { if (def.isHidden()) continue; if (currentGroup != def.section()) { currentGroup = def.section(); hlSubMenu = hlGroupMenu->addMenu(def.translatedSection()); } Q_ASSERT(hlSubMenu); auto action = hlSubMenu->addAction(def.translatedName()); action->setCheckable(true); action->setData(def.name()); hlActionGroup->addAction(action); if (def.name() == m_highlighter->definition().name()) action->setChecked(true); } connect(hlActionGroup, &QActionGroup::triggered, this, [this](QAction *action) { const auto defName = action->data().toString(); const auto def = m_repository.definitionForName(defName); m_highlighter->setDefinition(def); }); // theme selection auto themeGroup = new QActionGroup(menu); themeGroup->setExclusive(true); auto themeMenu = menu->addMenu(QStringLiteral("Theme")); - foreach (const auto &theme, m_repository.themes()) { + for (const auto &theme : m_repository.themes()) { auto action = themeMenu->addAction(theme.translatedName()); action->setCheckable(true); action->setData(theme.name()); themeGroup->addAction(action); if (theme.name() == m_highlighter->theme().name()) action->setChecked(true); } connect(themeGroup, &QActionGroup::triggered, this, [this](QAction *action) { const auto themeName = action->data().toString(); const auto theme = m_repository.theme(themeName); setTheme(theme); }); menu->exec(event->globalPos()); delete menu; } void CodeEditor::resizeEvent(QResizeEvent *event) { QPlainTextEdit::resizeEvent(event); updateSidebarGeometry(); } void CodeEditor::setTheme(const KSyntaxHighlighting::Theme &theme) { auto pal = qApp->palette(); if (theme.isValid()) { pal.setColor(QPalette::Base, theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor)); pal.setColor(QPalette::Text, theme.textColor(KSyntaxHighlighting::Theme::Normal)); pal.setColor(QPalette::Highlight, theme.editorColor(KSyntaxHighlighting::Theme::TextSelection)); } setPalette(pal); m_highlighter->setTheme(theme); m_highlighter->rehighlight(); highlightCurrentLine(); } int CodeEditor::sidebarWidth() const { int digits = 1; auto count = blockCount(); while (count >= 10) { ++digits; count /= 10; } return 4 + fontMetrics().width(QLatin1Char('9')) * digits + fontMetrics().lineSpacing(); } void CodeEditor::sidebarPaintEvent(QPaintEvent *event) { QPainter painter(m_sideBar); painter.fillRect(event->rect(), m_highlighter->theme().editorColor(KSyntaxHighlighting::Theme::IconBorder)); auto block = firstVisibleBlock(); auto blockNumber = block.blockNumber(); int top = blockBoundingGeometry(block).translated(contentOffset()).top(); int bottom = top + blockBoundingRect(block).height(); const int currentBlockNumber = textCursor().blockNumber(); const auto foldingMarkerSize = fontMetrics().lineSpacing(); while (block.isValid() && top <= event->rect().bottom()) { if (block.isVisible() && bottom >= event->rect().top()) { const auto number = QString::number(blockNumber + 1); painter.setPen(m_highlighter->theme().editorColor( (blockNumber == currentBlockNumber) ? KSyntaxHighlighting::Theme::CurrentLineNumber : KSyntaxHighlighting::Theme::LineNumbers)); painter.drawText(0, top, m_sideBar->width() - 2 - foldingMarkerSize, fontMetrics().height(), Qt::AlignRight, number); } // folding marker if (block.isVisible() && isFoldable(block)) { QPolygonF polygon; if (isFolded(block)) { polygon << QPointF(foldingMarkerSize * 0.4, foldingMarkerSize * 0.25); polygon << QPointF(foldingMarkerSize * 0.4, foldingMarkerSize * 0.75); polygon << QPointF(foldingMarkerSize * 0.8, foldingMarkerSize * 0.5); } else { polygon << QPointF(foldingMarkerSize * 0.25, foldingMarkerSize * 0.4); polygon << QPointF(foldingMarkerSize * 0.75, foldingMarkerSize * 0.4); polygon << QPointF(foldingMarkerSize * 0.5, foldingMarkerSize * 0.8); } painter.save(); painter.setRenderHint(QPainter::Antialiasing); painter.setPen(Qt::NoPen); painter.setBrush(QColor(m_highlighter->theme().editorColor(KSyntaxHighlighting::Theme::CodeFolding))); painter.translate(m_sideBar->width() - foldingMarkerSize, top); painter.drawPolygon(polygon); painter.restore(); } block = block.next(); top = bottom; bottom = top + blockBoundingRect(block).height(); ++blockNumber; } } void CodeEditor::updateSidebarGeometry() { setViewportMargins(sidebarWidth(), 0, 0, 0); const auto r = contentsRect(); m_sideBar->setGeometry(QRect(r.left(), r.top(), sidebarWidth(), r.height())); } void CodeEditor::updateSidebarArea(const QRect& rect, int dy) { if (dy) m_sideBar->scroll(0, dy); else m_sideBar->update(0, rect.y(), m_sideBar->width(), rect.height()); } void CodeEditor::highlightCurrentLine() { QTextEdit::ExtraSelection selection; selection.format.setBackground(QColor(m_highlighter->theme().editorColor(KSyntaxHighlighting::Theme::CurrentLine))); selection.format.setProperty(QTextFormat::FullWidthSelection, true); selection.cursor = textCursor(); selection.cursor.clearSelection(); QList extraSelections; extraSelections.append(selection); setExtraSelections(extraSelections); } QTextBlock CodeEditor::blockAtPosition(int y) const { auto block = firstVisibleBlock(); if (!block.isValid()) return QTextBlock(); int top = blockBoundingGeometry(block).translated(contentOffset()).top(); int bottom = top + blockBoundingRect(block).height(); do { if (top <= y && y <= bottom) return block; block = block.next(); top = bottom; bottom = top + blockBoundingRect(block).height(); } while (block.isValid()); return QTextBlock(); } bool CodeEditor::isFoldable(const QTextBlock &block) const { return m_highlighter->startsFoldingRegion(block); } bool CodeEditor::isFolded(const QTextBlock &block) const { if (!block.isValid()) return false; const auto nextBlock = block.next(); if (!nextBlock.isValid()) return false; return !nextBlock.isVisible(); } void CodeEditor::toggleFold(const QTextBlock &startBlock) { // we also want to fold the last line of the region, therefore the ".next()" const auto endBlock = m_highlighter->findFoldingRegionEnd(startBlock).next(); if (isFolded(startBlock)) { // unfold auto block = startBlock.next(); while (block.isValid() && !block.isVisible()) { block.setVisible(true); block.setLineCount(block.layout()->lineCount()); block = block.next(); } } else { // fold auto block = startBlock.next(); while (block.isValid() && block != endBlock) { block.setVisible(false); block.setLineCount(0); block = block.next(); } } // redraw document document()->markContentsDirty(startBlock.position(), endBlock.position() - startBlock.position() + 1); // update scrollbars emit document()->documentLayout()->documentSizeChanged(document()->documentLayout()->documentSize()); } #include "codeeditor.moc" diff --git a/src/cli/kate-syntax-highlighter.cpp b/src/cli/kate-syntax-highlighter.cpp index 80a15d2..8334dd3 100644 --- a/src/cli/kate-syntax-highlighter.cpp +++ b/src/cli/kate-syntax-highlighter.cpp @@ -1,164 +1,164 @@ /* Copyright (C) 2016 Volker Krause Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "ksyntaxhighlighting_version.h" #include #include #include #include #include #include #include #include #include #include using namespace KSyntaxHighlighting; int main(int argc, char **argv) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationName(QStringLiteral("kate-syntax-highlighter")); QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); QCoreApplication::setOrganizationName(QStringLiteral("KDE")); QCoreApplication::setApplicationVersion(QStringLiteral(SyntaxHighlighting_VERSION_STRING)); QCommandLineParser parser; parser.setApplicationDescription(app.translate("SyntaxHighlightingCLI", "Command line syntax highlighter using Kate syntax definitions.")); parser.addHelpOption(); parser.addVersionOption(); parser.addPositionalArgument(app.translate("SyntaxHighlightingCLI", "source"), app.translate("SyntaxHighlightingCLI", "The source file to highlight.")); QCommandLineOption listDefs(QStringList() << QStringLiteral("l") << QStringLiteral("list"), app.translate("SyntaxHighlightingCLI", "List all available syntax definitions.")); parser.addOption(listDefs); QCommandLineOption listThemes(QStringList() << QStringLiteral("list-themes"), app.translate("SyntaxHighlightingCLI", "List all available themes.")); parser.addOption(listThemes); QCommandLineOption updateDefs(QStringList() << QStringLiteral("u") << QStringLiteral("update"), app.translate("SyntaxHighlightingCLI", "Download new/updated syntax definitions.")); parser.addOption(updateDefs); QCommandLineOption outputName(QStringList() << QStringLiteral("o") << QStringLiteral("output"), app.translate("SyntaxHighlightingCLI", "File to write HTML output to (default: stdout)."), app.translate("SyntaxHighlightingCLI", "output")); parser.addOption(outputName); QCommandLineOption syntaxName(QStringList() << QStringLiteral("s") << QStringLiteral("syntax"), app.translate("SyntaxHighlightingCLI", "Highlight using this syntax definition (default: auto-detect based on input file)."), app.translate("SyntaxHighlightingCLI", "syntax")); parser.addOption(syntaxName); QCommandLineOption themeName(QStringList() << QStringLiteral("t") << QStringLiteral("theme"), app.translate("SyntaxHighlightingCLI", "Color theme to use for highlighting."), app.translate("SyntaxHighlightingCLI", "theme"), QStringLiteral("Default")); parser.addOption(themeName); QCommandLineOption titleOption(QStringList() << QStringLiteral("T") << QStringLiteral("title"), app.translate("SyntaxHighlightingCLI", "Set HTML page's title\n(default: the filename or \"Kate Syntax Highlighter\" if reading from stdin)."), app.translate("SyntaxHighlightingCLI", "title")); parser.addOption(titleOption); QCommandLineOption stdinOption(QStringList() << QStringLiteral("stdin"), app.translate("SyntaxHighlightingCLI", "Read file from stdin. The -s option must also be used.")); parser.addOption(stdinOption); parser.process(app); Repository repo; if (parser.isSet(listDefs)) { - foreach (const auto &def, repo.definitions()) { + for (const auto &def : repo.definitions()) { std::cout << qPrintable(def.name()) << std::endl; } return 0; } if (parser.isSet(listThemes)) { - foreach (const auto &theme, repo.themes()) + for (const auto &theme : repo.themes()) std::cout << qPrintable(theme.name()) << std::endl; return 0; } if (parser.isSet(updateDefs)) { DefinitionDownloader downloader(&repo); QObject::connect(&downloader, &DefinitionDownloader::informationMessage, [](const QString &msg) { std::cout << qPrintable(msg) << std::endl; }); QObject::connect(&downloader, &DefinitionDownloader::done, &app, &QCoreApplication::quit); downloader.start(); return app.exec(); } bool fromFileName = false; QString inFileName; if (parser.positionalArguments().size() == 1) { fromFileName = true; inFileName = parser.positionalArguments().at(0); } Definition def; if (parser.isSet(syntaxName)) { def = repo.definitionForName(parser.value(syntaxName)); if (!def.isValid()) /* see if it's a mimetype instead */ def = repo.definitionForMimeType(parser.value(syntaxName)); } else if (fromFileName) { def = repo.definitionForFileName(inFileName); } else { parser.showHelp(1); } QString title; if (parser.isSet(titleOption)) title = parser.value(titleOption); if (!def.isValid()) { std::cerr << "Unknown syntax." << std::endl; return 1; } HtmlHighlighter highlighter; highlighter.setDefinition(def); if (parser.isSet(outputName)) highlighter.setOutputFile(parser.value(outputName)); else highlighter.setOutputFile(stdout); highlighter.setTheme(repo.theme(parser.value(themeName))); if (fromFileName) { highlighter.highlightFile(inFileName, title); } else if (parser.isSet(stdinOption)) { QFile inFile; inFile.open(stdin, QIODevice::ReadOnly); highlighter.highlightData(&inFile, title); } else { parser.showHelp(1); } return 0; } diff --git a/src/indexer/katehighlightingindexer.cpp b/src/indexer/katehighlightingindexer.cpp index a32a757..757009e 100644 --- a/src/indexer/katehighlightingindexer.cpp +++ b/src/indexer/katehighlightingindexer.cpp @@ -1,672 +1,672 @@ /* Copyright (C) 2014 Christoph Cullmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include #include #include #include #include #include #include #include #include #ifdef QT_XMLPATTERNS_LIB #include #include #endif namespace { QStringList readListing(const QString &fileName) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { return QStringList(); } QXmlStreamReader xml(&file); QStringList listing; while (!xml.atEnd()) { xml.readNext(); // add only .xml files, no .json or stuff if (xml.isCharacters() && xml.text().toString().contains(QLatin1String(".xml"))) { listing.append(xml.text().toString()); } } if (xml.hasError()) { qWarning() << "XML error while reading" << fileName << " - " << qPrintable(xml.errorString()) << "@ offset" << xml.characterOffset(); } return listing; } /** * check if the "extensions" attribute have valid wildcards * @param extensions extensions string to check * @return valid? */ bool checkExtensions(QString extensions) { // get list of extensions const QStringList extensionParts = extensions.split(QLatin1Char(';'), QString::SkipEmptyParts); // ok if empty if (extensionParts.isEmpty()) { return true; } // check that only valid wildcard things are inside the parts for (const auto& extension : extensionParts) { for (const auto c : extension) { // eat normal things if (c.isDigit() || c.isLetter()) { continue; } // allow some special characters if (c == QLatin1Char('.') || c == QLatin1Char('-') || c == QLatin1Char('_') || c == QLatin1Char('+')) { continue; } // only allowed wildcard things: '?' and '*' if (c == QLatin1Char('?') || c == QLatin1Char('*')) { continue; } qWarning() << "invalid character" << c << " seen in extensions wildcard"; return false; } } // all checks passed return true; } //! Check that a regular expression in a RegExpr rule: //! - is not empty //! - isValid() //! - character ranges such as [A-Z] are valid and not accidentally e.g. [A-z]. bool checkRegularExpression(const QString &hlFilename, QXmlStreamReader &xml) { if (xml.name() == QLatin1String("RegExpr") || xml.name() == QLatin1String("emptyLine")) { // get right attribute const QString string (xml.attributes().value((xml.name() == QLatin1String("RegExpr")) ? QLatin1String("String") : QLatin1String("regexpr")).toString()); // validate regexp const QRegularExpression regexp (string); if (!regexp.isValid()) { qWarning() << hlFilename << "line" << xml.lineNumber() << "broken regex:" << string << "problem:" << regexp.errorString() << "at offset" << regexp.patternErrorOffset(); return false; } else if (string.isEmpty()) { qWarning() << hlFilename << "line" << xml.lineNumber() << "empty regex not allowed."; return false; } // catch possible case typos: [A-z] or [a-Z] const int azOffset = std::max(string.indexOf(QStringLiteral("A-z")), string.indexOf(QStringLiteral("a-Z"))); if (azOffset >= 0) { qWarning() << hlFilename << "line" << xml.lineNumber() << "broken regex:" << string << "problem: [a-Z] or [A-z] at offset" << azOffset; return false; } } return true; } //! Check that keyword list items do not have trailing or leading spaces, //! e.g.: keyword bool checkItemsTrimmed(const QString &hlFilename, QXmlStreamReader &xml) { if (xml.name() == QLatin1String("item")) { const QString keyword = xml.readElementText(); if (keyword != keyword.trimmed()) { qWarning() << hlFilename << "line" << xml.lineNumber() << "keyword with leading/trailing spaces:" << keyword; return false; } } return true; } //! Checks that DetectChar and Detect2Chars really only have one char //! in the attributes 'char' and 'char1'. bool checkSingleChars(const QString &hlFilename, QXmlStreamReader &xml) { const bool testChar1 = xml.name() == QLatin1String("Detect2Chars"); const bool testChar = testChar1 || xml.name() == QLatin1String("DetectChar"); if (testChar) { const QString c = xml.attributes().value(QLatin1String("char")).toString(); if (c.size() != 1) { qWarning() << hlFilename << "line" << xml.lineNumber() << "'char' must contain exactly one char:" << c; } } if (testChar1) { const QString c = xml.attributes().value(QLatin1String("char1")).toString(); if (c.size() != 1) { qWarning() << hlFilename << "line" << xml.lineNumber() << "'char1' must contain exactly one char:" << c; } } return true; } //! Search for rules with lookAhead="true" and context="#stay". //! This would cause an infinite loop. bool checkLookAhead(const QString &hlFilename, QXmlStreamReader &xml) { if (xml.attributes().hasAttribute(QStringLiteral("lookAhead"))) { auto lookAhead = xml.attributes().value(QStringLiteral("lookAhead")); if (lookAhead == QStringLiteral("true")) { auto context = xml.attributes().value(QStringLiteral("context")); if (context == QStringLiteral("#stay")) { qWarning() << hlFilename << "line" << xml.lineNumber() << "Infinite loop: lookAhead with context #stay"; return false; } } } return true; } /** * Helper class to search for non-existing keyword include. */ class KeywordIncludeChecker { public: void processElement(const QString &hlFilename, const QString &hlName, QXmlStreamReader &xml) { if (xml.name() == QLatin1String("list")) { auto &keywords = m_keywordMap[hlName]; keywords.filename = hlFilename; auto name = xml.attributes().value(QLatin1String("name")).toString(); m_currentIncludes = &keywords.includes[name]; } else if (xml.name() == QLatin1String("include")) { if (!m_currentIncludes) { qWarning() << hlFilename << "line" << xml.lineNumber() << " tag ouside "; m_success = false; } else { m_currentIncludes->push_back({xml.lineNumber(), xml.readElementText()}); } } } bool check() const { bool success = m_success; for (auto &keywords : m_keywordMap) { QMapIterator> includes(keywords.includes); while (includes.hasNext()) { includes.next(); for (auto &include : includes.value()) { bool containsKeywordName = true; int const idx = include.name.indexOf(QStringLiteral("##")); if (idx == -1) { auto &keywordName = includes.key(); containsKeywordName = keywords.includes.contains(keywordName); } else { auto defName = include.name.mid(idx + 2); auto listName = include.name.left(idx); auto it = m_keywordMap.find(defName); if (it == m_keywordMap.end()) { qWarning() << keywords.filename << "line" << include.line << "unknown definition in" << include.name; success = false; } else { containsKeywordName = it->includes.contains(listName); } } if (!containsKeywordName) { qWarning() << keywords.filename << "line" << include.line << "unknown keyword name in" << include.name; success = false; } } } } return success; } private: struct Keywords { QString filename; struct Include { qint64 line; QString name; }; QMap> includes; }; QHash m_keywordMap; QVector *m_currentIncludes = nullptr; bool m_success = true; }; /** * Helper class to search for non-existing or unreferenced keyword lists. */ class KeywordChecker { public: KeywordChecker(const QString &filename) : m_filename(filename) {} void processElement(QXmlStreamReader &xml) { if (xml.name() == QLatin1String("list")) { const QString name = xml.attributes().value(QLatin1String("name")).toString(); if (m_existingNames.contains(name)) { qWarning() << m_filename << "list duplicate:" << name; } m_existingNames.insert(name); } else if (xml.name() == QLatin1String("keyword")) { const QString context = xml.attributes().value(QLatin1String("String")).toString(); if (!context.isEmpty()) m_usedNames.insert(context); } } bool check() const { bool success = true; const auto invalidNames = m_usedNames - m_existingNames; if (!invalidNames.isEmpty()) { qWarning() << m_filename << "Reference of non-existing keyword list:" << invalidNames; success = false; } const auto unusedNames = m_existingNames - m_usedNames; if (!unusedNames.isEmpty()) { qWarning() << m_filename << "Unused keyword lists:" << unusedNames; } return success; } private: QString m_filename; QSet m_usedNames; QSet m_existingNames; }; /** * Helper class to search for non-existing contexts */ class ContextChecker { public: void processElement(const QString &hlFilename, const QString &hlName, QXmlStreamReader &xml) { if (xml.name() == QLatin1String("context")) { auto & language = m_contextMap[hlName]; language.hlFilename = hlFilename; const QString name = xml.attributes().value(QLatin1String("name")).toString(); if (language.isFirstContext) { language.isFirstContext = false; language.usedContextNames.insert(name); } if (language.existingContextNames.contains(name)) { qWarning() << hlFilename << "Duplicate context:" << name; } else { language.existingContextNames.insert(name); } if (xml.attributes().value(QLatin1String("fallthroughContext")).toString() == QLatin1String("#stay")) { qWarning() << hlFilename << "possible infinite loop due to fallthroughContext=\"#stay\" in context " << name; } processContext(hlName, xml.attributes().value(QLatin1String("lineEndContext")).toString()); processContext(hlName, xml.attributes().value(QLatin1String("lineEmptyContext")).toString()); processContext(hlName, xml.attributes().value(QLatin1String("fallthroughContext")).toString()); } else { if (xml.attributes().hasAttribute(QLatin1String("context"))) { const QString context = xml.attributes().value(QLatin1String("context")).toString(); if (context.isEmpty()) { qWarning() << hlFilename << "Missing context name in line" << xml.lineNumber(); } else { processContext(hlName, context); } } } } bool check() const { bool success = true; for (auto &language : m_contextMap) { const auto invalidContextNames = language.usedContextNames - language.existingContextNames; if (!invalidContextNames.isEmpty()) { qWarning() << language.hlFilename << "Reference of non-existing contexts:" << invalidContextNames; success = false; } const auto unusedNames = language.existingContextNames - language.usedContextNames; if (!unusedNames.isEmpty()) { qWarning() << language.hlFilename << "Unused contexts:" << unusedNames; } } return success; } private: //! Extract the referenced context name and language. //! Some input / output examples are: //! - "#stay" -> "" //! - "#pop" -> "" //! - "Comment" -> "Comment" //! - "#pop!Comment" -> "Comment" //! - "##ISO C++" -> "" //! - "Comment##ISO C++"-> "Comment" in ISO C++ void processContext(const QString &language, QString context) { if (context.isEmpty()) return; // filter out #stay and #pop static QRegularExpression stayPop(QStringLiteral("^(#stay|#pop)+")); context.remove(stayPop); // handle cross-language context references if (context.contains(QStringLiteral("##"))) { const QStringList list = context.split(QStringLiteral("##"), QString::SkipEmptyParts); if (list.size() == 1) { // nothing to do, other language is included: e.g. ##Doxygen } else if (list.size() == 2) { // specific context of other language, e.g. Comment##ISO C++ m_contextMap[list[1]].usedContextNames.insert(list[0]); } return; } // handle #pop!context" (#pop was already removed above) if (context.startsWith(QLatin1Char('!'))) context.remove(0, 1); if (!context.isEmpty()) m_contextMap[language].usedContextNames.insert(context); } private: class Language { public: // filename on disk or in Qt resource QString hlFilename; // Is true, if the first context in xml file is encountered, and // false in all other cases. This is required, since the first context // is typically not referenced explicitly. So we will simply add the // first context to the usedContextNames list. bool isFirstContext = true; // holds all contexts that were referenced QSet usedContextNames; // holds all existing context names QSet existingContextNames; }; /** * "Language name" to "Language" map. * Example key: "Doxygen" */ QHash m_contextMap; }; /** * Helper class to search for non-existing itemDatas. */ class AttributeChecker { public: AttributeChecker(const QString &filename) : m_filename(filename) {} void processElement(QXmlStreamReader &xml) { if (xml.name() == QLatin1String("itemData")) { const QString name = xml.attributes().value(QLatin1String("name")).toString(); if (!name.isEmpty()) { if (m_existingAttributeNames.contains(name)) { qWarning() << m_filename << "itemData duplicate:" << name; } else { m_existingAttributeNames.insert(name); } } } else if (xml.attributes().hasAttribute(QLatin1String("attribute"))) { const QString name = xml.attributes().value(QLatin1String("attribute")).toString(); if (name.isEmpty()) { qWarning() << m_filename << "specified attribute is empty:" << xml.name(); } else { m_usedAttributeNames.insert(name); } } } bool check() const { bool success = true; const auto invalidNames = m_usedAttributeNames - m_existingAttributeNames; if (!invalidNames.isEmpty()) { qWarning() << m_filename << "Reference of non-existing itemData attributes:" << invalidNames; success = false; } auto unusedNames = m_existingAttributeNames - m_usedAttributeNames; if (!unusedNames.isEmpty()) { qWarning() << m_filename << "Unused itemData:" << unusedNames; } return success; } private: QString m_filename; QSet m_usedAttributeNames; QSet m_existingAttributeNames; }; } int main(int argc, char *argv[]) { // get app instance QCoreApplication app(argc, argv); // ensure enough arguments are passed if (app.arguments().size() < 3) return 1; #ifdef QT_XMLPATTERNS_LIB // open schema QXmlSchema schema; if (!schema.load(QUrl::fromLocalFile(app.arguments().at(2)))) return 2; #endif const QString hlFilenamesListing = app.arguments().value(3); if (hlFilenamesListing.isEmpty()) { return 1; } QStringList hlFilenames = readListing(hlFilenamesListing); if (hlFilenames.isEmpty()) { qWarning("Failed to read %s", qPrintable(hlFilenamesListing)); return 3; } // text attributes const QStringList textAttributes = QStringList() << QStringLiteral("name") << QStringLiteral("section") << QStringLiteral("mimetype") << QStringLiteral("extensions") << QStringLiteral("style") << QStringLiteral("author") << QStringLiteral("license") << QStringLiteral("indenter"); // index all given highlightings ContextChecker contextChecker; KeywordIncludeChecker keywordIncludeChecker; QVariantMap hls; int anyError = 0; - foreach (const QString &hlFilename, hlFilenames) { + for (const QString &hlFilename : qAsConst(hlFilenames)) { QFile hlFile(hlFilename); if (!hlFile.open(QIODevice::ReadOnly)) { qWarning ("Failed to open %s", qPrintable(hlFilename)); anyError = 3; continue; } #ifdef QT_XMLPATTERNS_LIB // validate against schema QXmlSchemaValidator validator(schema); if (!validator.validate(&hlFile, QUrl::fromLocalFile(hlFile.fileName()))) { anyError = 4; continue; } #endif // read the needed attributes from toplevel language tag hlFile.reset(); QXmlStreamReader xml(&hlFile); if (xml.readNextStartElement()) { if (xml.name() != QLatin1String("language")) { anyError = 5; continue; } } else { anyError = 6; continue; } // map to store hl info QVariantMap hl; // transfer text attributes Q_FOREACH (const QString &attribute, textAttributes) { hl[attribute] = xml.attributes().value(attribute).toString(); } // check if extensions have the right format if (!checkExtensions(hl[QStringLiteral("extensions")].toString())) { qWarning() << hlFilename << "'extensions' wildcards invalid:" << hl[QStringLiteral("extensions")].toString(); anyError = 23; } // numerical attributes hl[QStringLiteral("version")] = xml.attributes().value(QLatin1String("version")).toInt(); hl[QStringLiteral("priority")] = xml.attributes().value(QLatin1String("priority")).toInt(); // add boolean one const QString hidden = xml.attributes().value(QLatin1String("hidden")).toString(); hl[QStringLiteral("hidden")] = (hidden == QLatin1String("true") || hidden == QLatin1String("1")); // remember hl hls[QFileInfo(hlFile).fileName()] = hl; AttributeChecker attributeChecker(hlFilename); KeywordChecker keywordChecker(hlFilename); const QString hlName = hl[QStringLiteral("name")].toString(); // scan for broken regex or keywords with spaces while (!xml.atEnd()) { xml.readNext(); if (!xml.isStartElement()) { continue; } // search for used/existing contexts if applicable contextChecker.processElement(hlFilename, hlName, xml); // search for existing keyword includes keywordIncludeChecker.processElement(hlFilename, hlName, xml); // search for used/existing attributes if applicable attributeChecker.processElement(xml); // search for used/existing keyword lists if applicable keywordChecker.processElement(xml); // scan for bad regex if (!checkRegularExpression(hlFilename, xml)) { anyError = 7; continue; } // scan for bogus lala spaces if (!checkItemsTrimmed(hlFilename, xml)) { anyError = 8; continue; } // check single chars in DetectChar and Detect2Chars if (!checkSingleChars(hlFilename, xml)) { anyError = 8; continue; } // scan for lookAhead="true" with context="#stay" if (!checkLookAhead(hlFilename, xml)) { anyError = 7; continue; } } if (!attributeChecker.check()) { anyError = 7; } if (!keywordChecker.check()) { anyError = 7; } } if (!contextChecker.check()) anyError = 7; if (!keywordIncludeChecker.check()) anyError = 7; // bail out if any problem was seen if (anyError) return anyError; // create outfile, after all has worked! QFile outFile(app.arguments().at(1)); if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) return 9; // write out json outFile.write(QJsonDocument::fromVariant(QVariant(hls)).toBinaryData()); // be done return 0; } diff --git a/src/lib/definition.cpp b/src/lib/definition.cpp index 114d155..d0296ec 100644 --- a/src/lib/definition.cpp +++ b/src/lib/definition.cpp @@ -1,833 +1,833 @@ /* Copyright (C) 2016 Volker Krause Copyright (C) 2018 Dominik Haumann Copyright (C) 2018 Christoph Cullmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "definition.h" #include "definition_p.h" #include "definitionref_p.h" #include "context_p.h" #include "format.h" #include "format_p.h" #include "repository.h" #include "repository_p.h" #include "rule_p.h" #include "ksyntaxhighlighting_logging.h" #include "ksyntaxhighlighting_version.h" #include "xml_p.h" #include #include #include #include #include #include #include #include #include using namespace KSyntaxHighlighting; DefinitionData::DefinitionData() : wordDelimiters(QStringLiteral("\t !%&()*+,-./:;<=>?[\\]^{|}~")) // must be sorted! , wordWrapDelimiters(wordDelimiters) { } DefinitionData::~DefinitionData() { qDeleteAll(contexts); } DefinitionData* DefinitionData::get(const Definition &def) { return def.d.get(); } Definition::Definition() : d(new DefinitionData) { } Definition::Definition(const Definition &other) : d(other.d) { d->q = *this; } Definition::Definition(const std::shared_ptr &dd) : d(dd) { } Definition::~Definition() { } Definition& Definition::operator=(const Definition &rhs) { d = rhs.d; return *this; } bool Definition::operator==(const Definition &other) const { return d->fileName == other.d->fileName; } bool Definition::operator!=(const Definition& other) const { return d->fileName != other.d->fileName; } bool Definition::isValid() const { return d->repo && !d->fileName.isEmpty() && !d->name.isEmpty(); } QString Definition::filePath() const { return d->fileName; } QString Definition::name() const { return d->name; } QString Definition::translatedName() const { return QCoreApplication::instance()->translate("Language", d->name.toUtf8().constData()); } QString Definition::section() const { return d->section; } QString Definition::translatedSection() const { return QCoreApplication::instance()->translate("Language Section", d->section.toUtf8().constData()); } QVector Definition::mimeTypes() const { return d->mimetypes; } QVector Definition::extensions() const { return d->extensions; } int Definition::version() const { return d->version; } int Definition::priority() const { return d->priority; } bool Definition::isHidden() const { return d->hidden; } QString Definition::style() const { return d->style; } QString Definition::indenter() const { return d->indenter; } QString Definition::author() const { return d->author; } QString Definition::license() const { return d->license; } bool Definition::isWordDelimiter(QChar c) const { d->load(); return d->isWordDelimiter(c); } bool Definition::isWordWrapDelimiter(QChar c) const { d->load(); return std::binary_search(d->wordWrapDelimiters.constBegin(), d->wordWrapDelimiters.constEnd(), c); } bool Definition::foldingEnabled() const { d->load(); if (d->hasFoldingRegions || indentationBasedFoldingEnabled()) { return true; } // check included definitions const auto defs = includedDefinitions(); for (const auto &def : defs) { if (def.foldingEnabled()) { d->hasFoldingRegions = true; break; } } return d->hasFoldingRegions; } bool Definition::indentationBasedFoldingEnabled() const { d->load(); return d->indentationBasedFolding; } QStringList Definition::foldingIgnoreList() const { d->load(); return d->foldingIgnoreList; } QStringList Definition::keywordLists() const { d->load(DefinitionData::OnlyKeywords(true)); return d->keywordLists.keys(); } QStringList Definition::keywordList(const QString& name) const { d->load(DefinitionData::OnlyKeywords(true)); const auto list = d->keywordList(name); return list ? list->keywords() : QStringList(); } QVector Definition::formats() const { d->load(); // sort formats so that the order matches the order of the itemDatas in the xml files. auto formatList = QVector::fromList(d->formats.values()); std::sort(formatList.begin(), formatList.end(), [](const KSyntaxHighlighting::Format & lhs, const KSyntaxHighlighting::Format & rhs){ return lhs.id() < rhs.id(); }); return formatList; } QVector Definition::includedDefinitions() const { d->load(); // init worklist and result used as guard with this definition QVector queue{*this}; QVector definitions{*this}; while (!queue.isEmpty()) { // Iterate all context rules to find associated Definitions. This will // automatically catch other Definitions referenced with IncludeRuldes or ContextSwitch. const auto definition = queue.takeLast(); for (const auto & context : qAsConst(definition.d->contexts)) { // handle context switch attributes of this context itself for (const auto switchContext : {context->lineEndContext().context(), context->lineEmptyContext().context(), context->fallthroughContext().context()}) { if (switchContext) { if (!definitions.contains(switchContext->definition())) { queue.push_back(switchContext->definition()); definitions.push_back(switchContext->definition()); } } } // handle the embedded rules for (const auto &rule : context->rules()) { // handle include rules like inclusion if (!definitions.contains(rule->definition())) { queue.push_back(rule->definition()); definitions.push_back(rule->definition()); } // handle context switch context inclusion if (auto switchContext = rule->context().context()) { if (!definitions.contains(switchContext->definition())) { queue.push_back(switchContext->definition()); definitions.push_back(switchContext->definition()); } } } } } // remove the 1st entry, since it is this Definition definitions.pop_front(); return definitions; } QString Definition::singleLineCommentMarker() const { d->load(); return d->singleLineCommentMarker; } CommentPosition Definition::singleLineCommentPosition() const { d->load(); return d->singleLineCommentPosition; } QPair Definition::multiLineCommentMarker() const { d->load(); return { d->multiLineCommentStartMarker, d->multiLineCommentEndMarker }; } QVector> Definition::characterEncodings() const { d->load(); return d->characterEncodings; } Context* DefinitionData::initialContext() const { Q_ASSERT(!contexts.isEmpty()); return contexts.first(); } Context* DefinitionData::contextByName(const QString& name) const { - foreach (auto context, contexts) { + for (const auto context : contexts) { if (context->name() == name) return context; } return nullptr; } KeywordList *DefinitionData::keywordList(const QString& name) { auto it = keywordLists.find(name); return (it == keywordLists.end()) ? nullptr : &it.value(); } bool DefinitionData::isWordDelimiter(QChar c) const { return std::binary_search(wordDelimiters.constBegin(), wordDelimiters.constEnd(), c); } Format DefinitionData::formatByName(const QString& name) const { const auto it = formats.constFind(name); if (it != formats.constEnd()) return it.value(); return Format(); } bool DefinitionData::isLoaded() const { return !contexts.isEmpty(); } bool DefinitionData::load(OnlyKeywords onlyKeywords) { if (fileName.isEmpty()) return false; if (isLoaded()) return true; if (bool(onlyKeywords) && keywordIsLoaded) return true; QFile file(fileName); if (!file.open(QFile::ReadOnly)) return false; QXmlStreamReader reader(&file); while (!reader.atEnd()) { const auto token = reader.readNext(); if (token != QXmlStreamReader::StartElement) continue; if (reader.name() == QLatin1String("highlighting")) { loadHighlighting(reader, onlyKeywords); if (bool(onlyKeywords)) { return true; } } else if (reader.name() == QLatin1String("general")) loadGeneral(reader); } for (auto it = keywordLists.begin(); it != keywordLists.end(); ++it) { it->setCaseSensitivity(caseSensitive); } - foreach (auto context, contexts) { + for (const auto context : qAsConst(contexts)) { context->resolveContexts(); context->resolveIncludes(); context->resolveAttributeFormat(); } Q_ASSERT(std::is_sorted(wordDelimiters.constBegin(), wordDelimiters.constEnd())); return true; } void DefinitionData::clear() { // keep only name and repo, so we can re-lookup to make references persist over repo reloads keywordLists.clear(); qDeleteAll(contexts); contexts.clear(); formats.clear(); fileName.clear(); section.clear(); style.clear(); indenter.clear(); author.clear(); license.clear(); mimetypes.clear(); extensions.clear(); wordDelimiters = QStringLiteral("\t !%&()*+,-./:;<=>?[\\]^{|}~"); // must be sorted! wordWrapDelimiters = wordDelimiters; caseSensitive = Qt::CaseSensitive; version = 0.0f; priority = 0; hidden = false; } bool DefinitionData::loadMetaData(const QString& definitionFileName) { fileName = definitionFileName; QFile file(definitionFileName); if (!file.open(QFile::ReadOnly)) return false; QXmlStreamReader reader(&file); while (!reader.atEnd()) { const auto token = reader.readNext(); if (token != QXmlStreamReader::StartElement) continue; if (reader.name() == QLatin1String("language")) { return loadLanguage(reader); } } return false; } bool DefinitionData::loadMetaData(const QString &file, const QJsonObject &obj) { name = obj.value(QLatin1String("name")).toString(); section = obj.value(QLatin1String("section")).toString(); version = obj.value(QLatin1String("version")).toInt(); priority = obj.value(QLatin1String("priority")).toInt(); style = obj.value(QLatin1String("style")).toString(); author = obj.value(QLatin1String("author")).toString(); license = obj.value(QLatin1String("license")).toString(); indenter = obj.value(QLatin1String("indenter")).toString(); hidden = obj.value(QLatin1String("hidden")).toBool(); fileName = file; const auto exts = obj.value(QLatin1String("extensions")).toString(); - foreach (const auto &ext, exts.split(QLatin1Char(';'), QString::SkipEmptyParts)) + for (const auto &ext : exts.split(QLatin1Char(';'), QString::SkipEmptyParts)) extensions.push_back(ext); const auto mts = obj.value(QLatin1String("mimetype")).toString(); - foreach (const auto &mt, mts.split(QLatin1Char(';'), QString::SkipEmptyParts)) + for (const auto &mt : mts.split(QLatin1Char(';'), QString::SkipEmptyParts)) mimetypes.push_back(mt); return true; } bool DefinitionData::loadLanguage(QXmlStreamReader &reader) { Q_ASSERT(reader.name() == QLatin1String("language")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); if (!checkKateVersion(reader.attributes().value(QStringLiteral("kateversion")))) return false; name = reader.attributes().value(QStringLiteral("name")).toString(); section = reader.attributes().value(QStringLiteral("section")).toString(); // toFloat instead of toInt for backward compatibility with old Kate files version = reader.attributes().value(QStringLiteral("version")).toFloat(); priority = reader.attributes().value(QStringLiteral("priority")).toInt(); hidden = Xml::attrToBool(reader.attributes().value(QStringLiteral("hidden"))); style = reader.attributes().value(QStringLiteral("style")).toString(); indenter = reader.attributes().value(QStringLiteral("indenter")).toString(); author = reader.attributes().value(QStringLiteral("author")).toString(); license = reader.attributes().value(QStringLiteral("license")).toString(); const auto exts = reader.attributes().value(QStringLiteral("extensions")).toString(); - foreach (const auto &ext, exts.split(QLatin1Char(';'), QString::SkipEmptyParts)) + for (const auto &ext : exts.split(QLatin1Char(';'), QString::SkipEmptyParts)) extensions.push_back(ext); const auto mts = reader.attributes().value(QStringLiteral("mimetype")).toString(); - foreach (const auto &mt, mts.split(QLatin1Char(';'), QString::SkipEmptyParts)) + for (const auto &mt : mts.split(QLatin1Char(';'), QString::SkipEmptyParts)) mimetypes.push_back(mt); if (reader.attributes().hasAttribute(QStringLiteral("casesensitive"))) caseSensitive = Xml::attrToBool(reader.attributes().value(QStringLiteral("casesensitive"))) ? Qt::CaseSensitive : Qt::CaseInsensitive; return true; } void DefinitionData::loadHighlighting(QXmlStreamReader& reader, OnlyKeywords onlyKeywords) { Q_ASSERT(reader.name() == QLatin1String("highlighting")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); // skip highlighting reader.readNext(); while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: if (reader.name() == QLatin1String("list")) { if (!keywordIsLoaded) { KeywordList keywords; keywords.load(reader); keywordLists.insert(keywords.name(), keywords); } else { reader.skipCurrentElement(); reader.readNext(); // Skip } } else if (bool(onlyKeywords)) { resolveIncludeKeywords(); return; } else if (reader.name() == QLatin1String("contexts")) { resolveIncludeKeywords(); loadContexts(reader); reader.readNext(); } else if (reader.name() == QLatin1String("itemDatas")) { loadItemData(reader); } else { reader.readNext(); } break; case QXmlStreamReader::EndElement: return; default: reader.readNext(); break; } } } void DefinitionData::resolveIncludeKeywords() { if (keywordIsLoaded) { return; } keywordIsLoaded = true; for (auto it = keywordLists.begin(); it != keywordLists.end(); ++it) { it->resolveIncludeKeywords(*this); } } void DefinitionData::loadContexts(QXmlStreamReader& reader) { Q_ASSERT(reader.name() == QLatin1String("contexts")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: if (reader.name() == QLatin1String("context")) { auto context = new Context; context->setDefinition(q); context->load(reader); contexts.push_back(context); } reader.readNext(); break; case QXmlStreamReader::EndElement: return; default: reader.readNext(); break; } } } void DefinitionData::loadItemData(QXmlStreamReader& reader) { Q_ASSERT(reader.name() == QLatin1String("itemDatas")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: if (reader.name() == QLatin1String("itemData")) { Format f; auto formatData = FormatPrivate::detachAndGet(f); formatData->definition = q; formatData->load(reader); formatData->id = RepositoryPrivate::get(repo)->nextFormatId(); formats.insert(f.name(), f); reader.readNext(); } reader.readNext(); break; case QXmlStreamReader::EndElement: return; default: reader.readNext(); break; } } } void DefinitionData::loadGeneral(QXmlStreamReader& reader) { Q_ASSERT(reader.name() == QLatin1String("general")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); reader.readNext(); // reference counter to count XML child elements, to not return too early int elementRefCounter = 1; while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: ++elementRefCounter; if (reader.name() == QLatin1String("keywords")) { if (reader.attributes().hasAttribute(QStringLiteral("casesensitive"))) caseSensitive = Xml::attrToBool(reader.attributes().value(QStringLiteral("casesensitive"))) ? Qt::CaseSensitive : Qt::CaseInsensitive; // adapt sorted wordDelimiters wordDelimiters += reader.attributes().value(QStringLiteral("additionalDeliminator")); std::sort(wordDelimiters.begin(), wordDelimiters.end()); auto it = std::unique(wordDelimiters.begin(), wordDelimiters.end()); wordDelimiters.truncate(std::distance(wordDelimiters.begin(), it)); - foreach (const auto c, reader.attributes().value(QLatin1String("weakDeliminator"))) + for (const auto c : reader.attributes().value(QLatin1String("weakDeliminator"))) wordDelimiters.remove(c); // adaptWordWrapDelimiters, and sort wordWrapDelimiters = reader.attributes().value(QStringLiteral("wordWrapDeliminator")).toString(); std::sort(wordWrapDelimiters.begin(), wordWrapDelimiters.end()); if (wordWrapDelimiters.isEmpty()) wordWrapDelimiters = wordDelimiters; } else if (reader.name() == QLatin1String("folding")) { if (reader.attributes().hasAttribute(QStringLiteral("indentationsensitive"))) indentationBasedFolding = Xml::attrToBool(reader.attributes().value(QStringLiteral("indentationsensitive"))); } else if (reader.name() == QLatin1String("emptyLines")) { loadFoldingIgnoreList(reader); } else if (reader.name() == QLatin1String("comments")) { loadComments(reader); } else if (reader.name() == QLatin1String("spellchecking")) { loadSpellchecking(reader); } else { reader.skipCurrentElement(); } reader.readNext(); break; case QXmlStreamReader::EndElement: --elementRefCounter; if (elementRefCounter == 0) return; reader.readNext(); break; default: reader.readNext(); break; } } } void DefinitionData::loadComments(QXmlStreamReader &reader) { Q_ASSERT(reader.name() == QLatin1String("comments")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); reader.readNext(); // reference counter to count XML child elements, to not return too early int elementRefCounter = 1; while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: ++elementRefCounter; if (reader.name() == QLatin1String("comment")) { const bool isSingleLine = reader.attributes().value(QStringLiteral("name")) == QStringLiteral("singleLine"); if (isSingleLine) { singleLineCommentMarker = reader.attributes().value(QStringLiteral("start")).toString(); const bool afterWhiteSpace = reader.attributes().value(QStringLiteral("position")).toString() == QStringLiteral("afterwhitespace"); singleLineCommentPosition = afterWhiteSpace ? CommentPosition::AfterWhitespace : CommentPosition::StartOfLine; } else { multiLineCommentStartMarker = reader.attributes().value(QStringLiteral("start")).toString(); multiLineCommentEndMarker = reader.attributes().value(QStringLiteral("end")).toString(); } } reader.readNext(); break; case QXmlStreamReader::EndElement: --elementRefCounter; if (elementRefCounter == 0) return; reader.readNext(); break; default: reader.readNext(); break; } } } void DefinitionData::loadFoldingIgnoreList(QXmlStreamReader& reader) { Q_ASSERT(reader.name() == QLatin1String("emptyLines")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); reader.readNext(); // reference counter to count XML child elements, to not return too early int elementRefCounter = 1; while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: ++elementRefCounter; if (reader.name() == QLatin1String("emptyLine")) { foldingIgnoreList << reader.attributes().value(QStringLiteral("regexpr")).toString(); } reader.readNext(); break; case QXmlStreamReader::EndElement: --elementRefCounter; if (elementRefCounter == 0) return; reader.readNext(); break; default: reader.readNext(); break; } } } void DefinitionData::loadSpellchecking(QXmlStreamReader &reader) { Q_ASSERT(reader.name() == QLatin1String("spellchecking")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); reader.readNext(); // reference counter to count XML child elements, to not return too early int elementRefCounter = 1; while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: ++elementRefCounter; if (reader.name() == QLatin1String("encoding")) { const auto charRef = reader.attributes().value(QStringLiteral("char")); if (!charRef.isEmpty()) { const auto str = reader.attributes().value(QStringLiteral("string")).toString(); characterEncodings.push_back({ charRef[0], str }); } } reader.readNext(); break; case QXmlStreamReader::EndElement: --elementRefCounter; if (elementRefCounter == 0) return; reader.readNext(); break; default: reader.readNext(); break; } } } bool DefinitionData::checkKateVersion(const QStringRef& verStr) { const auto idx = verStr.indexOf(QLatin1Char('.')); if (idx <= 0) { qCWarning(Log) << "Skipping" << fileName << "due to having no valid kateversion attribute:" << verStr; return false; } const auto major = verStr.left(idx).toInt(); const auto minor = verStr.mid(idx + 1).toInt(); if (major > SyntaxHighlighting_VERSION_MAJOR || (major == SyntaxHighlighting_VERSION_MAJOR && minor > SyntaxHighlighting_VERSION_MINOR)) { qCWarning(Log) << "Skipping" << fileName << "due to being too new, version:" << verStr; return false; } return true; } quint16 DefinitionData::foldingRegionId(const QString &foldName) { hasFoldingRegions = true; return RepositoryPrivate::get(repo)->foldingRegionId(name, foldName); } DefinitionRef::DefinitionRef() { } DefinitionRef::DefinitionRef(const Definition &def) : d(def.d) { } DefinitionRef::~DefinitionRef() { } DefinitionRef& DefinitionRef::operator=(const Definition &def) { d = def.d; return *this; } Definition DefinitionRef::definition() const { if (!d.expired()) return Definition(d.lock()); return Definition(); } bool DefinitionRef::operator==(const DefinitionRef &other) const { if (d.expired() != other.d.expired()) { return false; } return d.expired() || d.lock().get() == other.d.lock().get(); } diff --git a/src/lib/repository.cpp b/src/lib/repository.cpp index 6b2fabd..291a3ef 100644 --- a/src/lib/repository.cpp +++ b/src/lib/repository.cpp @@ -1,325 +1,325 @@ /* Copyright (C) 2016 Volker Krause Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "repository.h" #include "repository_p.h" #include "definition.h" #include "definition_p.h" #include "theme.h" #include "themedata_p.h" #include "ksyntaxhighlighting_logging.h" #include "wildcardmatcher_p.h" #include #include #include #include #include #include #ifndef NO_STANDARD_PATHS #include #endif #include using namespace KSyntaxHighlighting; static void initResource() { #ifdef HAS_SYNTAX_RESOURCE Q_INIT_RESOURCE(syntax_data); #endif Q_INIT_RESOURCE(theme_data); } RepositoryPrivate* RepositoryPrivate::get(Repository *repo) { return repo->d.get(); } Repository::Repository() : d(new RepositoryPrivate) { initResource(); d->load(this); } Repository::~Repository() { // reset repo so we can detect in still alive definition instances // that the repo was deleted - foreach (const auto &def, d->m_sortedDefs) + for (const auto &def : qAsConst(d->m_sortedDefs)) DefinitionData::get(def)->repo = nullptr; } Definition Repository::definitionForName(const QString& defName) const { return d->m_defs.value(defName); } static Definition bestCandidate(QVector& candidates) { if (candidates.isEmpty()) return Definition(); std::partial_sort(candidates.begin(), candidates.begin() + 1, candidates.end(), [](const Definition &lhs, const Definition &rhs) { return lhs.priority() > rhs.priority(); }); return candidates.at(0); } Definition Repository::definitionForFileName(const QString& fileName) const { QFileInfo fi(fileName); const auto name = fi.fileName(); QVector candidates; for (auto it = d->m_defs.constBegin(); it != d->m_defs.constEnd(); ++it) { auto def = it.value(); - foreach (const auto &pattern, def.extensions()) { + for (const auto &pattern : def.extensions()) { if (WildcardMatcher::exactMatch(name, pattern)) { candidates.push_back(def); break; } } } return bestCandidate(candidates); } Definition Repository::definitionForMimeType(const QString& mimeType) const { QVector candidates; for (auto it = d->m_defs.constBegin(); it != d->m_defs.constEnd(); ++it) { auto def = it.value(); - foreach (const auto &matchType, def.mimeTypes()) { + for (const auto &matchType : def.mimeTypes()) { if (mimeType == matchType) { candidates.push_back(def); break; } } } return bestCandidate(candidates); } QVector Repository::definitions() const { return d->m_sortedDefs; } QVector Repository::themes() const { return d->m_themes; } Theme Repository::theme(const QString &themeName) const { for (const auto &theme : qAsConst(d->m_themes)) { if (theme.name() == themeName) { return theme; } } return Theme(); } Theme Repository::defaultTheme(Repository::DefaultTheme t) { if (t == DarkTheme) return theme(QLatin1String("Breeze Dark")); return theme(QLatin1String("Default")); } void RepositoryPrivate::load(Repository *repo) { // always add invalid default "None" highlighting addDefinition(Definition()); // do lookup in standard paths, if not disabled #ifndef NO_STANDARD_PATHS - foreach (const auto &dir, QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("org.kde.syntax-highlighting/syntax"), QStandardPaths::LocateDirectory)) + for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("org.kde.syntax-highlighting/syntax"), QStandardPaths::LocateDirectory)) loadSyntaxFolder(repo, dir); // backward compatibility with Kate - foreach (const auto &dir, QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("katepart5/syntax"), QStandardPaths::LocateDirectory)) + for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("katepart5/syntax"), QStandardPaths::LocateDirectory)) loadSyntaxFolder(repo, dir); #endif // default resources are always used loadSyntaxFolder(repo, QStringLiteral(":/org.kde.syntax-highlighting/syntax")); // user given extra paths - foreach (const auto &path, m_customSearchPaths) + for (const auto &path : qAsConst(m_customSearchPaths)) loadSyntaxFolder(repo, path + QStringLiteral("/syntax")); m_sortedDefs.reserve(m_defs.size()); for (auto it = m_defs.constBegin(); it != m_defs.constEnd(); ++it) m_sortedDefs.push_back(it.value()); std::sort(m_sortedDefs.begin(), m_sortedDefs.end(), [](const Definition &left, const Definition &right) { auto comparison = left.translatedSection().compare(right.translatedSection(), Qt::CaseInsensitive); if (comparison == 0) comparison = left.translatedName().compare(right.translatedName(), Qt::CaseInsensitive); return comparison < 0; }); // load themes // do lookup in standard paths, if not disabled #ifndef NO_STANDARD_PATHS - foreach (const auto &dir, QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("org.kde.syntax-highlighting/themes"), QStandardPaths::LocateDirectory)) + for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("org.kde.syntax-highlighting/themes"), QStandardPaths::LocateDirectory)) loadThemeFolder(dir); #endif // default resources are always used loadThemeFolder(QStringLiteral(":/org.kde.syntax-highlighting/themes")); // user given extra paths - foreach (const auto &path, m_customSearchPaths) + for (const auto &path : qAsConst(m_customSearchPaths)) loadThemeFolder(path + QStringLiteral("/themes")); } void RepositoryPrivate::loadSyntaxFolder(Repository *repo, const QString &path) { if (loadSyntaxFolderFromIndex(repo, path)) return; QDirIterator it(path, QStringList() << QLatin1String("*.xml"), QDir::Files); while (it.hasNext()) { Definition def; auto defData = DefinitionData::get(def); defData->repo = repo; if (defData->loadMetaData(it.next())) addDefinition(def); } } bool RepositoryPrivate::loadSyntaxFolderFromIndex(Repository *repo, const QString &path) { QFile indexFile(path + QLatin1String("/index.katesyntax")); if (!indexFile.open(QFile::ReadOnly)) return false; const auto indexDoc(QJsonDocument::fromBinaryData(indexFile.readAll())); const auto index = indexDoc.object(); for (auto it = index.begin(); it != index.end(); ++it) { if (!it.value().isObject()) continue; const auto fileName = QString(path + QLatin1Char('/') + it.key()); const auto defMap = it.value().toObject(); Definition def; auto defData = DefinitionData::get(def); defData->repo = repo; if (defData->loadMetaData(fileName, defMap)) addDefinition(def); } return true; } void RepositoryPrivate::addDefinition(const Definition &def) { const auto it = m_defs.constFind(def.name()); if (it == m_defs.constEnd()) { m_defs.insert(def.name(), def); return; } if (it.value().version() >= def.version()) return; m_defs.insert(def.name(), def); } void RepositoryPrivate::loadThemeFolder(const QString &path) { QDirIterator it(path, QStringList() << QLatin1String("*.theme"), QDir::Files); while (it.hasNext()) { auto themeData = std::unique_ptr(new ThemeData); if (themeData->load(it.next())) addTheme(Theme(themeData.release())); } } static int themeRevision(const Theme &theme) { auto data = ThemeData::get(theme); return data->revision(); } void RepositoryPrivate::addTheme(const Theme &theme) { const auto it = std::lower_bound(m_themes.begin(), m_themes.end(), theme, [](const Theme &lhs, const Theme &rhs) { return lhs.name() < rhs.name(); }); if (it == m_themes.end() || (*it).name() != theme.name()) { m_themes.insert(it, theme); return; } if (themeRevision(*it) < themeRevision(theme)) *it = theme; } quint16 RepositoryPrivate::foldingRegionId(const QString &defName, const QString &foldName) { const auto it = m_foldingRegionIds.constFind(qMakePair(defName, foldName)); if (it != m_foldingRegionIds.constEnd()) return it.value(); m_foldingRegionIds.insert(qMakePair(defName, foldName), ++m_foldingRegionId); return m_foldingRegionId; } quint16 RepositoryPrivate::nextFormatId() { Q_ASSERT(m_formatId < std::numeric_limits::max()); return ++m_formatId; } void Repository::reload() { qCDebug(Log) << "Reloading syntax definitions!"; - foreach (const auto &def, d->m_sortedDefs) + for (const auto &def : qAsConst(d->m_sortedDefs)) DefinitionData::get(def)->clear(); d->m_defs.clear(); d->m_sortedDefs.clear(); d->m_themes.clear(); d->m_foldingRegionId = 0; d->m_foldingRegionIds.clear(); d->m_formatId = 0; d->load(this); } void Repository::addCustomSearchPath(const QString &path) { d->m_customSearchPaths.append(path); reload(); } QVector Repository::customSearchPaths() const { return d->m_customSearchPaths; }