/* SPDX-FileCopyrightText: 2021 Jonathan Poelen SPDX-License-Identifier: MIT */ #include "highlightingdata_p.hpp" #include "ksyntaxhighlighting_logging.h" #include "xml_p.h" #include #include using namespace KSyntaxHighlighting; template static void initRuleData(Data &data, Args &&...args) { new (&data) Data{std::move(args)...}; } static Qt::CaseSensitivity attrToCaseSensitivity(QStringView str) { return Xml::attrToBool(str) ? Qt::CaseInsensitive : Qt::CaseSensitive; } static HighlightingContextData::Rule::WordDelimiters loadAdditionalWordDelimiters(QXmlStreamReader &reader) { return HighlightingContextData::Rule::WordDelimiters{ reader.attributes().value(QLatin1String("additionalDeliminator")).toString(), reader.attributes().value(QLatin1String("weakDeliminator")).toString(), }; } static bool checkIsNotEmpty(QStringView str, const char *attrName, const QString &defName, QXmlStreamReader &reader) { if (!str.isEmpty()) { return true; } qCWarning(Log) << defName << "at line" << reader.lineNumber() << ": " << attrName << "attribute is empty"; return false; } static bool checkIsChar(QStringView str, const char *attrName, const QString &defName, QXmlStreamReader &reader) { if (str.size() == 1) { return true; } qCWarning(Log) << defName << "at line" << reader.lineNumber() << ": " << attrName << "attribute must contain exactly 1 character"; return false; } static bool loadRule(const QString &defName, HighlightingContextData::Rule &rule, QXmlStreamReader &reader) { using Rule = HighlightingContextData::Rule; QStringView name = reader.name(); const auto attrs = reader.attributes(); bool isIncludeRules = false; if (name == QLatin1String("DetectChar")) { const auto s = attrs.value(QLatin1String("char")); if (!checkIsChar(s, "char", defName, reader)) { return false; } const QChar c = s.at(0); const bool dynamic = Xml::attrToBool(attrs.value(QLatin1String("dynamic"))); initRuleData(rule.data.detectChar, c, dynamic); rule.type = Rule::Type::DetectChar; } else if (name == QLatin1String("RegExpr")) { const auto pattern = attrs.value(QLatin1String("String")); if (!checkIsNotEmpty(pattern, "String", defName, reader)) { return false; } const auto isCaseInsensitive = attrToCaseSensitivity(attrs.value(QLatin1String("insensitive"))); const auto isMinimal = Xml::attrToBool(attrs.value(QLatin1String("minimal"))); const auto dynamic = Xml::attrToBool(attrs.value(QLatin1String("dynamic"))); initRuleData(rule.data.regExpr, pattern.toString(), isCaseInsensitive, isMinimal, dynamic); rule.type = Rule::Type::RegExpr; } else if (name == QLatin1String("IncludeRules")) { const auto context = attrs.value(QLatin1String("context")); if (!checkIsNotEmpty(context, "context", defName, reader)) { return false; } const bool includeAttribute = Xml::attrToBool(attrs.value(QLatin1String("includeAttrib"))); initRuleData(rule.data.includeRules, context.toString(), includeAttribute); rule.type = Rule::Type::IncludeRules; isIncludeRules = true; } else if (name == QLatin1String("Detect2Chars")) { const auto s1 = attrs.value(QLatin1String("char")); const auto s2 = attrs.value(QLatin1String("char1")); if (!checkIsChar(s1, "char", defName, reader)) { return false; } if (!checkIsChar(s2, "char1", defName, reader)) { return false; } initRuleData(rule.data.detect2Chars, s1.at(0), s2.at(0)); rule.type = Rule::Type::Detect2Chars; } else if (name == QLatin1String("keyword")) { const auto s = attrs.value(QLatin1String("String")); if (!checkIsNotEmpty(s, "String", defName, reader)) { return false; } Qt::CaseSensitivity caseSensitivityOverride = Qt::CaseInsensitive; bool hasCaseSensitivityOverride = false; /** * we might overwrite the case sensitivity * then we need to init the list for lookup of that sensitivity setting */ if (attrs.hasAttribute(QLatin1String("insensitive"))) { hasCaseSensitivityOverride = true; caseSensitivityOverride = attrToCaseSensitivity(attrs.value(QLatin1String("insensitive"))); } initRuleData(rule.data.keyword, s.toString(), loadAdditionalWordDelimiters(reader), caseSensitivityOverride, hasCaseSensitivityOverride); rule.type = Rule::Type::Keyword; } else if (name == QLatin1String("DetectSpaces")) { rule.type = Rule::Type::DetectSpaces; } else if (name == QLatin1String("StringDetect")) { const auto string = attrs.value(QLatin1String("String")); if (!checkIsNotEmpty(string, "String", defName, reader)) { return false; } const auto caseSensitivity = attrToCaseSensitivity(attrs.value(QLatin1String("insensitive"))); const auto dynamic = Xml::attrToBool(attrs.value(QLatin1String("dynamic"))); const bool isSensitive = (caseSensitivity == Qt::CaseSensitive); // String can be replaced with DetectChar or AnyChar if (!dynamic && string.size() == 1) { QChar c = string.at(0); if (isSensitive || c.toLower() == c.toUpper()) { initRuleData(rule.data.detectChar, c, dynamic); rule.type = Rule::Type::DetectChar; } else { initRuleData(rule.data.anyChar, c.toLower() + c.toUpper()); rule.type = Rule::Type::AnyChar; } } // String can be replaced with Detect2Chars else if (isSensitive && !dynamic && string.size() == 2) { initRuleData(rule.data.detect2Chars, string.at(0), string.at(1)); rule.type = Rule::Type::Detect2Chars; } else { initRuleData(rule.data.stringDetect, string.toString(), caseSensitivity, dynamic); rule.type = Rule::Type::StringDetect; } } else if (name == QLatin1String("WordDetect")) { const auto word = attrs.value(QLatin1String("String")); if (!checkIsNotEmpty(word, "String", defName, reader)) { return false; } const auto caseSensitivity = attrToCaseSensitivity(attrs.value(QLatin1String("insensitive"))); initRuleData(rule.data.wordDetect, word.toString(), loadAdditionalWordDelimiters(reader), caseSensitivity); rule.type = Rule::Type::WordDetect; } else if (name == QLatin1String("AnyChar")) { const auto chars = attrs.value(QLatin1String("String")); if (!checkIsNotEmpty(chars, "String", defName, reader)) { return false; } // AnyChar can be replaced with DetectChar if (chars.size() == 1) { initRuleData(rule.data.detectChar, chars.at(0), false); rule.type = Rule::Type::DetectChar; } else { initRuleData(rule.data.anyChar, chars.toString()); rule.type = Rule::Type::AnyChar; } } else if (name == QLatin1String("DetectIdentifier")) { rule.type = Rule::Type::DetectIdentifier; } else if (name == QLatin1String("LineContinue")) { const auto s = attrs.value(QLatin1String("char")); const QChar c = s.isEmpty() ? QLatin1Char('\\') : s.at(0); initRuleData(rule.data.lineContinue, c); rule.type = Rule::Type::LineContinue; } else if (name == QLatin1String("Int")) { initRuleData(rule.data.detectInt, loadAdditionalWordDelimiters(reader)); rule.type = Rule::Type::Int; } else if (name == QLatin1String("Float")) { initRuleData(rule.data.detectFloat, loadAdditionalWordDelimiters(reader)); rule.type = Rule::Type::Float; } else if (name == QLatin1String("HlCStringChar")) { rule.type = Rule::Type::HlCStringChar; } else if (name == QLatin1String("RangeDetect")) { const auto s1 = attrs.value(QLatin1String("char")); const auto s2 = attrs.value(QLatin1String("char1")); if (!checkIsChar(s1, "char", defName, reader)) { return false; } if (!checkIsChar(s2, "char1", defName, reader)) { return false; } initRuleData(rule.data.rangeDetect, s1.at(0), s2.at(0)); rule.type = Rule::Type::RangeDetect; } else if (name == QLatin1String("HlCHex")) { initRuleData(rule.data.hlCHex, loadAdditionalWordDelimiters(reader)); rule.type = Rule::Type::HlCHex; } else if (name == QLatin1String("HlCChar")) { rule.type = Rule::Type::HlCChar; } else if (name == QLatin1String("HlCOct")) { initRuleData(rule.data.hlCOct, loadAdditionalWordDelimiters(reader)); rule.type = Rule::Type::HlCOct; } else { qCWarning(Log) << "Unknown rule type:" << name; return false; } if (!isIncludeRules) { rule.common.contextName = attrs.value(QLatin1String("context")).toString(); rule.common.beginRegionName = attrs.value(QLatin1String("beginRegion")).toString(); rule.common.endRegionName = attrs.value(QLatin1String("endRegion")).toString(); rule.common.firstNonSpace = Xml::attrToBool(attrs.value(QLatin1String("firstNonSpace"))); rule.common.lookAhead = Xml::attrToBool(attrs.value(QLatin1String("lookAhead"))); // attribute is only used when lookAhead is false if (!rule.common.lookAhead) { rule.common.attributeName = attrs.value(QLatin1String("attribute")).toString(); } bool colOk = false; rule.common.column = attrs.value(QLatin1String("column")).toInt(&colOk); if (!colOk) { rule.common.column = -1; } } return true; } template static void dataRuleVisit(HighlightingContextData::Rule::Type type, Data1 &&data1, Data2 &&data2, Visitor &&visitor) { using Rule = HighlightingContextData::Rule; using Type = Rule::Type; switch (type) { case Type::AnyChar: visitor(data1.anyChar, data2.anyChar); break; case Type::DetectChar: visitor(data1.detectChar, data2.detectChar); break; case Type::Detect2Chars: visitor(data1.detect2Chars, data2.detect2Chars); break; case Type::HlCOct: visitor(data1.hlCOct, data2.hlCOct); break; case Type::IncludeRules: visitor(data1.includeRules, data2.includeRules); break; case Type::Int: visitor(data1.detectInt, data2.detectInt); break; case Type::Keyword: visitor(data1.keyword, data2.keyword); break; case Type::LineContinue: visitor(data1.lineContinue, data2.lineContinue); break; case Type::RangeDetect: visitor(data1.rangeDetect, data2.rangeDetect); break; case Type::RegExpr: visitor(data1.regExpr, data2.regExpr); break; case Type::StringDetect: visitor(data1.stringDetect, data2.stringDetect); break; case Type::WordDetect: visitor(data1.wordDetect, data2.wordDetect); break; case Type::Float: visitor(data1.detectFloat, data2.detectFloat); break; case Type::HlCHex: visitor(data1.hlCHex, data2.hlCHex); break; case Type::HlCStringChar: case Type::DetectIdentifier: case Type::DetectSpaces: case Type::HlCChar: case Type::Unknown:; } } HighlightingContextData::Rule::Rule() noexcept = default; HighlightingContextData::Rule::Rule(Rule &&other) noexcept : common(std::move(other.common)) { dataRuleVisit(other.type, data, other.data, [](auto &data1, auto &data2) { using Data = std::remove_reference_t; new (&data1) Data(std::move(data2)); }); type = other.type; } HighlightingContextData::Rule::Rule(const Rule &other) : common(other.common) { dataRuleVisit(other.type, data, other.data, [](auto &data1, auto &data2) { using Data = std::remove_reference_t; new (&data1) Data(data2); }); type = other.type; } HighlightingContextData::Rule::~Rule() { dataRuleVisit(type, data, data, [](auto &data, auto &) { using Data = std::remove_reference_t; data.~Data(); }); } void HighlightingContextData::load(const QString &defName, QXmlStreamReader &reader) { Q_ASSERT(reader.name() == QLatin1String("context")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); name = reader.attributes().value(QLatin1String("name")).toString(); attribute = reader.attributes().value(QLatin1String("attribute")).toString(); lineEndContext = reader.attributes().value(QLatin1String("lineEndContext")).toString(); lineEmptyContext = reader.attributes().value(QLatin1String("lineEmptyContext")).toString(); fallthroughContext = reader.attributes().value(QLatin1String("fallthroughContext")).toString(); noIndentationBasedFolding = Xml::attrToBool(reader.attributes().value(QLatin1String("noIndentationBasedFolding"))); stopEmptyLineContextSwitchLoop = Xml::attrToBool(reader.attributes().value(QLatin1String("stopEmptyLineContextSwitchLoop"))); rules.reserve(8); reader.readNext(); while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: { auto &rule = rules.emplace_back(); if (!loadRule(defName, rule, reader)) { rules.pop_back(); } // be done with this rule, skip all subelements, e.g. no longer supported sub-rules reader.skipCurrentElement(); reader.readNext(); break; } case QXmlStreamReader::EndElement: return; default: reader.readNext(); break; } } }