diff --git a/autotests/syntaxrepository_test.cpp b/autotests/syntaxrepository_test.cpp index 64ba779..8501618 100644 --- a/autotests/syntaxrepository_test.cpp +++ b/autotests/syntaxrepository_test.cpp @@ -1,288 +1,288 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #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); } 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()) { QVERIFY(!def.name().isEmpty()); QVERIFY(!def.translatedName().isEmpty()); QVERIFY(!def.section().isEmpty()); QVERIFY(!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(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()); 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) { 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("C++")); QVERIFY(def.isValid()); auto defs = def.includedDefinitions(); const QStringList expectedDefinitionNames = { QStringLiteral("ISO C++"), QStringLiteral("GCCExtensions"), QStringLiteral("Doxygen"), QStringLiteral("Alerts"), QStringLiteral("Modelines") }; QStringList definitionNames; for (auto d : defs) { QVERIFY(d.isValid()); definitionNames.push_back(d.name()); } QCOMPARE(definitionNames, expectedDefinitionNames); } 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 testEmptyDefinition() { Definition def; QVERIFY(!def.isValid()); QVERIFY(def.filePath().isEmpty()); QVERIFY(def.name().isEmpty()); QVERIFY(def.translatedName().isEmpty()); 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.indentationBasedFoldingEnabled()); QVERIFY(def.foldingIgnoreList().isEmpty()); QVERIFY(def.keywordLists().isEmpty()); QVERIFY(def.formats().isEmpty()); QVERIFY(def.includedDefinitions().isEmpty()); for (QChar c : QStringLiteral("\t !%&()*+,-./:;<=>?[\\]^{|}~")) - QVERIFY(def.isDelimiter(c)); + QVERIFY(def.isWordDelimiter(c)); } }; } QTEST_GUILESS_MAIN(KSyntaxHighlighting::RepositoryTest) #include "syntaxrepository_test.moc" diff --git a/src/lib/definition.cpp b/src/lib/definition.cpp index b675f6a..8976d9c 100644 --- a/src/lib/definition.cpp +++ b/src/lib/definition.cpp @@ -1,625 +1,625 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "definition.h" #include "definition_p.h" #include "definitionref_p.h" #include "context_p.h" #include "format.h" #include "format_p.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 #include using namespace KSyntaxHighlighting; DefinitionData::DefinitionData() : delimiters(QStringLiteral("\t !%&()*+,-./:;<=>?[\\]^{|}~")) // must be sorted! { } 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::isDelimiter(QChar c) const +bool Definition::isWordDelimiter(QChar c) const { d->load(); - return d->isDelimiter(c); + return d->isWordDelimiter(c); } bool Definition::indentationBasedFoldingEnabled() const { d->load(); return d->indentationBasedFolding; } QStringList Definition::foldingIgnoreList() const { d->load(); return d->foldingIgnoreList; } QStringList Definition::keywordLists() const { d->load(); return d->keywordLists.keys(); } QStringList Definition::keywordList(const QString& name) const { d->load(); return d->keywordList(name).keywords(); } 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(); QVector definitions; QQueue queue; queue.enqueue(*this); while (!queue.isEmpty()) { const auto definition = queue.dequeue(); definitions.push_back(definition); // Iterate all context rules to find associated Definitions. This will // automatically catch other Definitions referenced with IncludeRuldes. foreach (const auto & context, definition.d->contexts) { foreach (const auto &rule, context->rules()) { if ((!definitions.contains(rule->definition())) && (!queue.contains(rule->definition()))) { queue.enqueue(rule->definition()); } } } } // remove the 1st entry, since it is this Definition definitions.pop_front(); return definitions; } Context* DefinitionData::initialContext() const { Q_ASSERT(!contexts.isEmpty()); return contexts.first(); } Context* DefinitionData::contextByName(const QString& name) const { foreach (auto context, contexts) { if (context->name() == name) return context; } return nullptr; } KeywordList DefinitionData::keywordList(const QString& name) const { return keywordLists.value(name); } -bool DefinitionData::isDelimiter(QChar c) const +bool DefinitionData::isWordDelimiter(QChar c) const { return std::binary_search(delimiters.constBegin(), delimiters.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() { if (fileName.isEmpty()) return false; if (isLoaded()) 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); else if (reader.name() == QLatin1String("general")) loadGeneral(reader); } for (auto it = keywordLists.begin(); it != keywordLists.end(); ++it) (*it).setCaseSensitivity(caseSensitive); foreach (auto context, contexts) { context->resolveContexts(); context->resolveIncludes(); } Q_ASSERT(std::is_sorted(delimiters.constBegin(), delimiters.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(); delimiters = QStringLiteral("\t !%&()*+,-./:;<=>?[\\]^{|}~"); // must be sorted! 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)) extensions.push_back(ext); const auto mts = obj.value(QLatin1String("mimetype")).toString(); foreach (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)) extensions.push_back(ext); const auto mts = reader.attributes().value(QStringLiteral("mimetype")).toString(); foreach (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) { Q_ASSERT(reader.name() == QLatin1String("highlighting")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: if (reader.name() == QLatin1String("list")) { KeywordList keywords; keywords.load(reader); keywordLists.insert(keywords.name(), keywords); } else if (reader.name() == QLatin1String("contexts")) { 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::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::get(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; delimiters += reader.attributes().value(QStringLiteral("additionalDeliminator")); std::sort(delimiters.begin(), delimiters.end()); auto it = std::unique(delimiters.begin(), delimiters.end()); delimiters.truncate(std::distance(delimiters.begin(), it)); foreach (const auto c, reader.attributes().value(QLatin1String("weakDeliminator"))) delimiters.remove(c); } 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 { reader.skipCurrentElement(); } 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; } } } 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) { 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(); } diff --git a/src/lib/definition.h b/src/lib/definition.h index 7a1e8f8..e2bec24 100644 --- a/src/lib/definition.h +++ b/src/lib/definition.h @@ -1,241 +1,241 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KSYNTAXHIGHLIGHTING_DEFINITION_H #define KSYNTAXHIGHLIGHTING_DEFINITION_H #include "ksyntaxhighlighting_export.h" #include #include class QChar; class QString; class QStringList; template class QVector; namespace KSyntaxHighlighting { class Context; class Format; class KeywordList; class DefinitionData; /** * Represents a syntax definition. * * @section def_intro Introduction to Definitions * * A Definition is the short term for a syntax highlighting definition. It * typically is defined in terms of an XML syntax highlighting file, containing * all information about a particular syntax highlighting. This includes the * highlighting of keywords, information about code folding regions, and * indentation preferences. * * @section def_info General Information * * Each Definition contains a non-translated unique name() and a section(). * In addition, for putting this information e.g. into menus, the functions * translatedName() and translatedSection() are provided. However, if isHidden() * returns @e true, the Definition should not be visible in the UI. The location * of the Definition can be obtained through filePath(), which either is the * location on disk or a path to a compiled-in Qt resource. * * The supported files of a Definition are defined by the list of extensions(), * and additionally by the list of mimeTypes(). Note, that extensions() returns * wildcards that need to be matched against the filename of the file that * requires highlighting. If multiple Definition%s match the file, then the one * with higher priority() wins. * * @see Repository * @since 5.28 */ class KSYNTAXHIGHLIGHTING_EXPORT Definition { public: /** * Default constructor, creating an empty (invalid) Definition instance. * isValid() for this instance returns @e false. * * Use the Repository instead to obtain valid instances. */ Definition(); /** * Copy constructor. * Both this definition as well as @p other share the Definition data. */ Definition(const Definition &other); /** * Destructor. */ ~Definition(); /** * Assignment operator. * Both this definition as well as @p rhs share the Definition data. */ Definition& operator=(const Definition &rhs); /** * Checks two definitions for equality. */ bool operator==(const Definition &other) const; /** * Checks two definitions for inequality. */ bool operator!=(const Definition &other) const; /** Checks whether this object refers to a valid syntax definition. */ bool isValid() const; /** Returns the full path to the definition XML file containing * the syntax definition. Note that this can be a path to QRC content. */ QString filePath() const; /** Name of the syntax. * Used for internal references, prefer translatedName() for display. */ QString name() const; /** Translated name for display. */ QString translatedName() const; /** The group this syntax definition belongs to. * For display, consider translatedSection(). */ QString section() const; /** Translated group name for display. */ QString translatedSection() const; /** Mime types associated with this syntax definition. */ QVector mimeTypes() const; /** * File extensions associated with this syntax definition. * The returned list contains wildcards. */ QVector extensions() const; /** Returns the definition version. */ int version() const; /** * Returns the definition priority. * A Definition with higher priority wins over Definitions with lower priorities. */ int priority() const; /** Returns @c true if this is an internal definition that should not be * displayed to the user. */ bool isHidden() const; /** Generalized language style, used for indentation. */ QString style() const; /** Indentation style to be used for this syntax. */ QString indenter() const; /** Name and email of the author of this syntax definition. */ QString author() const; /** License of this syntax definition. */ QString license() const; /** * Returns whether the character @p c is a word delimiter. * A delimiter defines whether a characters is a word boundary. Internally, * delimiters are used for matching keyword lists. As example, typically the * dot '.' is a word delimiter. However, if you have a keyword in a keyword * list that contains a dot, you have to add the dot to the * @e weakDeliminator attribute of the @e general section in your * highlighting definition. Similarly, sometimes additional delimiters are * required, which can be specified in @e additionalDeliminator. * * Checking whether a character is a delimiter is useful for instance if * text is selected with double click. Typically, the whole word should be * selected in this case. Similarly to the example above, the dot '.' * usually acts as word delimiter. However, using this function you can * implement text selection in such a way that keyword lists are correctly * selected. * * @note By default, the list of delimiters contains the following * characters: \\t !%&()*+,-./:;<=>?[\\]^{|}~ * * @since 5.50 */ - bool isDelimiter(QChar c) const; + bool isWordDelimiter(QChar c) const; /** * Returns whether indentation-based folding is enabled. * An example for indentation-based folding is Python. * When indentation-based folding is enabled, make sure to also check * foldingIgnoreList() for lines that should be treated as empty. * * @see foldingIgnoreList(), State::indentationBasedFoldingEnabled() */ bool indentationBasedFoldingEnabled() const; /** * If indentationBasedFoldingEnabled() returns @c true, this function returns * a list of regular expressions that represent empty lines. That is, all * lines matching entirely one of the regular expressions should be treated * as empty lines when calculating the indentation-based folding ranges. * * @note This list is only of relevance, if indentationBasedFoldingEnabled() * returns @c true. * * @see indentationBasedFoldingEnabled() */ QStringList foldingIgnoreList() const; /** * Returns the section names of keywords. * @since 5.49 * @see keywordList() */ QStringList keywordLists() const; /** * Returns the list of keywords for the keyword list @p name. * @since 5.49 * @see keywordLists() */ QStringList keywordList(const QString& name) const; /** * Returns a list of all Format items used by this definition. * The order of the Format items equals the order of the itemDatas in the xml file. * @since 5.49 */ QVector formats() const; /** * Returns a list of Definitions that are referenced with the IncludeRules rule. * The returned list does not include this Definition. In case no other * Definitions are referenced via IncludeRules, the returned list is empty. * * @since 5.49 */ QVector includedDefinitions() const; private: friend class DefinitionData; friend class DefinitionRef; explicit Definition(const std::shared_ptr &dd); std::shared_ptr d; }; } Q_DECLARE_TYPEINFO(KSyntaxHighlighting::Definition, Q_MOVABLE_TYPE); #endif diff --git a/src/lib/definition_p.h b/src/lib/definition_p.h index c61b0da..2c67f60 100644 --- a/src/lib/definition_p.h +++ b/src/lib/definition_p.h @@ -1,96 +1,96 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KSYNTAXHIGHLIGHTING_DEFINITION_P_H #define KSYNTAXHIGHLIGHTING_DEFINITION_P_H #include "definitionref_p.h" #include #include #include QT_BEGIN_NAMESPACE class QXmlStreamReader; class QJsonObject; QT_END_NAMESPACE namespace KSyntaxHighlighting { class Definition; class Repository; class DefinitionData { public: DefinitionData(); ~DefinitionData(); static DefinitionData* get(const Definition &def); bool isLoaded() const; bool loadMetaData(const QString &definitionFileName); bool loadMetaData(const QString &fileName, const QJsonObject &obj); void clear(); bool load(); bool loadLanguage(QXmlStreamReader &reader); void loadHighlighting(QXmlStreamReader &reader); void loadContexts(QXmlStreamReader &reader); void loadItemData(QXmlStreamReader &reader); void loadGeneral(QXmlStreamReader &reader); void loadFoldingIgnoreList(QXmlStreamReader &reader); bool checkKateVersion(const QStringRef &verStr); KeywordList keywordList(const QString &name) const; - bool isDelimiter(QChar c) const; + bool isWordDelimiter(QChar c) const; Context* initialContext() const; Context* contextByName(const QString &name) const; Format formatByName(const QString &name) const; quint16 foldingRegionId(const QString &foldName); DefinitionRef q; Repository *repo = nullptr; QHash keywordLists; QVector contexts; QHash formats; QString delimiters; bool indentationBasedFolding = false; QStringList foldingIgnoreList; QString fileName; QString name; QString section; QString style; QString indenter; QString author; QString license; QVector mimetypes; QVector extensions; Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive; int version = 0; int priority = 0; bool hidden = false; }; } #endif diff --git a/src/lib/rule.cpp b/src/lib/rule.cpp index 6d1f843..8cebcf1 100644 --- a/src/lib/rule.cpp +++ b/src/lib/rule.cpp @@ -1,682 +1,682 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "rule_p.h" #include "definition_p.h" #include "ksyntaxhighlighting_logging.h" #include "xml_p.h" #include #include using namespace KSyntaxHighlighting; static bool isOctalChar(QChar c) { return c.isNumber() && c != QLatin1Char('9') && c != QLatin1Char('8'); } static bool isHexChar(QChar c) { return c.isNumber() || c == QLatin1Char('a') || c == QLatin1Char('A') || c == QLatin1Char('b') || c == QLatin1Char('B') || c == QLatin1Char('c') || c == QLatin1Char('C') || c == QLatin1Char('d') || c == QLatin1Char('D') || c == QLatin1Char('e') || c == QLatin1Char('E') || c == QLatin1Char('f') || c == QLatin1Char('F'); } static int matchEscapedChar(const QString &text, int offset) { if (text.at(offset) != QLatin1Char('\\') || text.size() < offset + 2) return offset; const auto c = text.at(offset + 1); static const auto controlChars = QStringLiteral("abefnrtv\"'?\\"); if (controlChars.contains(c)) return offset + 2; if (c == QLatin1Char('x')) { // hex encoded character auto newOffset = offset + 2; for (int i = 0; i < 2 && newOffset + i < text.size(); ++i, ++newOffset) { if (!isHexChar(text.at(newOffset))) break; } if (newOffset == offset + 2) return offset; return newOffset; } if (isOctalChar(c)) { // octal encoding auto newOffset = offset + 2; for (int i = 0; i < 2 && newOffset + i < text.size(); ++i, ++newOffset) { if (!isOctalChar(text.at(newOffset))) break; } if (newOffset == offset + 2) return offset; return newOffset; } return offset; } static QString replaceCaptures(const QString &pattern, const QStringList &captures, bool quote) { auto result = pattern; for (int i = captures.size() - 1; i >= 1; --i) { result.replace(QLatin1Char('%') + QString::number(i), quote ? QRegularExpression::escape(captures.at(i)) : captures.at(i)); } return result; } Definition Rule::definition() const { return m_def.definition(); } void Rule::setDefinition(const Definition &def) { m_def = def; } QString Rule::attribute() const { return m_attribute; } ContextSwitch Rule::context() const { return m_context; } bool Rule::isLookAhead() const { return m_lookAhead; } bool Rule::isDynamic() const { return m_dynamic; } bool Rule::firstNonSpace() const { return m_firstNonSpace; } int Rule::requiredColumn() const { return m_column; } FoldingRegion Rule::beginRegion() const { return m_beginRegion; } FoldingRegion Rule::endRegion() const { return m_endRegion; } bool Rule::load(QXmlStreamReader &reader) { Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); m_attribute = reader.attributes().value(QStringLiteral("attribute")).toString(); if (reader.name() != QLatin1String("IncludeRules")) // IncludeRules uses this with a different semantic m_context.parse(reader.attributes().value(QStringLiteral("context"))); m_firstNonSpace = Xml::attrToBool(reader.attributes().value(QStringLiteral("firstNonSpace"))); m_lookAhead = Xml::attrToBool(reader.attributes().value(QStringLiteral("lookAhead"))); bool colOk = false; m_column = reader.attributes().value(QStringLiteral("column")).toInt(&colOk); if (!colOk) m_column = -1; m_dynamic = Xml::attrToBool(reader.attributes().value(QStringLiteral("dynamic"))); auto regionName = reader.attributes().value(QLatin1String("beginRegion")); if (!regionName.isEmpty()) m_beginRegion = FoldingRegion(FoldingRegion::Begin, DefinitionData::get(m_def.definition())->foldingRegionId(regionName.toString())); regionName = reader.attributes().value(QLatin1String("endRegion")); if (!regionName.isEmpty()) m_endRegion = FoldingRegion(FoldingRegion::End, DefinitionData::get(m_def.definition())->foldingRegionId(regionName.toString())); auto result = doLoad(reader); if (m_lookAhead && m_context.isStay()) result = false; reader.readNext(); while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: { auto rule = Rule::create(reader.name()); if (rule) { rule->setDefinition(m_def.definition()); if (rule->load(reader)) { m_subRules.push_back(rule); reader.readNext(); } } else { reader.skipCurrentElement(); } break; } case QXmlStreamReader::EndElement: return result; default: reader.readNext(); break; } } return result; } void Rule::resolveContext() { m_context.resolve(m_def.definition()); foreach (const auto &rule, m_subRules) rule->resolveContext(); } bool Rule::doLoad(QXmlStreamReader& reader) { Q_UNUSED(reader); return true; } MatchResult Rule::match(const QString &text, int offset, const QStringList &captures) { Q_ASSERT(!text.isEmpty()); const auto result = doMatch(text, offset, captures); if (result.offset() == offset || result.offset() == text.size()) return result; foreach (const auto &subRule, m_subRules) { const auto subResult = subRule->match(text, result.offset(), QStringList()); if (subResult.offset() > result.offset()) return MatchResult(subResult.offset(), result.captures()); } return result; } Rule::Ptr Rule::create(const QStringRef& name) { Rule *rule = nullptr; if (name == QLatin1String("AnyChar")) rule = new AnyChar; else if (name == QLatin1String("DetectChar")) rule = new DetectChar; else if (name == QLatin1String("Detect2Chars")) rule = new Detect2Char; else if (name == QLatin1String("DetectIdentifier")) rule = new DetectIdentifier; else if (name == QLatin1String("DetectSpaces")) rule = new DetectSpaces; else if (name == QLatin1String("Float")) rule = new Float; else if (name == QLatin1String("Int")) rule = new Int; else if (name == QLatin1String("HlCChar")) rule = new HlCChar; else if (name == QLatin1String("HlCHex")) rule = new HlCHex; else if (name == QLatin1String("HlCOct")) rule = new HlCOct; else if (name == QLatin1String("HlCStringChar")) rule = new HlCStringChar; else if (name == QLatin1String("IncludeRules")) rule = new IncludeRules; else if (name == QLatin1String("keyword")) rule = new KeywordListRule; else if (name == QLatin1String("LineContinue")) rule = new LineContinue; else if (name == QLatin1String("RangeDetect")) rule = new RangeDetect; else if (name == QLatin1String("RegExpr")) rule = new RegExpr; else if (name == QLatin1String("StringDetect")) rule = new StringDetect; else if (name == QLatin1String("WordDetect")) rule = new WordDetect; else qCWarning(Log) << "Unknown rule type:" << name; return Ptr(rule); } -bool Rule::isDelimiter(QChar c) const +bool Rule::isWordDelimiter(QChar c) const { auto defData = DefinitionData::get(m_def.definition()); - return defData->isDelimiter(c); + return defData->isWordDelimiter(c); } bool AnyChar::doLoad(QXmlStreamReader& reader) { m_chars = reader.attributes().value(QStringLiteral("String")).toString(); if (m_chars.size() == 1) qCDebug(Log) << "AnyChar rule with just one char: use DetectChar instead."; return !m_chars.isEmpty(); } MatchResult AnyChar::doMatch(const QString& text, int offset, const QStringList&) { if (m_chars.contains(text.at(offset))) return offset + 1; return offset; } bool DetectChar::doLoad(QXmlStreamReader& reader) { const auto s = reader.attributes().value(QStringLiteral("char")); if (s.isEmpty()) return false; m_char = s.at(0); if (isDynamic()) { m_captureIndex = m_char.digitValue(); } return true; } MatchResult DetectChar::doMatch(const QString& text, int offset, const QStringList &captures) { if (isDynamic()) { if (captures.size() <= m_captureIndex || captures.at(m_captureIndex).isEmpty()) return offset; if (text.at(offset) == captures.at(m_captureIndex).at(0)) return offset + 1; return offset; } if (text.at(offset) == m_char) return offset + 1; return offset; } bool Detect2Char::doLoad(QXmlStreamReader& reader) { const auto s1 = reader.attributes().value(QStringLiteral("char")); const auto s2 = reader.attributes().value(QStringLiteral("char1")); if (s1.isEmpty() || s2.isEmpty()) return false; m_char1 = s1.at(0); m_char2 = s2.at(0); return true; } MatchResult Detect2Char::doMatch(const QString& text, int offset, const QStringList &captures) { Q_UNUSED(captures); // TODO if (text.size() - offset < 2) return offset; if (text.at(offset) == m_char1 && text.at(offset + 1) == m_char2) return offset + 2; return offset; } MatchResult DetectIdentifier::doMatch(const QString& text, int offset, const QStringList&) { if (!text.at(offset).isLetter() && text.at(offset) != QLatin1Char('_')) return offset; for (int i = offset + 1; i < text.size(); ++i) { const auto c = text.at(i); if (!c.isLetterOrNumber() && c != QLatin1Char('_')) return i; } return text.size(); } MatchResult DetectSpaces::doMatch(const QString& text, int offset, const QStringList&) { while(offset < text.size() && text.at(offset).isSpace()) ++offset; return offset; } MatchResult Float::doMatch(const QString& text, int offset, const QStringList&) { - if (offset > 0 && !isDelimiter(text.at(offset - 1))) + if (offset > 0 && !isWordDelimiter(text.at(offset - 1))) return offset; auto newOffset = offset; while (newOffset < text.size() && text.at(newOffset).isDigit()) ++newOffset; if (newOffset >= text.size() || text.at(newOffset) != QLatin1Char('.')) return offset; ++newOffset; while (newOffset < text.size() && text.at(newOffset).isDigit()) ++newOffset; if (newOffset == offset + 1) // we only found a decimal point return offset; auto expOffset = newOffset; if (expOffset >= text.size() || (text.at(expOffset) != QLatin1Char('e') && text.at(expOffset) != QLatin1Char('E'))) return newOffset; ++expOffset; if (expOffset < text.size() && (text.at(expOffset) == QLatin1Char('+') || text.at(expOffset) == QLatin1Char('-'))) ++expOffset; bool foundExpDigit = false; while (expOffset < text.size() && text.at(expOffset).isDigit()) { ++expOffset; foundExpDigit = true; } if (!foundExpDigit) return newOffset; return expOffset; } MatchResult HlCChar::doMatch(const QString& text, int offset, const QStringList&) { if (text.size() < offset + 3) return offset; if (text.at(offset) != QLatin1Char('\'') || text.at(offset + 1) == QLatin1Char('\'')) return offset; auto newOffset = matchEscapedChar(text, offset + 1); if (newOffset == offset + 1) { if (text.at(newOffset) == QLatin1Char('\\')) return offset; else ++newOffset; } if (newOffset >= text.size()) return offset; if (text.at(newOffset) == QLatin1Char('\'')) return newOffset + 1; return offset; } MatchResult HlCHex::doMatch(const QString& text, int offset, const QStringList&) { - if (offset > 0 && !isDelimiter(text.at(offset - 1))) + if (offset > 0 && !isWordDelimiter(text.at(offset - 1))) return offset; if (text.size() < offset + 3) return offset; if (text.at(offset) != QLatin1Char('0') || (text.at(offset + 1) != QLatin1Char('x') && text.at(offset + 1) != QLatin1Char('X'))) return offset; if (!isHexChar(text.at(offset + 2))) return offset; offset += 3; while (offset < text.size() && isHexChar(text.at(offset))) ++offset; // TODO Kate matches U/L suffix, QtC does not? return offset; } MatchResult HlCOct::doMatch(const QString& text, int offset, const QStringList&) { - if (offset > 0 && !isDelimiter(text.at(offset - 1))) + if (offset > 0 && !isWordDelimiter(text.at(offset - 1))) return offset; if (text.size() < offset + 2) return offset; if (text.at(offset) != QLatin1Char('0')) return offset; if (!isOctalChar(text.at(offset + 1))) return offset; offset += 2; while (offset < text.size() && isOctalChar(text.at(offset))) ++offset; return offset; } MatchResult HlCStringChar::doMatch(const QString& text, int offset, const QStringList&) { return matchEscapedChar(text, offset); } QString IncludeRules::contextName() const { return m_contextName; } QString IncludeRules::definitionName() const { return m_defName; } bool IncludeRules::includeAttribute() const { return m_includeAttribute; } bool IncludeRules::doLoad(QXmlStreamReader& reader) { const auto s = reader.attributes().value(QLatin1String("context")); auto splitted = s.split(QLatin1String("##"), QString::KeepEmptyParts); if (splitted.isEmpty()) return false; m_contextName = splitted.at(0).toString(); if (splitted.size() > 1) m_defName = splitted.at(1).toString(); m_includeAttribute = Xml::attrToBool(reader.attributes().value(QLatin1String("includeAttrib"))); return !m_contextName.isEmpty() || !m_defName.isEmpty(); } MatchResult IncludeRules::doMatch(const QString& text, int offset, const QStringList&) { Q_UNUSED(text); qCWarning(Log) << "Unresolved include rule for" << m_contextName << "##" << m_defName; return offset; } MatchResult Int::doMatch(const QString& text, int offset, const QStringList &captures) { - if (offset > 0 && !isDelimiter(text.at(offset - 1))) + if (offset > 0 && !isWordDelimiter(text.at(offset - 1))) return offset; Q_UNUSED(captures); // ### the doc says this can be dynamic, but how?? while(offset < text.size() && text.at(offset).isDigit()) ++offset; return offset; } bool KeywordListRule::doLoad(QXmlStreamReader& reader) { m_listName = reader.attributes().value(QLatin1String("String")).toString(); if (reader.attributes().hasAttribute(QLatin1String("insensitive"))) { m_hasCaseSensitivityOverride = true; m_caseSensitivityOverride = Xml::attrToBool(reader.attributes().value(QLatin1String("insensitive"))) ? Qt::CaseInsensitive : Qt::CaseSensitive; } else { m_hasCaseSensitivityOverride = false; } return !m_listName.isEmpty(); } MatchResult KeywordListRule::doMatch(const QString& text, int offset, const QStringList&) { - if (offset > 0 && !isDelimiter(text.at(offset - 1))) + if (offset > 0 && !isWordDelimiter(text.at(offset - 1))) return offset; if (m_keywordList.isEmpty()) { const auto def = definition(); Q_ASSERT(def.isValid()); auto defData = DefinitionData::get(def); m_keywordList = defData->keywordList(m_listName); } auto newOffset = offset; - while (text.size() > newOffset && !isDelimiter(text.at(newOffset))) + while (text.size() > newOffset && !isWordDelimiter(text.at(newOffset))) ++newOffset; if (newOffset == offset) return offset; if (m_hasCaseSensitivityOverride) { if (m_keywordList.contains(text.midRef(offset, newOffset - offset), m_caseSensitivityOverride)) return newOffset; } else { if (m_keywordList.contains(text.midRef(offset, newOffset - offset))) return newOffset; } return offset; } bool LineContinue::doLoad(QXmlStreamReader& reader) { const auto s = reader.attributes().value(QStringLiteral("char")); if (s.isEmpty()) m_char = QLatin1Char('\\'); else m_char = s.at(0); return true; } MatchResult LineContinue::doMatch(const QString& text, int offset, const QStringList&) { if (offset == text.size() - 1 && text.at(offset) == m_char) return offset + 1; return offset; } bool RangeDetect::doLoad(QXmlStreamReader& reader) { const auto s1 = reader.attributes().value(QStringLiteral("char")); const auto s2 = reader.attributes().value(QStringLiteral("char1")); if (s1.isEmpty() || s2.isEmpty()) return false; m_begin = s1.at(0); m_end = s2.at(0); return true; } MatchResult RangeDetect::doMatch(const QString& text, int offset, const QStringList&) { if (text.size() - offset < 2) return offset; if (text.at(offset) != m_begin) return offset; auto newOffset = offset + 1; while (newOffset < text.size()) { if (text.at(newOffset) == m_end) return newOffset + 1; ++newOffset; } return offset; } bool RegExpr::doLoad(QXmlStreamReader& reader) { m_pattern = reader.attributes().value(QStringLiteral("String")).toString(); m_regexp.setPattern(m_pattern); const auto isMinimal = Xml::attrToBool(reader.attributes().value(QStringLiteral("minimal"))); const auto isCaseInsensitive = Xml::attrToBool(reader.attributes().value(QStringLiteral("insensitive"))); m_regexp.setPatternOptions( (isMinimal ? QRegularExpression::InvertedGreedinessOption : QRegularExpression::NoPatternOption) | (isCaseInsensitive ? QRegularExpression::CaseInsensitiveOption : QRegularExpression::NoPatternOption)); return !m_pattern.isEmpty(); // m_regexp.isValid() would be better, but parses the regexp and thus is way too expensive } MatchResult RegExpr::doMatch(const QString& text, int offset, const QStringList &captures) { Q_ASSERT(m_regexp.isValid()); if (isDynamic()) m_regexp.setPattern(replaceCaptures(m_pattern, captures, true)); auto result = m_regexp.match(text, offset, QRegularExpression::NormalMatch, QRegularExpression::DontCheckSubjectStringMatchOption); if (result.capturedStart() == offset) return MatchResult(offset + result.capturedLength(), result.capturedTexts()); return MatchResult(offset, result.capturedStart()); } bool StringDetect::doLoad(QXmlStreamReader& reader) { m_string = reader.attributes().value(QStringLiteral("String")).toString(); m_caseSensitivity = Xml::attrToBool(reader.attributes().value(QStringLiteral("insensitive"))) ? Qt::CaseInsensitive : Qt::CaseSensitive; return !m_string.isEmpty(); } MatchResult StringDetect::doMatch(const QString& text, int offset, const QStringList &captures) { auto pattern = m_string; if (isDynamic()) pattern = replaceCaptures(m_string, captures, false); if (text.midRef(offset, pattern.size()).compare(pattern, m_caseSensitivity) == 0) return offset + pattern.size(); return offset; } bool WordDetect::doLoad(QXmlStreamReader& reader) { m_word = reader.attributes().value(QStringLiteral("String")).toString(); m_caseSensitivity = Xml::attrToBool(reader.attributes().value(QStringLiteral("insensitive"))) ? Qt::CaseInsensitive : Qt::CaseSensitive; return !m_word.isEmpty(); } MatchResult WordDetect::doMatch(const QString& text, int offset, const QStringList &captures) { Q_UNUSED(captures); // TODO if (text.size() - offset < m_word.size()) return offset; - if (offset > 0 && !isDelimiter(text.at(offset - 1))) + if (offset > 0 && !isWordDelimiter(text.at(offset - 1))) return offset; if (text.midRef(offset, m_word.size()).compare(m_word, m_caseSensitivity) != 0) return offset; - if (text.size() == offset + m_word.size() || isDelimiter(text.at(offset + m_word.size()))) + if (text.size() == offset + m_word.size() || isWordDelimiter(text.at(offset + m_word.size()))) return offset + m_word.size(); return offset; } diff --git a/src/lib/rule_p.h b/src/lib/rule_p.h index a1cb2ce..63aa811 100644 --- a/src/lib/rule_p.h +++ b/src/lib/rule_p.h @@ -1,254 +1,254 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KSYNTAXHIGHLIGHTING_RULE_P_H #define KSYNTAXHIGHLIGHTING_RULE_P_H #include "contextswitch_p.h" #include "definition.h" #include "definitionref_p.h" #include "foldingregion.h" #include "keywordlist_p.h" #include "matchresult_p.h" #include #include #include #include class QXmlStreamReader; namespace KSyntaxHighlighting { class Rule { public: Rule() = default; virtual ~Rule() = default; typedef std::shared_ptr Ptr; Definition definition() const; void setDefinition(const Definition &def); QString attribute() const; ContextSwitch context() const; bool isLookAhead() const; bool isDynamic() const; bool firstNonSpace() const; int requiredColumn() const; FoldingRegion beginRegion() const; FoldingRegion endRegion() const; bool load(QXmlStreamReader &reader); void resolveContext(); MatchResult match(const QString &text, int offset, const QStringList &captures); static Rule::Ptr create(const QStringRef &name); protected: virtual bool doLoad(QXmlStreamReader &reader); virtual MatchResult doMatch(const QString &text, int offset, const QStringList &captures) = 0; - bool isDelimiter(QChar c) const; + bool isWordDelimiter(QChar c) const; private: Q_DISABLE_COPY(Rule) DefinitionRef m_def; QString m_attribute; ContextSwitch m_context; QVector m_subRules; int m_column = -1; FoldingRegion m_beginRegion; FoldingRegion m_endRegion; bool m_firstNonSpace = false; bool m_lookAhead = false; bool m_dynamic = false; }; class AnyChar : public Rule { protected: bool doLoad(QXmlStreamReader & reader) override; MatchResult doMatch(const QString & text, int offset, const QStringList&) override; private: QString m_chars; }; class DetectChar : public Rule { protected: bool doLoad(QXmlStreamReader & reader) override; MatchResult doMatch(const QString & text, int offset, const QStringList &captures) override; private: QChar m_char; int m_captureIndex; }; class Detect2Char : public Rule { protected: bool doLoad(QXmlStreamReader & reader) override; MatchResult doMatch(const QString & text, int offset, const QStringList &captures) override; private: QChar m_char1; QChar m_char2; }; class DetectIdentifier : public Rule { protected: MatchResult doMatch(const QString & text, int offset, const QStringList&) override; }; class DetectSpaces : public Rule { protected: MatchResult doMatch(const QString & text, int offset, const QStringList&) override; }; class Float : public Rule { protected: MatchResult doMatch(const QString & text, int offset, const QStringList&) override; }; class IncludeRules : public Rule { public: QString contextName() const; QString definitionName() const; bool includeAttribute() const; protected: bool doLoad(QXmlStreamReader & reader) override; MatchResult doMatch(const QString & text, int offset, const QStringList&) override; private: QString m_contextName; QString m_defName; bool m_includeAttribute; }; class Int : public Rule { protected: MatchResult doMatch(const QString & text, int offset, const QStringList &captures) override; }; class HlCChar : public Rule { protected: MatchResult doMatch(const QString & text, int offset, const QStringList&) override; }; class HlCHex : public Rule { protected: MatchResult doMatch(const QString & text, int offset, const QStringList&) override; }; class HlCOct : public Rule { protected: MatchResult doMatch(const QString & text, int offset, const QStringList&) override; }; class HlCStringChar : public Rule { protected: MatchResult doMatch(const QString & text, int offset, const QStringList&) override; }; class KeywordListRule : public Rule { protected: bool doLoad(QXmlStreamReader & reader) override; MatchResult doMatch(const QString & text, int offset, const QStringList&) override; private: QString m_listName; KeywordList m_keywordList; bool m_hasCaseSensitivityOverride; Qt::CaseSensitivity m_caseSensitivityOverride; }; class LineContinue : public Rule { protected: bool doLoad(QXmlStreamReader & reader) override; MatchResult doMatch(const QString & text, int offset, const QStringList&) override; private: QChar m_char; }; class RangeDetect : public Rule { protected: bool doLoad(QXmlStreamReader & reader) override; MatchResult doMatch(const QString & text, int offset, const QStringList&) override; private: QChar m_begin; QChar m_end; }; class RegExpr : public Rule { protected: bool doLoad(QXmlStreamReader & reader) override; MatchResult doMatch(const QString & text, int offset, const QStringList &captures) override; private: QString m_pattern; QRegularExpression m_regexp; }; class StringDetect : public Rule { protected: bool doLoad(QXmlStreamReader & reader) override; MatchResult doMatch(const QString & text, int offset, const QStringList &captures) override; private: QString m_string; Qt::CaseSensitivity m_caseSensitivity; }; class WordDetect : public Rule { protected: bool doLoad(QXmlStreamReader & reader) override; MatchResult doMatch(const QString & text, int offset, const QStringList &captures) override; private: QString m_word; Qt::CaseSensitivity m_caseSensitivity; }; } #endif // KSYNTAXHIGHLIGHTING_RULE_P_H