feat: add missing KF6 framework recipes

This commit is contained in:
2026-05-07 07:53:26 +01:00
parent d8d498f831
commit a69f479b52
2374 changed files with 2610246 additions and 0 deletions
@@ -0,0 +1,14 @@
add_subdirectory(indexer)
if(TARGET Qt6::Gui)
add_subdirectory(lib)
add_subdirectory(cli)
endif()
if(TARGET Qt6::Quick)
add_subdirectory(quick)
endif()
ecm_qt_install_logging_categories(
EXPORT KSYNTAXHIGHLIGHTING
FILE ksyntaxhighlighting.categories
DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}
)
@@ -0,0 +1,11 @@
#!/bin/sh
# extract language and section names from XML syntax definition files
# and adapt for lupdate-based extraction to match the rest of the code
$EXTRACTATTR --attr=language,name,Language --attr="language,section,Language Section" ../data/syntax/*.xml >> rc.cpp || exit 12
sed -i -e 's/^i18nc/QT_TRANSLATE_NOOP/' rc.cpp
grep --no-filename '"name"' ../data/themes/*.theme | \
sed -r -e 's/"name"/QT_TRANSLATE_NOOP("Theme", /; s/://; s/,?$/);/' >> rc.cpp
$EXTRACT_TR_STRINGS `find . -name \*.cpp -o -name \*.h -o -name \*.ui -o -name \*.qml` -o $podir/syntaxhighlighting6_qt.pot
@@ -0,0 +1,5 @@
add_executable(ksyntaxhighlighter6 ksyntaxhighlighter.cpp)
ecm_mark_nongui_executable(ksyntaxhighlighter6)
target_link_libraries(ksyntaxhighlighter6 KF6SyntaxHighlighting)
install(TARGETS ksyntaxhighlighter6 ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
@@ -0,0 +1,305 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#include "ksyntaxhighlighting_version.h"
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/DefinitionDownloader>
#include <KSyntaxHighlighting/Repository>
#include <KSyntaxHighlighting/Theme>
#include <ansihighlighter.h>
#include <htmlhighlighter.h>
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QFile>
#include <cstdio>
using namespace KSyntaxHighlighting;
template<class Highlighter, class... Ts>
static void applyHighlighter(Highlighter &highlighter,
QCommandLineParser &parser,
bool fromFileName,
const QString &inFileName,
const QCommandLineOption &outputName,
const Ts &...highlightParams)
{
if (parser.isSet(outputName)) {
highlighter.setOutputFile(parser.value(outputName));
} else {
highlighter.setOutputFile(stdout);
}
if (fromFileName) {
highlighter.highlightFile(inFileName, highlightParams...);
} else {
QFile inFile;
inFile.open(stdin, QIODevice::ReadOnly);
highlighter.highlightData(&inFile, highlightParams...);
}
}
static Theme theme(const Repository &repo, const QString &themeName, Repository::DefaultTheme t)
{
if (themeName.isEmpty()) {
return repo.defaultTheme(t);
}
return repo.theme(themeName);
}
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
QCoreApplication::setApplicationName(QStringLiteral("ksyntaxhighlighter"));
QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org"));
QCoreApplication::setOrganizationName(QStringLiteral("KDE"));
QCoreApplication::setApplicationVersion(QStringLiteral(KSYNTAXHIGHLIGHTING_VERSION_STRING));
QCommandLineParser parser;
parser.setApplicationDescription(app.translate("SyntaxHighlightingCLI", "Command line syntax highlighter using KSyntaxHighlighting syntax definitions."));
parser.addHelpOption();
parser.addVersionOption();
parser.addPositionalArgument(
app.translate("SyntaxHighlightingCLI", "source"),
app.translate("SyntaxHighlightingCLI", "The source file to highlight. If absent, read the file from stdin and the --syntax option must be used."));
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"));
parser.addOption(themeName);
QCommandLineOption outputFormatOption(QStringList() << QStringLiteral("f") << QStringLiteral("output-format"),
app.translate("SyntaxHighlightingCLI", "Use the specified format instead of html. Must be html, ansi or ansi256."),
app.translate("SyntaxHighlightingCLI", "format"),
QStringLiteral("html"));
parser.addOption(outputFormatOption);
QCommandLineOption traceOption(QStringList() << QStringLiteral("syntax-trace"),
app.translate("SyntaxHighlightingCLI",
"Add information to debug a syntax file. Only works with --output-format=ansi or ansi256. Possible "
"values are format, region, context, stackSize and all."),
app.translate("SyntaxHighlightingCLI", "type"));
parser.addOption(traceOption);
QCommandLineOption noAnsiEditorBg(QStringList() << QStringLiteral("b") << QStringLiteral("no-ansi-background"),
app.translate("SyntaxHighlightingCLI", "Disable ANSI background for the default color."));
parser.addOption(noAnsiEditorBg);
QCommandLineOption bgRole(QStringList() << QStringLiteral("B") << QStringLiteral("background-role"),
app.translate("SyntaxHighlightingCLI", "Select background color role from theme."),
app.translate("SyntaxHighlightingCLI", "role"));
parser.addOption(bgRole);
QCommandLineOption unbufferedAnsi(QStringList() << QStringLiteral("U") << QStringLiteral("unbuffered"),
app.translate("SyntaxHighlightingCLI", "For ansi and ansi256 formats, flush the output buffer on each line."));
parser.addOption(unbufferedAnsi);
QCommandLineOption titleOption(
QStringList() << QStringLiteral("T") << QStringLiteral("title"),
app.translate("SyntaxHighlightingCLI", "Set HTML page's title\n(default: the filename or \"KSyntaxHighlighter\" if reading from stdin)."),
app.translate("SyntaxHighlightingCLI", "title"));
parser.addOption(titleOption);
parser.process(app);
Repository repo;
if (parser.isSet(listDefs)) {
for (const auto &def : repo.definitions()) {
fprintf(stdout, "%s\n", qPrintable(def.name()));
}
return 0;
}
if (parser.isSet(listThemes)) {
for (const auto &theme : repo.themes()) {
fprintf(stdout, "%s\n", qPrintable(theme.name()));
}
return 0;
}
Theme::EditorColorRole bgColorRole = Theme::BackgroundColor;
if (parser.isSet(bgRole)) {
/*
* Theme::EditorColorRole contains border, foreground and background colors.
* To ensure that only the background colors used in text editing are used,
* QMetaEnum is avoided and values are listed in hard.
*/
struct BgRole {
QStringView name;
Theme::EditorColorRole role;
// name for display
const char *asciiName;
};
#define BG_ROLE(role) \
BgRole \
{ \
QStringView(u"" #role, sizeof(#role) - 1), Theme::role, #role \
}
constexpr BgRole bgRoles[] = {
BG_ROLE(BackgroundColor),
BG_ROLE(TextSelection),
BG_ROLE(CurrentLine),
BG_ROLE(SearchHighlight),
BG_ROLE(ReplaceHighlight),
BG_ROLE(BracketMatching),
BG_ROLE(CodeFolding),
BG_ROLE(MarkBookmark),
BG_ROLE(MarkBreakpointActive),
BG_ROLE(MarkBreakpointReached),
BG_ROLE(MarkBreakpointDisabled),
BG_ROLE(MarkExecution),
BG_ROLE(MarkWarning),
BG_ROLE(MarkError),
BG_ROLE(TemplateBackground),
BG_ROLE(TemplatePlaceholder),
BG_ROLE(TemplateFocusedPlaceholder),
BG_ROLE(TemplateReadOnlyPlaceholder),
};
#undef BG_ROLE
const auto role = parser.value(bgRole);
bool ok = false;
for (const auto &def : bgRoles) {
if (def.name == role) {
bgColorRole = def.role;
ok = true;
break;
}
}
if (!ok) {
fprintf(stderr, "Unknown background role. Expected:\n");
for (const auto &def : bgRoles) {
fprintf(stderr, " - %s\n", def.asciiName);
}
return 1;
}
}
if (parser.isSet(updateDefs)) {
DefinitionDownloader downloader(&repo);
QObject::connect(&downloader, &DefinitionDownloader::informationMessage, &app, [](const QString &msg) {
fprintf(stdout, "%s\n", qPrintable(msg));
});
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)) {
const QString syntax = parser.value(syntaxName);
def = repo.definitionForName(syntax);
if (!def.isValid()) {
/* see if it's a mimetype instead */
def = repo.definitionForMimeType(syntax);
if (!def.isValid()) {
/* see if it's a extension instead */
def = repo.definitionForFileName(QLatin1String("f.") + syntax);
if (!def.isValid()) {
/* see if it's a filename instead */
def = repo.definitionForFileName(syntax);
}
}
}
} else if (fromFileName) {
def = repo.definitionForFileName(inFileName);
} else {
parser.showHelp(1);
}
if (!def.isValid()) {
fprintf(stderr, "Unknown syntax.\n");
return 1;
}
const QString outputFormat = parser.value(outputFormatOption);
if (0 == outputFormat.compare(QLatin1String("html"), Qt::CaseInsensitive)) {
QString title;
if (parser.isSet(titleOption)) {
title = parser.value(titleOption);
}
HtmlHighlighter highlighter;
highlighter.setDefinition(def);
highlighter.setBackgroundRole(bgColorRole);
highlighter.setTheme(theme(repo, parser.value(themeName), Repository::LightTheme));
applyHighlighter(highlighter, parser, fromFileName, inFileName, outputName, title);
} else {
auto AnsiFormat = AnsiHighlighter::AnsiFormat::TrueColor;
// compatible with the old ansi256Colors value
if (outputFormat.startsWith(QLatin1String("ansi256"), Qt::CaseInsensitive)) {
AnsiFormat = AnsiHighlighter::AnsiFormat::XTerm256Color;
} else if (0 != outputFormat.compare(QLatin1String("ansi"), Qt::CaseInsensitive)) {
fprintf(stderr, "Unknown output format.\n");
return 2;
}
AnsiHighlighter::Options options{};
options |= parser.isSet(noAnsiEditorBg) ? AnsiHighlighter::Option::NoOptions : AnsiHighlighter::Option::UseEditorBackground;
options |= parser.isSet(unbufferedAnsi) ? AnsiHighlighter::Option::Unbuffered : AnsiHighlighter::Option::NoOptions;
if (parser.isSet(traceOption)) {
const auto traceOptions = parser.values(traceOption);
for (auto const &option : traceOptions) {
if (option == QStringLiteral("format")) {
options |= AnsiHighlighter::Option::TraceFormat;
} else if (option == QStringLiteral("region")) {
options |= AnsiHighlighter::Option::TraceRegion;
} else if (option == QStringLiteral("context")) {
options |= AnsiHighlighter::Option::TraceContext;
} else if (option == QStringLiteral("stackSize")) {
options |= AnsiHighlighter::Option::TraceStackSize;
} else if (option == QStringLiteral("all")) {
options |= AnsiHighlighter::Option::TraceAll;
} else {
fprintf(stderr, "Unknown trace name.\n");
return 2;
}
}
}
AnsiHighlighter highlighter;
highlighter.setDefinition(def);
highlighter.setBackgroundRole(bgColorRole);
highlighter.setTheme(theme(repo, parser.value(themeName), Repository::DarkTheme));
applyHighlighter(highlighter, parser, fromFileName, inFileName, outputName, AnsiFormat, options);
}
return 0;
}
@@ -0,0 +1,36 @@
# when cross compiling, use either the executable offered or try to cross-compile it in place
if(CMAKE_CROSSCOMPILING AND KATEHIGHLIGHTINGINDEXER_EXECUTABLE)
add_executable(katehighlightingindexer IMPORTED GLOBAL)
set_target_properties(katehighlightingindexer PROPERTIES IMPORTED_LOCATION ${KATEHIGHLIGHTINGINDEXER_EXECUTABLE})
elseif(CMAKE_CROSSCOMPILING)
include(ECMQueryQt)
ecm_query_qt(NATIVE_PREFIX QT_HOST_PREFIX)
message(STATUS "Building katehighlightingindexer against ${NATIVE_PREFIX}")
include(ExternalProject)
ExternalProject_Add(native_katehighlightingindexer
SOURCE_DIR ${CMAKE_SOURCE_DIR}
CMAKE_ARGS -DKSYNTAXHIGHLIGHTING_USE_GUI=OFF
-DECM_DIR=${ECM_DIR} -DCMAKE_PREFIX_PATH=${NATIVE_PREFIX}
-DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}
-DQT_MAJOR_VERSION=6
INSTALL_COMMAND ""
BUILD_BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/native_katehighlightingindexer-prefix/src/native_katehighlightingindexer-build/bin/katehighlightingindexer
)
add_executable(katehighlightingindexer IMPORTED GLOBAL)
add_dependencies(katehighlightingindexer native_katehighlightingindexer)
set_target_properties(katehighlightingindexer PROPERTIES IMPORTED_LOCATION
${CMAKE_CURRENT_BINARY_DIR}/native_katehighlightingindexer-prefix/src/native_katehighlightingindexer-build/bin/katehighlightingindexer)
else()
# host build
add_executable(katehighlightingindexer katehighlightingindexer.cpp ../lib/worddelimiters.cpp)
ecm_mark_nongui_executable(katehighlightingindexer)
if(XercesC_FOUND)
add_definitions(-DHAS_XERCESC)
kde_target_enable_exceptions(katehighlightingindexer PRIVATE)
target_link_libraries(katehighlightingindexer Qt6::Core XercesC::XercesC)
else()
target_link_libraries(katehighlightingindexer Qt6::Core)
endif()
endif()
@@ -0,0 +1,115 @@
add_library(KF6SyntaxHighlighting)
set_target_properties(KF6SyntaxHighlighting PROPERTIES
VERSION ${KSYNTAXHIGHLIGHTING_VERSION}
SOVERSION ${KSYNTAXHIGHLIGHTING_SOVERSION}
EXPORT_NAME SyntaxHighlighting
)
ecm_create_qm_loader(syntax_highlighting_QM_LOADER syntaxhighlighting6_qt)
target_sources(KF6SyntaxHighlighting PRIVATE
abstracthighlighter.cpp
context.cpp
contextswitch.cpp
definitiondownloader.cpp
highlightingdata.cpp
foldingregion.cpp
format.cpp
htmlhighlighter.cpp
ansihighlighter.cpp
keywordlist.cpp
rule.cpp
definition.cpp
repository.cpp
state.cpp
syntaxhighlighter.cpp
theme.cpp
wildcardmatcher.cpp
themedata.cpp
worddelimiters.cpp
${syntax_highlighting_QM_LOADER}
$<TARGET_OBJECTS:SyntaxHighlightingData>
)
ecm_qt_declare_logging_category(KF6SyntaxHighlighting
HEADER ksyntaxhighlighting_logging.h
IDENTIFIER KSyntaxHighlighting::Log
CATEGORY_NAME kf.syntaxhighlighting
OLD_CATEGORY_NAMES org.kde.ksyntaxhighlighting
DESCRIPTION "Syntax Highlighting"
EXPORT KSYNTAXHIGHLIGHTING
)
ecm_generate_export_header(KF6SyntaxHighlighting
BASE_NAME KSyntaxHighlighting
GROUP_BASE_NAME KF
VERSION ${KF_VERSION}
USE_VERSION_HEADER
DEPRECATED_BASE_VERSION 0
DEPRECATION_VERSIONS
EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT}
)
target_link_libraries(KF6SyntaxHighlighting
PUBLIC
Qt6::Gui
PRIVATE
Qt6::Network
)
set(Forwarding_Header_Names
AbstractHighlighter
Definition
DefinitionDownloader
FoldingRegion
Format
Repository
State
SyntaxHighlighter
Theme
WildcardMatcher
)
ecm_generate_headers(CamelCase_HEADERS
HEADER_NAMES ${Forwarding_Header_Names}
REQUIRED_HEADERS SyntaxHighlighting_HEADERS
OUTPUT_DIR ${CMAKE_BINARY_DIR}/KSyntaxHighlighting/KSyntaxHighlighting
)
target_include_directories(KF6SyntaxHighlighting
INTERFACE "$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/KSyntaxHighlighting>"
PUBLIC "$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/KSyntaxHighlighting;>"
)
install(TARGETS KF6SyntaxHighlighting EXPORT KF6SyntaxHighlightingTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES
${CamelCase_HEADERS}
${SyntaxHighlighting_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/ksyntaxhighlighting_export.h
DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KSyntaxHighlighting/KSyntaxHighlighting
)
if(BUILD_QCH)
ecm_add_qch(
KF6SyntaxHighlighting_QCH
NAME KSyntaxHighlighting
BASE_NAME KF6SyntaxHighlighting
VERSION ${KF_VERSION}
ORG_DOMAIN org.kde
SOURCES # using only public headers, to cover only public API
${SyntaxHighlighting_HEADERS}
MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md"
LINK_QCHS
Qt6Core_QCH
Qt6Gui_QCH
INCLUDE_DIRS
${CMAKE_CURRENT_BINARY_DIR}
BLANK_MACROS
KSYNTAXHIGHLIGHTING_EXPORT
KSYNTAXHIGHLIGHTING_DEPRECATED
TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
COMPONENT Devel
)
endif()
@@ -0,0 +1,440 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#include "abstracthighlighter.h"
#include "abstracthighlighter_p.h"
#include "context_p.h"
#include "definition_p.h"
#include "foldingregion.h"
#include "format.h"
#include "ksyntaxhighlighting_logging.h"
#include "repository.h"
#include "repository_p.h"
#include "rule_p.h"
#include "state.h"
#include "state_p.h"
#include "theme.h"
using namespace KSyntaxHighlighting;
AbstractHighlighterPrivate::AbstractHighlighterPrivate()
{
}
AbstractHighlighterPrivate::~AbstractHighlighterPrivate()
{
}
void AbstractHighlighterPrivate::ensureDefinitionLoaded()
{
auto defData = DefinitionData::get(m_definition);
if (Q_UNLIKELY(!m_definition.isValid())) {
if (defData->repo && !defData->name.isEmpty()) {
qCDebug(Log) << "Definition became invalid, trying re-lookup.";
m_definition = defData->repo->definitionForName(defData->name);
defData = DefinitionData::get(m_definition);
}
if (Q_UNLIKELY(!defData->repo && !defData->fileName.isEmpty())) {
qCCritical(Log) << "Repository got deleted while a highlighter is still active!";
}
}
if (m_definition.isValid()) {
defData->load();
}
}
AbstractHighlighter::AbstractHighlighter()
: d_ptr(new AbstractHighlighterPrivate)
{
}
AbstractHighlighter::AbstractHighlighter(AbstractHighlighterPrivate *dd)
: d_ptr(dd)
{
}
AbstractHighlighter::~AbstractHighlighter()
{
delete d_ptr;
}
Definition AbstractHighlighter::definition() const
{
return d_ptr->m_definition;
}
void AbstractHighlighter::setDefinition(const Definition &def)
{
Q_D(AbstractHighlighter);
d->m_definition = def;
}
Theme AbstractHighlighter::theme() const
{
Q_D(const AbstractHighlighter);
return d->m_theme;
}
void AbstractHighlighter::setTheme(const Theme &theme)
{
Q_D(AbstractHighlighter);
d->m_theme = theme;
}
/**
* Returns the index of the first non-space character. If the line is empty,
* or only contains white spaces, text.size() is returned.
*/
static inline int firstNonSpaceChar(QStringView text)
{
for (int i = 0; i < text.length(); ++i) {
if (!text[i].isSpace()) {
return i;
}
}
return text.size();
}
State AbstractHighlighter::highlightLine(QStringView text, const State &state)
{
Q_D(AbstractHighlighter);
// verify definition, deal with no highlighting being enabled
d->ensureDefinitionLoaded();
const auto defData = DefinitionData::get(d->m_definition);
if (!d->m_definition.isValid() || !defData->isLoaded()) {
applyFormat(0, text.size(), Format());
return State();
}
// limit the cache for unification to some reasonable size
// we use here at the moment 64k elements to not hog too much memory
// and to make the clearing no big stall
if (defData->unify.size() > 64 * 1024)
defData->unify.clear();
// verify/initialize state
auto newState = state;
auto stateData = StateData::get(newState);
bool isSharedData = true;
if (Q_UNLIKELY(stateData && stateData->m_defId != defData->id)) {
qCDebug(Log) << "Got invalid state, resetting.";
stateData = nullptr;
}
if (Q_UNLIKELY(!stateData)) {
stateData = StateData::reset(newState);
stateData->push(defData->initialContext(), QStringList());
stateData->m_defId = defData->id;
isSharedData = false;
}
// process empty lines
if (Q_UNLIKELY(text.isEmpty())) {
/**
* handle line empty context switches
* guard against endless loops
* see https://phabricator.kde.org/D18509
*/
int endlessLoopingCounter = 0;
while (!stateData->topContext()->lineEmptyContext().isStay()) {
/**
* line empty context switches
*/
if (!d->switchContext(stateData, stateData->topContext()->lineEmptyContext(), QStringList(), newState, isSharedData)) {
/**
* end when trying to #pop the main context
*/
break;
}
if (stateData->topContext()->stopEmptyLineContextSwitchLoop()) {
break;
}
// guard against endless loops
++endlessLoopingCounter;
if (endlessLoopingCounter > 1024) {
qCDebug(Log) << "Endless switch context transitions for line empty context, aborting highlighting of line.";
break;
}
}
auto context = stateData->topContext();
applyFormat(0, 0, context->attributeFormat());
return *defData->unify.insert(newState);
}
auto &dynamicRegexpCache = RepositoryPrivate::get(defData->repo)->m_dynamicRegexpCache;
int offset = 0;
int beginOffset = 0;
bool lineContinuation = false;
/**
* for expensive rules like regexes we do:
* - match them for the complete line, as this is faster than re-trying them at all positions
* - store the result of the first position that matches (or -1 for no match in the full line) in the skipOffsets hash for re-use
* - have capturesForLastDynamicSkipOffset as guard for dynamic regexes to invalidate the cache if they might have changed
*/
QVarLengthArray<QPair<const Rule *, int>, 8> skipOffsets;
QStringList capturesForLastDynamicSkipOffset;
auto getSkipOffsetValue = [&skipOffsets](const Rule *r) -> int {
auto i = std::find_if(skipOffsets.begin(), skipOffsets.end(), [r](const auto &v) {
return v.first == r;
});
if (i == skipOffsets.end())
return 0;
return i->second;
};
auto insertSkipOffset = [&skipOffsets](const Rule *r, int i) {
auto it = std::find_if(skipOffsets.begin(), skipOffsets.end(), [r](const auto &v) {
return v.first == r;
});
if (it == skipOffsets.end()) {
skipOffsets.push_back({r, i});
} else {
it->second = i;
}
};
/**
* current active format
* stored as pointer to avoid deconstruction/constructions inside the internal loop
* the pointers are stable, the formats are either in the contexts or rules
*/
auto currentFormat = &stateData->topContext()->attributeFormat();
/**
* cached first non-space character, needs to be computed if < 0
*/
int firstNonSpace = -1;
int lastOffset = offset;
int endlessLoopingCounter = 0;
do {
/**
* avoid that we loop endless for some broken hl definitions
*/
if (lastOffset == offset) {
++endlessLoopingCounter;
if (endlessLoopingCounter > 1024) {
qCDebug(Log) << "Endless state transitions, aborting highlighting of line.";
break;
}
} else {
// ensure we made progress, clear the endlessLoopingCounter
Q_ASSERT(offset > lastOffset);
lastOffset = offset;
endlessLoopingCounter = 0;
}
/**
* try to match all rules in the context in order of declaration in XML
*/
bool isLookAhead = false;
int newOffset = 0;
const Format *newFormat = nullptr;
for (const auto &ruleShared : stateData->topContext()->rules()) {
auto rule = ruleShared.get();
/**
* filter out rules that require a specific column
*/
if ((rule->requiredColumn() >= 0) && (rule->requiredColumn() != offset)) {
continue;
}
/**
* filter out rules that only match for leading whitespace
*/
if (rule->firstNonSpace()) {
/**
* compute the first non-space lazy
* avoids computing it for contexts without any such rules
*/
if (firstNonSpace < 0) {
firstNonSpace = firstNonSpaceChar(text);
}
/**
* can we skip?
*/
if (offset > firstNonSpace) {
continue;
}
}
int currentSkipOffset = 0;
if (Q_UNLIKELY(rule->hasSkipOffset())) {
/**
* shall we skip application of this rule? two cases:
* - rule can't match at all => currentSkipOffset < 0
* - rule will only match for some higher offset => currentSkipOffset > offset
*
* we need to invalidate this if we are dynamic and have different captures then last time
*/
if (rule->isDynamic() && (capturesForLastDynamicSkipOffset != stateData->topCaptures())) {
skipOffsets.clear();
} else {
currentSkipOffset = getSkipOffsetValue(rule);
if (currentSkipOffset < 0 || currentSkipOffset > offset) {
continue;
}
}
}
auto newResult = rule->doMatch(text, offset, stateData->topCaptures(), dynamicRegexpCache);
newOffset = newResult.offset();
/**
* update skip offset if new one rules out any later match or is larger than current one
*/
if (newResult.skipOffset() < 0 || newResult.skipOffset() > currentSkipOffset) {
insertSkipOffset(rule, newResult.skipOffset());
// remember new captures, if dynamic to enforce proper reset above on change!
if (rule->isDynamic()) {
capturesForLastDynamicSkipOffset = stateData->topCaptures();
}
}
if (newOffset <= offset) {
continue;
}
/**
* apply folding.
* special cases:
* - rule with endRegion + beginRegion: in endRegion, the length is 0
* - rule with lookAhead: length is 0
*/
if (rule->endRegion().isValid() && rule->beginRegion().isValid()) {
applyFolding(offset, 0, rule->endRegion());
} else if (rule->endRegion().isValid()) {
applyFolding(offset, rule->isLookAhead() ? 0 : newOffset - offset, rule->endRegion());
}
if (rule->beginRegion().isValid()) {
applyFolding(offset, rule->isLookAhead() ? 0 : newOffset - offset, rule->beginRegion());
}
if (rule->isLookAhead()) {
Q_ASSERT(!rule->context().isStay());
d->switchContext(stateData, rule->context(), std::move(newResult.captures()), newState, isSharedData);
isLookAhead = true;
break;
}
d->switchContext(stateData, rule->context(), std::move(newResult.captures()), newState, isSharedData);
newFormat = rule->attributeFormat().isValid() ? &rule->attributeFormat() : &stateData->topContext()->attributeFormat();
if (newOffset == text.size() && rule->isLineContinue()) {
lineContinuation = true;
}
break;
}
if (isLookAhead) {
continue;
}
if (newOffset <= offset) { // no matching rule
if (stateData->topContext()->fallthrough()) {
d->switchContext(stateData, stateData->topContext()->fallthroughContext(), QStringList(), newState, isSharedData);
continue;
}
newOffset = offset + 1;
newFormat = &stateData->topContext()->attributeFormat();
}
/**
* if we arrive here, some new format has to be set!
*/
Q_ASSERT(newFormat);
/**
* on format change, apply the last one and switch to new one
*/
if (newFormat != currentFormat && newFormat->id() != currentFormat->id()) {
if (offset > 0) {
applyFormat(beginOffset, offset - beginOffset, *currentFormat);
}
beginOffset = offset;
currentFormat = newFormat;
}
/**
* we must have made progress if we arrive here!
*/
Q_ASSERT(newOffset > offset);
offset = newOffset;
} while (offset < text.size());
/**
* apply format for remaining text, if any
*/
if (beginOffset < offset) {
applyFormat(beginOffset, text.size() - beginOffset, *currentFormat);
}
/**
* handle line end context switches
* guard against endless loops
* see https://phabricator.kde.org/D18509
*/
{
int endlessLoopingCounter = 0;
while (!stateData->topContext()->lineEndContext().isStay() && !lineContinuation) {
if (!d->switchContext(stateData, stateData->topContext()->lineEndContext(), QStringList(), newState, isSharedData)) {
break;
}
// guard against endless loops
++endlessLoopingCounter;
if (endlessLoopingCounter > 1024) {
qCDebug(Log) << "Endless switch context transitions for line end context, aborting highlighting of line.";
break;
}
}
}
return *defData->unify.insert(newState);
}
bool AbstractHighlighterPrivate::switchContext(StateData *&data, const ContextSwitch &contextSwitch, QStringList &&captures, State &state, bool &isSharedData)
{
const auto popCount = contextSwitch.popCount();
const auto context = contextSwitch.context();
if (popCount <= 0 && !context) {
return true;
}
// a modified state must be detached before modification
if (isSharedData) {
data = StateData::detach(state);
isSharedData = false;
}
// kill as many items as requested from the stack, will always keep the initial context alive!
const bool initialContextSurvived = data->pop(popCount);
// if we have a new context to add, push it
// then we always "succeed"
if (context) {
data->push(context, std::move(captures));
return true;
}
// else we abort, if we did try to pop the initial context
return initialContextSurvived;
}
void AbstractHighlighter::applyFolding(int offset, int length, FoldingRegion region)
{
Q_UNUSED(offset);
Q_UNUSED(length);
Q_UNUSED(region);
}
@@ -0,0 +1,169 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_ABSTRACTHIGHLIGHTERM_H
#define KSYNTAXHIGHLIGHTING_ABSTRACTHIGHLIGHTERM_H
#include "definition.h"
#include "ksyntaxhighlighting_export.h"
#include <QObject>
#include <QStringView>
namespace KSyntaxHighlighting
{
class AbstractHighlighterPrivate;
class FoldingRegion;
class Format;
class State;
class Theme;
/**
* Abstract base class for highlighters.
*
* @section abshl_intro Introduction
*
* The AbstractHighlighter provides an interface to highlight text.
*
* The SyntaxHighlighting framework already ships with one implementation,
* namely the SyntaxHighlighter, which also derives from QSyntaxHighlighter,
* meaning that it can be used to highlight a QTextDocument or a QML TextEdit.
* In order to use the SyntaxHighlighter, just call setDefinition() and
* setTheme(), and the associated documents will automatically be highlighted.
*
* However, if you want to use the SyntaxHighlighting framework to implement
* your own syntax highlighter, you need to sublcass from AbstractHighlighter.
*
* @section abshl_impl Implementing your own Syntax Highlighter
*
* In order to implement your own syntax highlighter, you need to inherit from
* AbstractHighlighter. Then, pass each text line that needs to be highlighted
* in order to highlightLine(). Internally, highlightLine() uses the Definition
* initially set through setDefinition() and the State of the previous text line
* to parse and highlight the given text line. For each visual highlighting
* change, highlightLine() will call applyFormat(). Therefore, reimplement
* applyFormat() to get notified of the Format that is valid in the range
* starting at the given offset with the specified length. Similarly, for each
* text part that starts or ends a code folding region, highlightLine() will
* call applyFolding(). Therefore, if you are interested in code folding,
* reimplement applyFolding() to get notified of the starting and ending code
* folding regions, again specified in the range starting at the given offset
* with the given length.
*
* The Format class itself depends on the current Theme. A theme must be
* initially set once such that the Format%s instances can be queried for
* concrete colors.
*
* Optionally, you can also reimplement setTheme() and setDefinition() to get
* notified whenever the Definition or the Theme changes.
*
* @see SyntaxHighlighter
* @since 5.28
*/
class KSYNTAXHIGHLIGHTING_EXPORT AbstractHighlighter
{
public:
virtual ~AbstractHighlighter();
/**
* Returns the syntax definition used for highlighting.
*
* @see setDefinition()
*/
Definition definition() const;
/**
* Sets the syntax definition used for highlighting.
*
* Subclasses can re-implement this method to e.g. trigger
* re-highlighting or clear internal data structures if needed.
*/
virtual void setDefinition(const Definition &def);
/**
* Returns the currently selected theme for highlighting.
*
* @note If no Theme was set through setTheme(), the returned Theme will be
* invalid, see Theme::isValid().
*/
Theme theme() const;
/**
* Sets the theme used for highlighting.
*
* Subclasses can re-implement this method to e.g. trigger
* re-highlighing or to do general palette color setup.
*/
virtual void setTheme(const Theme &theme);
protected:
AbstractHighlighter();
KSYNTAXHIGHLIGHTING_NO_EXPORT explicit AbstractHighlighter(AbstractHighlighterPrivate *dd);
/**
* Highlight the given line. Call this from your derived class
* where appropriate. This will result in any number of applyFormat()
* and applyFolding() calls as a result.
* @param text A string containing the text of the line to highlight.
* @param state The highlighting state handle returned by the call
* to highlightLine() for the previous line. For the very first line,
* just pass a default constructed State().
* @returns The state of the highlighting engine after processing the
* given line. This needs to passed into highlightLine() for the
* next line. You can store the state for efficient partial
* re-highlighting for example during editing.
*
* @see applyFormat(), applyFolding()
*/
State highlightLine(QStringView text, const State &state);
/**
* Reimplement this to apply formats to your output. The provided @p format
* is valid for the interval [@p offset, @p offset + @p length).
*
* @param offset The start column of the interval for which @p format matches
* @param length The length of the matching text
* @param format The Format that applies to the range [offset, offset + length)
*
* @note Make sure to set a valid Definition, otherwise the parameter
* @p format is invalid for the entire line passed to highlightLine()
* (cf. Format::isValid()).
*
* @see applyFolding(), highlightLine()
*/
virtual void applyFormat(int offset, int length, const Format &format) = 0;
/**
* Reimplement this to apply folding to your output. The provided
* FoldingRegion @p region either stars or ends a code folding region in the
* interval [@p offset, @p offset + @p length).
*
* @param offset The start column of the FoldingRegion
* @param length The length of the matching text that starts / ends a
* folding region
* @param region The FoldingRegion that applies to the range [offset, offset + length)
*
* @note The FoldingRegion @p region is @e always either of type
* FoldingRegion::Type::Begin or FoldingRegion::Type::End.
*
* @see applyFormat(), highlightLine(), FoldingRegion
*/
virtual void applyFolding(int offset, int length, FoldingRegion region);
protected:
AbstractHighlighterPrivate *d_ptr;
private:
Q_DECLARE_PRIVATE(AbstractHighlighter)
Q_DISABLE_COPY(AbstractHighlighter)
};
}
QT_BEGIN_NAMESPACE
Q_DECLARE_INTERFACE(KSyntaxHighlighting::AbstractHighlighter, "org.kde.SyntaxHighlighting.AbstractHighlighter")
QT_END_NAMESPACE
#endif // KSYNTAXHIGHLIGHTING_ABSTRACTHIGHLIGHTERM_H
@@ -0,0 +1,34 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_ABSTRACTHIGHLIGHTER_P_H
#define KSYNTAXHIGHLIGHTING_ABSTRACTHIGHLIGHTER_P_H
#include "definition.h"
#include "theme.h"
namespace KSyntaxHighlighting
{
class ContextSwitch;
class StateData;
class State;
class AbstractHighlighterPrivate
{
public:
AbstractHighlighterPrivate();
virtual ~AbstractHighlighterPrivate();
void ensureDefinitionLoaded();
bool switchContext(StateData *&data, const ContextSwitch &contextSwitch, QStringList &&captures, State &state, bool &isSharedData);
Definition m_definition;
Theme m_theme;
};
}
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,69 @@
/*
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_ANSIHIGHLIGHTER_H
#define KSYNTAXHIGHLIGHTING_ANSIHIGHLIGHTER_H
#include "abstracthighlighter.h"
#include "ksyntaxhighlighting_export.h"
#include "theme.h"
#include <QFlags>
#include <QString>
QT_BEGIN_NAMESPACE
class QIODevice;
QT_END_NAMESPACE
namespace KSyntaxHighlighting
{
class AnsiHighlighterPrivate;
// Exported for a bundled helper program
class KSYNTAXHIGHLIGHTING_EXPORT AnsiHighlighter final : public AbstractHighlighter
{
public:
enum class AnsiFormat {
TrueColor,
XTerm256Color,
};
enum class Option {
NoOptions,
UseEditorBackground = 1 << 0,
Unbuffered = 1 << 1,
// Options that displays a useful visual aid for syntax creation
TraceFormat = 1 << 2,
TraceRegion = 1 << 3,
TraceContext = 1 << 4,
TraceStackSize = 1 << 5,
TraceAll = TraceFormat | TraceRegion | TraceContext | TraceStackSize,
};
Q_DECLARE_FLAGS(Options, Option)
AnsiHighlighter();
~AnsiHighlighter() override;
void highlightFile(const QString &fileName, AnsiFormat format = AnsiFormat::TrueColor, Options options = Option::UseEditorBackground);
void highlightData(QIODevice *device, AnsiFormat format = AnsiFormat::TrueColor, Options options = Option::UseEditorBackground);
void setOutputFile(const QString &fileName);
void setOutputFile(FILE *fileHandle);
void setBackgroundRole(Theme::EditorColorRole bgRole);
protected:
void applyFormat(int offset, int length, const Format &format) override;
private:
Q_DECLARE_PRIVATE(AnsiHighlighter)
};
}
Q_DECLARE_OPERATORS_FOR_FLAGS(KSyntaxHighlighting::AnsiHighlighter::Options)
#endif // KSYNTAXHIGHLIGHTING_ANSIHIGHLIGHTER_H
@@ -0,0 +1,131 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#include "context_p.h"
#include "definition_p.h"
#include "format.h"
#include "ksyntaxhighlighting_logging.h"
#include "rule_p.h"
#include "xml_p.h"
#include <QString>
#include <QXmlStreamReader>
using namespace KSyntaxHighlighting;
Context::Context(const DefinitionData &def, const HighlightingContextData &data)
: m_name(data.name)
, m_attributeFormat(data.attribute.isEmpty() ? Format() : def.formatByName(data.attribute))
, m_indentationBasedFolding(!data.noIndentationBasedFolding && def.indentationBasedFolding)
{
if (!data.attribute.isEmpty() && !m_attributeFormat.isValid()) {
qCWarning(Log) << "Context: Unknown format" << data.attribute << "in context" << m_name << "of definition" << def.name;
}
}
bool Context::indentationBasedFoldingEnabled() const
{
return m_indentationBasedFolding;
}
void Context::resolveContexts(DefinitionData &def, const HighlightingContextData &data)
{
m_lineEndContext.resolve(def, data.lineEndContext);
m_lineEmptyContext.resolve(def, data.lineEmptyContext);
m_fallthroughContext.resolve(def, data.fallthroughContext);
m_stopEmptyLineContextSwitchLoop = data.stopEmptyLineContextSwitchLoop;
/**
* line end context switches only when lineEmptyContext is #stay. This avoids
* skipping empty lines after a line continuation character (see bug 405903)
*/
if (m_lineEmptyContext.isStay()) {
m_lineEmptyContext = m_lineEndContext;
}
m_rules.reserve(data.rules.size());
for (const auto &ruleData : data.rules) {
m_rules.push_back(Rule::create(def, ruleData, m_name));
if (!m_rules.back()) {
m_rules.pop_back();
}
}
}
void Context::resolveIncludes(DefinitionData &def)
{
if (m_resolveState == Resolved) {
return;
}
if (m_resolveState == Resolving) {
qCWarning(Log) << "Cyclic dependency!";
return;
}
Q_ASSERT(m_resolveState == Unresolved);
m_resolveState = Resolving; // cycle guard
for (auto it = m_rules.begin(); it != m_rules.end();) {
const IncludeRules *includeRules = it->get()->castToIncludeRules();
if (!includeRules) {
m_hasDynamicRule = m_hasDynamicRule || it->get()->isDynamic();
++it;
continue;
}
const QStringView includeContext = includeRules->contextName();
const qsizetype idx = includeContext.indexOf(QLatin1String("##"));
Context *context = nullptr;
DefinitionData *defData = &def;
if (idx <= -1) { // local include
context = def.contextByName(includeContext);
} else {
const auto definitionName = includeContext.sliced(idx + 2);
const auto contextName = includeContext.sliced(0, idx);
auto resolvedContext = def.resolveIncludedContext(definitionName, contextName);
defData = resolvedContext.def;
context = resolvedContext.context;
if (!defData) {
qCWarning(Log) << "Unable to resolve external include rule for definition" << definitionName << "in" << def.name;
}
}
if (!context) {
qCWarning(Log) << "Unable to resolve include rule for definition" << includeContext << "in" << def.name;
++it;
continue;
}
if (context == this) {
qCWarning(Log) << "Unable to resolve self include rule for definition" << includeContext << "in" << def.name;
++it;
continue;
}
if (context->m_resolveState != Resolved) {
context->resolveIncludes(*defData);
}
m_hasDynamicRule = m_hasDynamicRule || context->m_hasDynamicRule;
/**
* handle included attribute
* transitive closure: we might include attributes included from somewhere else
*/
if (includeRules->includeAttribute()) {
m_attributeFormat = context->m_attributeFormat;
}
it = m_rules.erase(it);
it = m_rules.insert(it, context->rules().begin(), context->rules().end());
it += context->rules().size();
}
m_resolveState = Resolved;
}
@@ -0,0 +1,111 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_CONTEXT_P_H
#define KSYNTAXHIGHLIGHTING_CONTEXT_P_H
#include "contextswitch_p.h"
#include "format.h"
#include "highlightingdata_p.hpp"
#include "rule_p.h"
#include <QString>
#include <vector>
namespace KSyntaxHighlighting
{
class DefinitionData;
class Context
{
public:
Q_DISABLE_COPY(Context)
Context(Context &&) = default;
Context &operator=(Context &&) = default;
Context(const DefinitionData &def, const HighlightingContextData &data);
~Context() = default;
const QString &name() const
{
return m_name;
}
const ContextSwitch &lineEndContext() const
{
return m_lineEndContext;
}
const ContextSwitch &lineEmptyContext() const
{
return m_lineEmptyContext;
}
bool fallthrough() const
{
return !m_fallthroughContext.isStay();
}
bool hasDynamicRule() const
{
return m_hasDynamicRule;
}
bool stopEmptyLineContextSwitchLoop() const
{
return m_stopEmptyLineContextSwitchLoop;
}
const ContextSwitch &fallthroughContext() const
{
return m_fallthroughContext;
}
const Format &attributeFormat() const
{
return m_attributeFormat;
}
const std::vector<Rule::Ptr> &rules() const
{
return m_rules;
}
/**
* Returns @c true, when indentationBasedFolding is enabled for the
* associated Definition and when "noIndentationBasedFolding" is NOT set.
*/
bool indentationBasedFoldingEnabled() const;
void resolveContexts(DefinitionData &def, const HighlightingContextData &data);
void resolveIncludes(DefinitionData &def);
private:
enum ResolveState : quint8 { Unresolved, Resolving, Resolved };
std::vector<Rule::Ptr> m_rules;
QString m_name;
ContextSwitch m_lineEndContext;
ContextSwitch m_lineEmptyContext;
ContextSwitch m_fallthroughContext;
/**
* resolved format for our attribute, done in constructor and resolveIncludes
*/
Format m_attributeFormat;
ResolveState m_resolveState = Unresolved;
bool m_hasDynamicRule = false;
bool m_stopEmptyLineContextSwitchLoop = true;
bool m_indentationBasedFolding;
};
}
#endif // KSYNTAXHIGHLIGHTING_CONTEXT_P_H
@@ -0,0 +1,46 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2024 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#include "contextswitch_p.h"
#include "definition_p.h"
#include "ksyntaxhighlighting_logging.h"
using namespace KSyntaxHighlighting;
void ContextSwitch::resolve(DefinitionData &def, QStringView context)
{
if (context.isEmpty() || context == QStringLiteral("#stay")) {
return;
}
while (context.startsWith(QStringLiteral("#pop"))) {
++m_popCount;
if (context.size() > 4 && context.at(4) == QLatin1Char('!')) {
context = context.sliced(5);
break;
}
context = context.sliced(4);
}
m_isStay = !m_popCount;
if (context.isEmpty()) {
return;
}
const qsizetype defNameIndex = context.indexOf(QStringLiteral("##"));
auto defName = (defNameIndex <= -1) ? QStringView() : context.sliced(defNameIndex + 2);
auto contextName = (defNameIndex <= -1) ? context : context.sliced(0, defNameIndex);
m_context = def.resolveIncludedContext(defName, contextName).context;
if (m_context) {
m_isStay = false;
} else {
qCWarning(Log) << "cannot find context" << contextName << "in" << def.name;
}
}
@@ -0,0 +1,47 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_CONTEXTSWITCH_P_H
#define KSYNTAXHIGHLIGHTING_CONTEXTSWITCH_P_H
#include <QStringView>
namespace KSyntaxHighlighting
{
class Context;
class DefinitionData;
class ContextSwitch
{
public:
ContextSwitch() = default;
~ContextSwitch() = default;
bool isStay() const
{
return m_isStay;
}
int popCount() const
{
return m_popCount;
}
Context *context() const
{
return m_context;
}
void resolve(DefinitionData &def, QStringView context);
private:
Context *m_context = nullptr;
int m_popCount = 0;
bool m_isStay = true;
};
}
#endif // KSYNTAXHIGHLIGHTING_CONTEXTSWITCH_P_H
@@ -0,0 +1,878 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2018 Dominik Haumann <dhaumann@kde.org>
SPDX-FileCopyrightText: 2018 Christoph Cullmann <cullmann@kde.org>
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#include "definition.h"
#include "definition_p.h"
#include "context_p.h"
#include "format.h"
#include "format_p.h"
#include "highlightingdata_p.hpp"
#include "ksyntaxhighlighting_logging.h"
#include "ksyntaxhighlighting_version.h"
#include "repository.h"
#include "repository_p.h"
#include "rule_p.h"
#include "worddelimiters_p.h"
#include "xml_p.h"
#include <QCborMap>
#include <QCoreApplication>
#include <QFile>
#include <QStringTokenizer>
#include <QXmlStreamReader>
#include <algorithm>
#include <atomic>
using namespace KSyntaxHighlighting;
DefinitionData::DefinitionData()
: wordDelimiters()
, wordWrapDelimiters(wordDelimiters)
{
}
DefinitionData::~DefinitionData() = default;
Definition::Definition()
: d(std::make_shared<DefinitionData>())
{
d->q = d;
}
Definition::Definition(Definition &&other) noexcept = default;
Definition::Definition(const Definition &) = default;
Definition::~Definition() = default;
Definition &Definition::operator=(Definition &&other) noexcept = default;
Definition &Definition::operator=(const Definition &) = default;
Definition::Definition(const DefinitionData &defData)
: d(defData.q.lock())
{
}
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;
}
QStringList Definition::alternativeNames() const
{
return d->alternativeNames;
}
QString Definition::translatedName() const
{
if (d->translatedName.isEmpty()) {
d->translatedName = QCoreApplication::instance()->translate("Language", d->nameUtf8.isEmpty() ? d->name.toUtf8().constData() : d->nameUtf8.constData());
}
return d->translatedName;
}
QString Definition::section() const
{
return d->section;
}
QString Definition::translatedSection() const
{
if (d->translatedSection.isEmpty()) {
d->translatedSection = QCoreApplication::instance()->translate("Language Section",
d->sectionUtf8.isEmpty() ? d->section.toUtf8().constData() : d->sectionUtf8.constData());
}
return d->translatedSection;
}
QList<QString> Definition::mimeTypes() const
{
return d->mimetypes;
}
QList<QString> 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->wordDelimiters.contains(c);
}
bool Definition::isWordWrapDelimiter(QChar c) const
{
d->load();
return d->wordWrapDelimiters.contains(c);
}
bool Definition::foldingEnabled() const
{
if (d->foldingRegionsState == DefinitionData::FoldingRegionsState::Undetermined) {
d->load();
}
return d->foldingRegionsState == DefinitionData::FoldingRegionsState::ContainsFoldingRegions || d->indentationBasedFolding;
}
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();
}
bool Definition::setKeywordList(const QString &name, const QStringList &content)
{
d->load(DefinitionData::OnlyKeywords(true));
KeywordList *list = d->keywordList(name);
if (list) {
list->setKeywordList(content);
return true;
} else {
return false;
}
}
QList<Format> Definition::formats() const
{
d->load();
// sort formats so that the order matches the order of the itemDatas in the xml files.
auto formatList = d->formats.values();
std::sort(formatList.begin(), formatList.end(), [](const KSyntaxHighlighting::Format &lhs, const KSyntaxHighlighting::Format &rhs) {
return lhs.id() < rhs.id();
});
return formatList;
}
QList<Definition> Definition::includedDefinitions() const
{
QList<Definition> definitions;
if (isValid()) {
d->load();
// init worklist and result used as guard with this definition
QVarLengthArray<const DefinitionData *, 4> queue{d.get()};
definitions.push_back(*this);
while (!queue.empty()) {
const auto *def = queue.back();
queue.pop_back();
for (const auto *defData : def->immediateIncludedDefinitions) {
auto pred = [defData](const Definition &def) {
return DefinitionData::get(def) == defData;
};
if (std::find_if(definitions.begin(), definitions.end(), pred) == definitions.end()) {
definitions.push_back(Definition(*defData));
queue.push_back(defData);
}
}
}
// remove the 1st entry, since it is this Definition
definitions.front() = std::move(definitions.back());
definitions.pop_back();
}
return definitions;
}
QString Definition::singleLineCommentMarker() const
{
d->load();
return d->singleLineCommentMarker;
}
CommentPosition Definition::singleLineCommentPosition() const
{
d->load();
return d->singleLineCommentPosition;
}
QPair<QString, QString> Definition::multiLineCommentMarker() const
{
d->load();
return {d->multiLineCommentStartMarker, d->multiLineCommentEndMarker};
}
QList<QPair<QChar, QString>> Definition::characterEncodings() const
{
d->load();
return d->characterEncodings;
}
Context *DefinitionData::initialContext()
{
Q_ASSERT(!contexts.empty());
return &contexts.front();
}
Context *DefinitionData::contextByName(QStringView wantedName)
{
for (auto &context : contexts) {
if (context.name() == wantedName) {
return &context;
}
}
return nullptr;
}
KeywordList *DefinitionData::keywordList(const QString &wantedName)
{
auto it = keywordLists.find(wantedName);
return (it == keywordLists.end()) ? nullptr : &it.value();
}
Format DefinitionData::formatByName(QStringView wantedName) const
{
const auto it = formats.constFind(wantedName);
if (it != formats.constEnd()) {
return it.value();
}
return Format();
}
bool DefinitionData::isLoaded() const
{
return !contexts.empty();
}
namespace
{
std::atomic<uint64_t> definitionId{1};
}
bool DefinitionData::load(OnlyKeywords onlyKeywords)
{
if (isLoaded()) {
return true;
}
if (fileName.isEmpty()) {
return false;
}
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 &kw : keywordLists) {
kw.setCaseSensitivity(caseSensitive);
}
resolveContexts();
id = definitionId.fetch_add(1, std::memory_order_relaxed);
return true;
}
void DefinitionData::clear()
{
// keep only name and repo, so we can re-lookup to make references persist over repo reloads
// see AbstractHighlighterPrivate::ensureDefinitionLoaded()
id = 0;
keywordLists.clear();
contexts.clear();
formats.clear();
contextDatas.clear();
immediateIncludedDefinitions.clear();
wordDelimiters = WordDelimiters();
wordWrapDelimiters = wordDelimiters;
keywordIsLoaded = false;
foldingRegionsState = FoldingRegionsState::Undetermined;
indentationBasedFolding = false;
foldingIgnoreList.clear();
singleLineCommentMarker.clear();
singleLineCommentPosition = CommentPosition::StartOfLine;
multiLineCommentStartMarker.clear();
multiLineCommentEndMarker.clear();
characterEncodings.clear();
fileName.clear();
nameUtf8.clear();
translatedName.clear();
section.clear();
sectionUtf8.clear();
translatedSection.clear();
style.clear();
indenter.clear();
author.clear();
license.clear();
mimetypes.clear();
extensions.clear();
caseSensitive = Qt::CaseSensitive;
version = 0.0f;
priority = 0;
hidden = false;
// purge our cache that is used to unify states
unify.clear();
}
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 QCborMap &obj)
{
name = obj.value(QLatin1String("name")).toString();
if (name.isEmpty()) {
return false;
}
nameUtf8 = obj.value(QLatin1String("name")).toByteArray();
section = obj.value(QLatin1String("section")).toString();
sectionUtf8 = obj.value(QLatin1String("section")).toByteArray();
version = obj.value(QLatin1String("version")).toInteger();
priority = obj.value(QLatin1String("priority")).toInteger();
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 names = obj.value(QLatin1String("alternativeNames")).toString();
alternativeNames = names.split(QLatin1Char(';'), Qt::SkipEmptyParts);
const auto exts = obj.value(QLatin1String("extensions")).toString();
extensions = exts.split(QLatin1Char(';'), Qt::SkipEmptyParts);
const auto mts = obj.value(QLatin1String("mimetype")).toString();
mimetypes = mts.split(QLatin1Char(';'), Qt::SkipEmptyParts);
return true;
}
bool DefinitionData::loadLanguage(QXmlStreamReader &reader)
{
Q_ASSERT(reader.name() == QLatin1String("language"));
Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement);
if (!checkKateVersion(reader.attributes().value(QLatin1String("kateversion")))) {
return false;
}
name = reader.attributes().value(QLatin1String("name")).toString();
if (name.isEmpty()) {
return false;
}
const auto names = reader.attributes().value(QLatin1String("alternativeNames"));
for (const auto &n : QStringTokenizer(names, u';', Qt::SkipEmptyParts)) {
alternativeNames.push_back(n.toString());
}
section = reader.attributes().value(QLatin1String("section")).toString();
// toFloat instead of toInt for backward compatibility with old Kate files
version = reader.attributes().value(QLatin1String("version")).toFloat();
priority = reader.attributes().value(QLatin1String("priority")).toInt();
hidden = Xml::attrToBool(reader.attributes().value(QLatin1String("hidden")));
style = reader.attributes().value(QLatin1String("style")).toString();
indenter = reader.attributes().value(QLatin1String("indenter")).toString();
author = reader.attributes().value(QLatin1String("author")).toString();
license = reader.attributes().value(QLatin1String("license")).toString();
const auto exts = reader.attributes().value(QLatin1String("extensions"));
for (const auto &ext : QStringTokenizer(exts, u';', Qt::SkipEmptyParts)) {
extensions.push_back(ext.toString());
}
const auto mts = reader.attributes().value(QLatin1String("mimetype"));
for (const auto &mt : QStringTokenizer(mts, u';', Qt::SkipEmptyParts)) {
mimetypes.push_back(mt.toString());
}
if (reader.attributes().hasAttribute(QLatin1String("casesensitive"))) {
caseSensitive = Xml::attrToBool(reader.attributes().value(QLatin1String("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 </list>
}
} 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 &kw : keywordLists) {
kw.resolveIncludeKeywords(*this);
}
}
void DefinitionData::loadContexts(QXmlStreamReader &reader)
{
Q_ASSERT(reader.name() == QLatin1String("contexts"));
Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement);
contextDatas.reserve(32);
while (!reader.atEnd()) {
switch (reader.tokenType()) {
case QXmlStreamReader::StartElement:
if (reader.name() == QLatin1String("context")) {
contextDatas.emplace_back().load(name, reader);
}
reader.readNext();
break;
case QXmlStreamReader::EndElement:
return;
default:
reader.readNext();
break;
}
}
}
void DefinitionData::resolveContexts()
{
contexts.reserve(contextDatas.size());
/**
* Transform all HighlightingContextData to Context.
* This is necessary so that Context::resolveContexts() can find the referenced contexts.
*/
for (const auto &contextData : std::as_const(contextDatas)) {
contexts.emplace_back(*this, contextData);
}
/**
* Resolves contexts and rules.
*/
auto ctxIt = contexts.begin();
for (const auto &contextData : std::as_const(contextDatas)) {
ctxIt->resolveContexts(*this, contextData);
++ctxIt;
}
/**
* To free the memory, constDatas is emptied because it is no longer used.
*/
contextDatas.clear();
contextDatas.shrink_to_fit();
/**
* Resolved includeRules.
*/
for (auto &context : contexts) {
context.resolveIncludes(*this);
}
// when a context includes a folding region this value is Yes, otherwise it remains undetermined
if (foldingRegionsState == FoldingRegionsState::Undetermined) {
foldingRegionsState = FoldingRegionsState::NoFoldingRegions;
}
}
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->definitionName = name;
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(QLatin1String("casesensitive"))) {
caseSensitive = Xml::attrToBool(reader.attributes().value(QLatin1String("casesensitive"))) ? Qt::CaseSensitive : Qt::CaseInsensitive;
}
// adapt wordDelimiters
wordDelimiters.append(reader.attributes().value(QLatin1String("additionalDeliminator")));
wordDelimiters.remove(reader.attributes().value(QLatin1String("weakDeliminator")));
// adapt WordWrapDelimiters
auto wordWrapDeliminatorAttr = reader.attributes().value(QLatin1String("wordWrapDeliminator"));
if (wordWrapDeliminatorAttr.isEmpty()) {
wordWrapDelimiters = wordDelimiters;
} else {
wordWrapDelimiters.append(wordWrapDeliminatorAttr);
}
} else if (reader.name() == QLatin1String("folding")) {
if (reader.attributes().hasAttribute(QLatin1String("indentationsensitive"))) {
indentationBasedFolding = Xml::attrToBool(reader.attributes().value(QLatin1String("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(QLatin1String("name")) == QLatin1String("singleLine");
if (isSingleLine) {
singleLineCommentMarker = reader.attributes().value(QLatin1String("start")).toString();
const bool afterWhiteSpace = reader.attributes().value(QLatin1String("position")) == QLatin1String("afterwhitespace");
singleLineCommentPosition = afterWhiteSpace ? CommentPosition::AfterWhitespace : CommentPosition::StartOfLine;
} else {
multiLineCommentStartMarker = reader.attributes().value(QLatin1String("start")).toString();
multiLineCommentEndMarker = reader.attributes().value(QLatin1String("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(QLatin1String("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(QLatin1String("char"));
if (!charRef.isEmpty()) {
const auto str = reader.attributes().value(QLatin1String("string"));
characterEncodings.push_back({charRef[0], str.toString()});
}
}
reader.readNext();
break;
case QXmlStreamReader::EndElement:
--elementRefCounter;
if (elementRefCounter == 0) {
return;
}
reader.readNext();
break;
default:
reader.readNext();
break;
}
}
}
bool DefinitionData::checkKateVersion(QStringView 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.sliced(0, idx).toInt();
const auto minor = verStr.sliced(idx + 1).toInt();
if (major > KSYNTAXHIGHLIGHTING_VERSION_MAJOR || (major == KSYNTAXHIGHLIGHTING_VERSION_MAJOR && minor > KSYNTAXHIGHLIGHTING_VERSION_MINOR)) {
qCWarning(Log) << "Skipping" << fileName << "due to being too new, version:" << verStr;
return false;
}
return true;
}
quint16 DefinitionData::foldingRegionId(const QString &foldName)
{
foldingRegionsState = FoldingRegionsState::ContainsFoldingRegions;
return RepositoryPrivate::get(repo)->foldingRegionId(name, foldName);
}
DefinitionData::ResolvedContext DefinitionData::resolveIncludedContext(QStringView defName, QStringView contextName)
{
if (defName.isEmpty()) {
return {this, contextByName(contextName)};
}
auto d = repo->definitionForName(defName.toString());
if (d.isValid()) {
auto *resolvedDef = get(d);
if (resolvedDef != this) {
if (std::find(immediateIncludedDefinitions.begin(), immediateIncludedDefinitions.end(), resolvedDef) == immediateIncludedDefinitions.end()) {
immediateIncludedDefinitions.push_back(resolvedDef);
resolvedDef->load();
if (resolvedDef->foldingRegionsState == FoldingRegionsState::ContainsFoldingRegions) {
foldingRegionsState = FoldingRegionsState::ContainsFoldingRegions;
}
}
}
if (contextName.isEmpty()) {
return {resolvedDef, resolvedDef->initialContext()};
} else {
return {resolvedDef, resolvedDef->contextByName(contextName)};
}
}
return {nullptr, nullptr};
}
#include "moc_definition.cpp"
@@ -0,0 +1,424 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_DEFINITION_H
#define KSYNTAXHIGHLIGHTING_DEFINITION_H
#include "ksyntaxhighlighting_export.h"
#include <QList>
#include <QPair>
#include <QString>
#include <memory>
#include <qobjectdefs.h>
namespace KSyntaxHighlighting
{
class Context;
class Format;
class KeywordList;
class DefinitionData;
/**
* Defines the insert position when commenting code.
* @since 5.50
* @see Definition::singleLineCommentPosition()
*/
enum class CommentPosition {
//! The comment marker is inserted at the beginning of a line at column 0
StartOfLine = 0,
//! The comment marker is inserted after leading whitespaces right befire
//! the first non-whitespace character.
AfterWhitespace
};
/**
* 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 Header Data
*
* 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.
*
* @section def_metadata Advanced Definition Data
*
* Advanced text editors such as Kate require additional information from a
* Definition. For instance, foldingEnabled() defines whether a Definition has
* code folding regions that can be shown in a code folding pane. Or
* singleLineCommentMarker() and multiLineCommentMarker() provide comment
* markers that can be used for commenting/uncommenting code. Similarly,
* formats() returns a list of Format items defined by this Definition (which
* equal the itemDatas of a highlighting definition file). includedDefinitions()
* returns a list of all included Definition%s referenced by this Definition via
* the rule IncludeRules, which is useful for displaying all Format items for
* color configuration in the user interface.
*
* @see Repository
* @since 5.28
*/
class KSYNTAXHIGHLIGHTING_EXPORT Definition
{
Q_GADGET
Q_PROPERTY(QString name READ name)
Q_PROPERTY(QString translatedName READ translatedName)
Q_PROPERTY(QString section READ section)
Q_PROPERTY(QString translatedSection READ translatedSection)
Q_PROPERTY(QString author READ author)
Q_PROPERTY(QString license READ license)
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();
/**
* Move constructor.
* This definition takes the Definition data from @p other.
* @note @p other may only be assigned to or destroyed afterwards.
* @since 5.86
*/
Definition(Definition &&other) noexcept;
/**
* Copy constructor.
* Both this definition as well as @p other share the Definition data.
*/
Definition(const Definition &other);
/**
* Destructor.
*/
~Definition();
/**
* Move assignment operator.
* This definition takes the Definition data from @p other.
* @note @p other may only be assigned to or destroyed afterwards.
* @since 5.86
*/
Definition &operator=(Definition &&other) noexcept;
/**
* Copy 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;
/**
* @name General Header Data
*
* @{
*/
/**
* 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;
/**
* Alternate names the syntax can be referred to by.
*
* @since 6.1
*/
QStringList alternativeNames() 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.
*/
QList<QString> mimeTypes() const;
/**
* File extensions associated with this syntax definition.
* The returned list contains wildcards.
*/
QList<QString> 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;
/**
* @}
*
* @name Advanced Definition Data
*/
/**
* 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
* @see isWordWrapDelimiter()
*/
bool isWordDelimiter(QChar c) const;
/**
* Returns whether it is safe to break a line at before the character @c.
* This is useful when wrapping a line e.g. by applying static word wrap.
*
* As example, consider the LaTeX code
* @code
* \command1\command2
* @endcode
* Applying static word wrap could lead to the following code:
* @code
* \command1\
* command2
* @endcode
* command2 without a leading backslash is invalid in LaTeX. If '\\' is set
* as word wrap delimiter, isWordWrapDelimiter('\\') then returns true,
* meaning that it is safe to break the line before @c. The resulting code
* then would be
* @code
* \command1
* \command2
* @endcode
*
* @note By default, the word wrap delimiters are equal to the word
* delimiters in isWordDelimiter().
*
* @since 5.50
* @see isWordDelimiter()
*/
bool isWordWrapDelimiter(QChar c) const;
/**
* Returns whether the highlighting supports code folding.
* Code folding is supported either if the highlighting defines code folding
* regions or if indentationBasedFoldingEnabled() returns @e true.
* @since 5.50
* @see indentationBasedFoldingEnabled()
*/
bool foldingEnabled() 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(), setKeywordList()
*/
QStringList keywordList(const QString &name) const;
/**
* Set the contents of the keyword list @p name to @p content.
* Only existing keywordLists() can be changed. For non-existent keyword lists,
* false is returned.
*
* Whenever you change a keyword list, make sure to trigger a rehighlight of
* your documents. In case you are using QSyntaxHighlighter via SyntaxHighlighter,
* this can be done by calling SyntaxHighlighter::rehighlight().
*
* @note In general, changing keyword lists via setKeywordList() is discouraged,
* since if a keyword list name in the syntax highlighting definition
* file changes, the call setKeywordList() may suddenly fail.
*
* @see keywordList(), keywordLists()
* @since 5.62
*/
bool setKeywordList(const QString &name, const QStringList &content);
/**
* 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
*/
QList<Format> 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
*/
QList<Definition> includedDefinitions() const;
/**
* Returns the marker that starts a single line comment.
* For instance, in C++ the single line comment marker is "//".
* @since 5.50
* @see singleLineCommentPosition();
*/
QString singleLineCommentMarker() const;
/**
* Returns the insert position of the comment marker for sinle line
* comments.
* @since 5.50
* @see singleLineCommentMarker();
*/
CommentPosition singleLineCommentPosition() const;
/**
* Returns the markers that start and end multiline comments.
* For instance, in XML this is defined as "<!--" and "-->".
* @since 5.50
*/
QPair<QString, QString> multiLineCommentMarker() const;
/**
* Returns a list of character/string mapping that can be used for spell
* checking. This is useful for instance when spell checking LaTeX, where
* the string \"{A} represents the character Ä.
* @since 5.50
*/
QList<QPair<QChar, QString>> characterEncodings() const;
/**
* @}
*/
private:
friend class DefinitionData;
KSYNTAXHIGHLIGHTING_NO_EXPORT explicit Definition(const DefinitionData &defData);
std::shared_ptr<DefinitionData> d;
};
}
QT_BEGIN_NAMESPACE
Q_DECLARE_TYPEINFO(KSyntaxHighlighting::Definition, Q_RELOCATABLE_TYPE);
QT_END_NAMESPACE
#endif
@@ -0,0 +1,139 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_DEFINITION_P_H
#define KSYNTAXHIGHLIGHTING_DEFINITION_P_H
#include "definition.h"
#include "highlightingdata_p.hpp"
#include "state.h"
#include "worddelimiters_p.h"
#include <QHash>
#include <QList>
#include <QSet>
#include <QString>
#include <QVarLengthArray>
#include <vector>
QT_BEGIN_NAMESPACE
class QCborMap;
class QXmlStreamReader;
QT_END_NAMESPACE
namespace KSyntaxHighlighting
{
class Repository;
class DefinitionData
{
public:
DefinitionData();
~DefinitionData();
DefinitionData(const DefinitionData &) = delete;
DefinitionData &operator=(const DefinitionData &) = delete;
static DefinitionData *get(const Definition &def)
{
return def.d.get();
}
bool isLoaded() const;
bool loadMetaData(const QString &definitionFileName);
bool loadMetaData(const QString &fileName, const QCborMap &obj);
void clear();
enum class OnlyKeywords : bool;
bool load(OnlyKeywords onlyKeywords = OnlyKeywords(false));
bool loadLanguage(QXmlStreamReader &reader);
void loadHighlighting(QXmlStreamReader &reader, OnlyKeywords onlyKeywords);
void loadContexts(QXmlStreamReader &reader);
void loadItemData(QXmlStreamReader &reader);
void loadGeneral(QXmlStreamReader &reader);
void loadComments(QXmlStreamReader &reader);
void loadFoldingIgnoreList(QXmlStreamReader &reader);
void loadSpellchecking(QXmlStreamReader &reader);
bool checkKateVersion(QStringView verStr);
void resolveContexts();
void resolveIncludeKeywords();
KeywordList *keywordList(const QString &name);
Context *initialContext();
Context *contextByName(QStringView name);
Format formatByName(QStringView name) const;
quint16 foldingRegionId(const QString &foldName);
struct ResolvedContext {
DefinitionData *def;
Context *context;
};
ResolvedContext resolveIncludedContext(QStringView defName, QStringView contextName);
enum class FoldingRegionsState : uint8_t {
Undetermined,
ContainsFoldingRegions,
NoFoldingRegions,
};
std::weak_ptr<DefinitionData> q;
uint64_t id = 0;
Repository *repo = nullptr;
QHash<QString, KeywordList> keywordLists;
std::vector<Context> contexts;
QHash<QStringView, Format> formats;
// data loaded from xml file and emptied after loading contexts
std::vector<HighlightingContextData> contextDatas;
// Definition referenced by IncludeRules and ContextSwitch
QVarLengthArray<const DefinitionData *, 4> immediateIncludedDefinitions;
WordDelimiters wordDelimiters;
WordDelimiters wordWrapDelimiters;
bool keywordIsLoaded = false;
FoldingRegionsState foldingRegionsState = FoldingRegionsState::Undetermined;
bool indentationBasedFolding = false;
QStringList foldingIgnoreList;
QString singleLineCommentMarker;
CommentPosition singleLineCommentPosition = CommentPosition::StartOfLine;
QString multiLineCommentStartMarker;
QString multiLineCommentEndMarker;
QList<QPair<QChar, QString>> characterEncodings;
QString fileName;
QString name = QStringLiteral(QT_TRANSLATE_NOOP("Language", "None"));
QStringList alternativeNames;
QByteArray nameUtf8;
mutable QString translatedName;
QString section;
QByteArray sectionUtf8;
mutable QString translatedSection;
QString style;
QString indenter;
QString author;
QString license;
QList<QString> mimetypes;
QList<QString> extensions;
Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive;
int version = 0;
int priority = 0;
bool hidden = false;
// cache that is used to unify states in AbstractHighlighter::highlightLine
mutable QSet<State> unify;
};
}
#endif
@@ -0,0 +1,182 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#include "definitiondownloader.h"
#include "definition.h"
#include "ksyntaxhighlighting_logging.h"
#include "ksyntaxhighlighting_version.h"
#include "repository.h"
#include <QDir>
#include <QFile>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QStandardPaths>
#include <QTimer>
#include <QXmlStreamReader>
using namespace KSyntaxHighlighting;
class KSyntaxHighlighting::DefinitionDownloaderPrivate
{
public:
DefinitionDownloader *q;
Repository *repo;
QNetworkAccessManager *nam;
QString downloadLocation;
int pendingDownloads;
bool needsReload;
void definitionListDownloadFinished(QNetworkReply *reply);
void updateDefinition(QXmlStreamReader &parser);
void downloadDefinition(const QUrl &url);
void downloadDefinitionFinished(QNetworkReply *reply);
void checkDone();
};
void DefinitionDownloaderPrivate::definitionListDownloadFinished(QNetworkReply *reply)
{
const auto networkError = reply->error();
if (networkError != QNetworkReply::NoError) {
qCWarning(Log) << networkError;
Q_EMIT q->done(); // TODO return error
return;
}
QXmlStreamReader parser(reply);
while (!parser.atEnd()) {
switch (parser.readNext()) {
case QXmlStreamReader::StartElement:
if (parser.name() == QLatin1String("Definition")) {
updateDefinition(parser);
}
break;
default:
break;
}
}
if (pendingDownloads == 0) {
Q_EMIT q->informationMessage(QObject::tr("All syntax definitions are up-to-date."));
}
checkDone();
}
void DefinitionDownloaderPrivate::updateDefinition(QXmlStreamReader &parser)
{
const auto name = parser.attributes().value(QLatin1String("name"));
if (name.isEmpty()) {
return;
}
auto localDef = repo->definitionForName(name.toString());
if (!localDef.isValid()) {
Q_EMIT q->informationMessage(QObject::tr("Downloading new syntax definition for '%1'…", "@info").arg(name));
downloadDefinition(QUrl(parser.attributes().value(QLatin1String("url")).toString()));
return;
}
const auto version = parser.attributes().value(QLatin1String("version"));
if (localDef.version() < version.toFloat()) {
Q_EMIT q->informationMessage(QObject::tr("Updating syntax definition for '%1' to version %2…", "@info").arg(name, version));
downloadDefinition(QUrl(parser.attributes().value(QLatin1String("url")).toString()));
}
}
void DefinitionDownloaderPrivate::downloadDefinition(const QUrl &downloadUrl)
{
if (!downloadUrl.isValid()) {
return;
}
auto url = downloadUrl;
if (url.scheme() == QLatin1String("http")) {
url.setScheme(QStringLiteral("https"));
}
QNetworkRequest req(url);
auto reply = nam->get(req);
QObject::connect(reply, &QNetworkReply::finished, q, [this, reply]() {
downloadDefinitionFinished(reply);
});
++pendingDownloads;
needsReload = true;
}
void DefinitionDownloaderPrivate::downloadDefinitionFinished(QNetworkReply *reply)
{
--pendingDownloads;
const auto networkError = reply->error();
if (networkError != QNetworkReply::NoError) {
qCWarning(Log) << "Failed to download definition file" << reply->url() << networkError;
checkDone();
return;
}
// handle redirects
// needs to be done manually, download server redirects to unsafe http links
const auto redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
if (!redirectUrl.isEmpty()) {
downloadDefinition(reply->url().resolved(redirectUrl));
checkDone();
return;
}
QFile file(downloadLocation + QLatin1Char('/') + reply->url().fileName());
if (!file.open(QFile::WriteOnly)) {
qCWarning(Log) << "Failed to open" << file.fileName() << file.error();
} else {
file.write(reply->readAll());
}
checkDone();
}
void DefinitionDownloaderPrivate::checkDone()
{
if (pendingDownloads == 0) {
if (needsReload) {
repo->reload();
}
Q_EMIT QTimer::singleShot(0, q, &DefinitionDownloader::done);
}
}
DefinitionDownloader::DefinitionDownloader(Repository *repo, QObject *parent)
: QObject(parent)
, d(new DefinitionDownloaderPrivate())
{
Q_ASSERT(repo);
d->q = this;
d->repo = repo;
d->nam = new QNetworkAccessManager(this);
d->pendingDownloads = 0;
d->needsReload = false;
d->downloadLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/org.kde.syntax-highlighting/syntax");
QDir().mkpath(d->downloadLocation);
Q_ASSERT(QFile::exists(d->downloadLocation));
}
DefinitionDownloader::~DefinitionDownloader()
{
}
void DefinitionDownloader::start()
{
const QString url = QLatin1String("https://www.kate-editor.org/syntax/update-") + QString::number(KSYNTAXHIGHLIGHTING_VERSION_MAJOR) + QLatin1Char('.')
+ QString::number(KSYNTAXHIGHLIGHTING_VERSION_MINOR) + QLatin1String(".xml");
auto req = QNetworkRequest(QUrl(url));
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
auto reply = d->nam->get(req);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() {
d->definitionListDownloadFinished(reply);
});
}
#include "moc_definitiondownloader.cpp"
@@ -0,0 +1,96 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_DEFINITIONDOWNLOADER_H
#define KSYNTAXHIGHLIGHTING_DEFINITIONDOWNLOADER_H
#include "ksyntaxhighlighting_export.h"
#include <QObject>
#include <memory>
namespace KSyntaxHighlighting
{
class DefinitionDownloaderPrivate;
class Repository;
/**
* Helper class to download definition file updates.
*
* With the DefinitionDownloader you can download new and update existing
* syntax highlighting definition files (xml files).
*
* An example that updates the highlighting Definition%s and prints the current
* update progress to the console may look as follows:
*
* @code
* auto downloader = new DefinitionDownloader(repo); // repo is a pointer to a Repository
*
* // print update progress to console
* QObject::connect(downloader, &DefinitionDownloader::informationMessage, [](const QString &msg) {
* std::cout << qPrintable(msg) << std::endl;
* });
*
* // connect to signal done to delete the downloader later
* QObject::connect(downloader, &DefinitionDownloader::done,
* downloader, &DefinitionDownloader::deleteLater);
* downloader->start();
* @endcode
*
* @see Repository, Definition
* @since 5.28
*/
class KSYNTAXHIGHLIGHTING_EXPORT DefinitionDownloader : public QObject
{
Q_OBJECT
public:
/**
* Constructor.
* The Repository @p repo is used as reference to compare the versions of
* the existing Definition%s with the ones that are available online.
*
* Optionally, @p parent is a pointer to the owner of this instance.
*/
explicit DefinitionDownloader(Repository *repo, QObject *parent = nullptr);
/**
* Destructor.
*/
~DefinitionDownloader() override;
/**
* Starts the update procedure.
* Once no more updates are available (i.e. either the local definition files
* are up-to-date, or all updates have been downloaded), the signal done()
* is emitted.
*
* During the update process, the signal informationMessage() can be used
* to display the current update progress to the user.
*
* @see done(), informationMessage()
*/
void start();
Q_SIGNALS:
/**
* Prints the information about the current state of the definition files.
* If all files are up-to-date, this signal is emitted informing you that
* all highlighting files are up-to-date. If there are updates, this signal
* is emitted for each update being downloaded.
*/
void informationMessage(const QString &msg);
/**
* This signal is emitted when there are no pending downloads anymore.
*/
void done();
private:
std::unique_ptr<DefinitionDownloaderPrivate> d;
};
}
#endif // KSYNTAXHIGHLIGHTING_DEFINITIONDOWNLOADER_H
@@ -0,0 +1,39 @@
/*
SPDX-FileCopyrightText: 2023 Jonathan Poelen <jonathan.poelen+kde@gmail.com>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_DYNAMICREGEXPCACHE_P_H
#define KSYNTAXHIGHLIGHTING_DYNAMICREGEXPCACHE_P_H
#include <QCache>
#include <QRegularExpression>
#include <QString>
#include <utility>
namespace KSyntaxHighlighting
{
class DynamicRegexpCache
{
public:
const QRegularExpression &compileRegexp(QString &&pattern, QRegularExpression::PatternOptions patternOptions)
{
const auto key = std::pair{std::move(pattern), patternOptions};
if (const auto regexp = m_cache.object(key)) {
return *regexp;
}
auto regexp = new QRegularExpression(key.first, patternOptions);
m_cache.insert(key, regexp);
return *regexp;
}
private:
QCache<std::pair<QString, QRegularExpression::PatternOptions>, QRegularExpression> m_cache;
};
}
#endif
@@ -0,0 +1,46 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#include "foldingregion.h"
using namespace KSyntaxHighlighting;
static_assert(sizeof(FoldingRegion) == sizeof(int), "FoldingRegion is size-sensitive to frequent use in KTextEditor!");
FoldingRegion::FoldingRegion() = default;
FoldingRegion::FoldingRegion(Type type, int id)
: m_idWithType((type == End) ? -id : id)
{
}
bool FoldingRegion::operator==(const FoldingRegion &other) const
{
return m_idWithType == other.m_idWithType;
}
bool FoldingRegion::isValid() const
{
return m_idWithType != 0;
}
int FoldingRegion::id() const
{
return (m_idWithType < 0) ? -m_idWithType : m_idWithType;
}
FoldingRegion::Type FoldingRegion::type() const
{
if (isValid()) {
return (m_idWithType < 0) ? End : Begin;
}
return None;
}
FoldingRegion FoldingRegion::sibling() const
{
return isValid() ? FoldingRegion(type() ? End : Begin, id()) : FoldingRegion();
}
@@ -0,0 +1,100 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_FOLDINGREGION_H
#define KSYNTAXHIGHLIGHTING_FOLDINGREGION_H
#include "ksyntaxhighlighting_export.h"
#include <QTypeInfo>
namespace KSyntaxHighlighting
{
/** Represents a begin or end of a folding region.
* @since 5.28 */
class KSYNTAXHIGHLIGHTING_EXPORT FoldingRegion
{
public:
/**
* Defines whether a FoldingRegion starts or ends a folding region.
*/
enum Type {
//! Used internally as indicator for an invalid FoldingRegion.
None,
//! Indicates the start of a FoldingRegion.
Begin,
//! Indicates the end of a FoldingRegion.
End
};
/**
* Constructs an invalid folding region, meaning that isValid() returns @e false.
* To obtain valid instances, see AbstractHighlighter::applyFolding().
*/
FoldingRegion();
/** Compares two FoldingRegion instances for equality. */
bool operator==(const FoldingRegion &other) const;
/**
* Returns @c true if this is a valid folding region.
* A valid FoldingRegion is defined by a type() other than Type::None.
*
* @note The FoldingRegion%s passed in AbstractHighlighter::applyFolding()
* are always valid.
*/
bool isValid() const;
/**
* Returns a unique identifier for this folding region.
*
* As example, the C/C++ highlighter starts and ends a folding region for
* scopes, e.g.:
* \code
* void foo() { // '{' starts a folding region
* if (bar()) { // '{' starts a (nested) folding region
* } // '}' ends the (nested) folding region
* } // '}' ends the outer folding region
* \endcode
* In this example, all braces '{' and '}' have the same id(), meaning that
* if you want to find the matching closing region for the first opening
* brace, you need to do kind of a reference counting to find the correct
* closing brace.
*/
int id() const;
/**
* Returns whether this is the begin or end of a region.
*
* @note The FoldingRegion%s passed in AbstractHighlighter::applyFolding()
* are always valid, i.e. either Type::Begin or Type::End.
*/
Type type() const;
/**
* Returns the matching start or end region.
*
* @note Will return invalid region for an invalid region.
*
* @since 6.0
*/
FoldingRegion sibling() const;
private:
friend class Rule;
KSYNTAXHIGHLIGHTING_NO_EXPORT FoldingRegion(Type type, int id);
// 0 is invalid, positive begin, negative end
int m_idWithType = 0;
};
}
QT_BEGIN_NAMESPACE
Q_DECLARE_TYPEINFO(KSyntaxHighlighting::FoldingRegion, Q_PRIMITIVE_TYPE);
QT_END_NAMESPACE
#endif
@@ -0,0 +1,283 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#include "format.h"
#include "definition.h"
#include "format_p.h"
#include "textstyledata_p.h"
#include "themedata_p.h"
#include "xml_p.h"
#include <QColor>
#include <QMetaEnum>
#include <QXmlStreamReader>
using namespace KSyntaxHighlighting;
static Theme::TextStyle stringToDefaultFormat(QStringView str)
{
if (!str.startsWith(QLatin1String("ds"))) {
return Theme::Normal;
}
const auto metaEnum = QMetaEnum::fromType<Theme::TextStyle>();
bool ok = false;
const auto value = metaEnum.keyToValue(str.sliced(2).toLatin1().constData(), &ok);
if (!ok || value < 0) {
return Theme::Normal;
}
return static_cast<Theme::TextStyle>(value);
}
FormatPrivate *FormatPrivate::detachAndGet(Format &format)
{
format.d.detach();
return format.d.data();
}
TextStyleData FormatPrivate::styleOverride(const Theme &theme) const
{
return ThemeData::get(theme)->textStyleOverride(definitionName, name);
}
QColor FormatPrivate::color(const Theme &theme, StyleColor styleColor, ThemeColor themeColor) const
{
const auto overrideStyle = styleOverride(theme);
if (overrideStyle.*styleColor) {
return QColor::fromRgb(overrideStyle.*styleColor);
}
// use QColor::fromRgba for QRgb => QColor conversion to avoid unset colors == black!
return QColor::fromRgba(style.*styleColor ? style.*styleColor : (theme.*themeColor)(defaultStyle));
}
bool FormatPrivate::hasColor(const Theme &theme, StyleColor styleColor, ThemeColor themeColor) const
{
// use QColor::fromRgba for background QRgb => QColor conversion to avoid unset colors == black!
return color(theme, styleColor, themeColor) != QColor::fromRgba((theme.*themeColor)(Theme::Normal)) && (style.*styleColor || (theme.*themeColor)(defaultStyle) || styleOverride(theme).*styleColor);
}
static QExplicitlySharedDataPointer<FormatPrivate> &sharedDefaultPrivate()
{
static QExplicitlySharedDataPointer<FormatPrivate> def(new FormatPrivate);
return def;
}
Format::Format()
: d(sharedDefaultPrivate())
{
}
Format::Format(const Format &other)
: d(other.d)
{
}
Format::~Format()
{
}
Format &Format::operator=(const Format &other)
{
d = other.d;
return *this;
}
bool Format::isValid() const
{
return !d->name.isEmpty();
}
QString Format::name() const
{
return d->name;
}
int Format::id() const
{
return d->id;
}
Theme::TextStyle Format::textStyle() const
{
return d->defaultStyle;
}
bool Format::isDefaultTextStyle(const Theme &theme) const
{
// use QColor::fromRgba for background QRgb => QColor conversion to avoid unset colors == black!
return (!hasTextColor(theme)) && (!hasBackgroundColor(theme)) && (selectedTextColor(theme).rgba() == theme.selectedTextColor(Theme::Normal))
&& (selectedBackgroundColor(theme).rgba() == (theme.selectedBackgroundColor(Theme::Normal))) && (isBold(theme) == theme.isBold(Theme::Normal))
&& (isItalic(theme) == theme.isItalic(Theme::Normal)) && (isUnderline(theme) == theme.isUnderline(Theme::Normal))
&& (isStrikeThrough(theme) == theme.isStrikeThrough(Theme::Normal));
}
bool Format::hasTextColor(const Theme &theme) const
{
return d->hasColor(theme, &TextStyleData::textColor, &Theme::textColor);
}
QColor Format::textColor(const Theme &theme) const
{
return d->color(theme, &TextStyleData::textColor, &Theme::textColor);
}
QColor Format::selectedTextColor(const Theme &theme) const
{
return d->color(theme, &TextStyleData::selectedTextColor, &Theme::selectedTextColor);
}
bool Format::hasBackgroundColor(const Theme &theme) const
{
return d->hasColor(theme, &TextStyleData::backgroundColor, &Theme::backgroundColor);
}
QColor Format::backgroundColor(const Theme &theme) const
{
return d->color(theme, &TextStyleData::backgroundColor, &Theme::backgroundColor);
}
QColor Format::selectedBackgroundColor(const Theme &theme) const
{
return d->color(theme, &TextStyleData::selectedBackgroundColor, &Theme::selectedBackgroundColor);
}
bool Format::isBold(const Theme &theme) const
{
const auto overrideStyle = d->styleOverride(theme);
if (overrideStyle.hasBold) {
return overrideStyle.bold;
}
return d->style.hasBold ? d->style.bold : theme.isBold(d->defaultStyle);
}
bool Format::isItalic(const Theme &theme) const
{
const auto overrideStyle = d->styleOverride(theme);
if (overrideStyle.hasItalic) {
return overrideStyle.italic;
}
return d->style.hasItalic ? d->style.italic : theme.isItalic(d->defaultStyle);
}
bool Format::isUnderline(const Theme &theme) const
{
const auto overrideStyle = d->styleOverride(theme);
if (overrideStyle.hasUnderline) {
return overrideStyle.underline;
}
return d->style.hasUnderline ? d->style.underline : theme.isUnderline(d->defaultStyle);
}
bool Format::isStrikeThrough(const Theme &theme) const
{
const auto overrideStyle = d->styleOverride(theme);
if (overrideStyle.hasStrikeThrough) {
return overrideStyle.strikeThrough;
}
return d->style.hasStrikeThrough ? d->style.strikeThrough : theme.isStrikeThrough(d->defaultStyle);
}
bool Format::spellCheck() const
{
return d->spellCheck;
}
bool Format::hasBoldOverride() const
{
return d->style.hasBold;
}
bool Format::hasItalicOverride() const
{
return d->style.hasItalic;
}
bool Format::hasUnderlineOverride() const
{
return d->style.hasUnderline;
}
bool Format::hasStrikeThroughOverride() const
{
return d->style.hasStrikeThrough;
}
bool Format::hasTextColorOverride() const
{
return d->style.textColor;
}
bool Format::hasBackgroundColorOverride() const
{
return d->style.backgroundColor;
}
bool Format::hasSelectedTextColorOverride() const
{
return d->style.selectedTextColor;
}
bool Format::hasSelectedBackgroundColorOverride() const
{
return d->style.selectedBackgroundColor;
}
void FormatPrivate::load(QXmlStreamReader &reader)
{
name = reader.attributes().value(QLatin1String("name")).toString();
defaultStyle = stringToDefaultFormat(reader.attributes().value(QLatin1String("defStyleNum")));
QStringView attribute = reader.attributes().value(QLatin1String("color"));
if (!attribute.isEmpty()) {
style.textColor = QColor(attribute).rgba();
}
attribute = reader.attributes().value(QLatin1String("selColor"));
if (!attribute.isEmpty()) {
style.selectedTextColor = QColor(attribute).rgba();
}
attribute = reader.attributes().value(QLatin1String("backgroundColor"));
if (!attribute.isEmpty()) {
style.backgroundColor = QColor(attribute).rgba();
}
attribute = reader.attributes().value(QLatin1String("selBackgroundColor"));
if (!attribute.isEmpty()) {
style.selectedBackgroundColor = QColor(attribute).rgba();
}
attribute = reader.attributes().value(QLatin1String("italic"));
if (!attribute.isEmpty()) {
style.hasItalic = true;
style.italic = Xml::attrToBool(attribute);
}
attribute = reader.attributes().value(QLatin1String("bold"));
if (!attribute.isEmpty()) {
style.hasBold = true;
style.bold = Xml::attrToBool(attribute);
}
attribute = reader.attributes().value(QLatin1String("underline"));
if (!attribute.isEmpty()) {
style.hasUnderline = true;
style.underline = Xml::attrToBool(attribute);
}
attribute = reader.attributes().value(QLatin1String("strikeOut"));
if (!attribute.isEmpty()) {
style.hasStrikeThrough = true;
style.strikeThrough = Xml::attrToBool(attribute);
}
attribute = reader.attributes().value(QLatin1String("spellChecking"));
if (!attribute.isEmpty()) {
spellCheck = Xml::attrToBool(attribute);
}
}
@@ -0,0 +1,192 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_FORMAT_H
#define KSYNTAXHIGHLIGHTING_FORMAT_H
#include "ksyntaxhighlighting_export.h"
#include "theme.h"
#include <QExplicitlySharedDataPointer>
namespace KSyntaxHighlighting
{
class FormatPrivate;
/** Describes the format to be used for a specific text fragment.
* The actual format used for displaying is merged from the format information
* in the syntax definition file, and a theme.
*
* @see Theme
* @since 5.28
*/
class KSYNTAXHIGHLIGHTING_EXPORT Format
{
public:
/** Creates an empty/invalid format. */
Format();
Format(const Format &other);
~Format();
Format &operator=(const Format &other);
/** Returns @c true if this is a valid format, ie. one that
* was read from a syntax definition file.
*/
bool isValid() const;
/** The name of this format as used in the syntax definition file. */
QString name() const;
/** Returns a unique identifier of this format.
* This is useful for efficient storing of formats in a text line. The
* identifier is unique per Repository instance, but will change when
* the repository is reloaded (which also invalidatess the corresponding
* Definition anyway).
*/
int id() const;
/** Returns the underlying TextStyle of this Format.
* Every Theme::TextStyle is visually defined by a Theme. A Format uses one
* of the Theme::TextStyle%s and on top allows modifications such as setting
* a different foreground color etc.
* @see Theme::TextStyle
* @since 5.49
*/
Theme::TextStyle textStyle() const;
/** Returns @c true if the combination of this format and the theme @p theme
* do not change the default text format in any way.
* This is useful for output formats where changing formatting implies cost,
* and thus benefit from optimizing the default case of not having any format
* applied. If you make use of this, make sure to set the default text style
* to what the corresponding theme sets for Theme::Normal.
*/
bool isDefaultTextStyle(const Theme &theme) const;
/** Returns @c true if the combination of this format and the theme @p theme
* change the foreground color compared to the default format.
*/
bool hasTextColor(const Theme &theme) const;
/** Returns the foreground color of the combination of this format and the
* given theme.
*/
QColor textColor(const Theme &theme) const;
/** Returns the foreground color for selected text of the combination of
* this format and the given theme.
*/
QColor selectedTextColor(const Theme &theme) const;
/** Returns @c true if the combination of this format and the theme @p theme
* change the background color compared to the default format.
*/
bool hasBackgroundColor(const Theme &theme) const;
/** Returns the background color of the combination of this format and the
* given theme.
*/
QColor backgroundColor(const Theme &theme) const;
/** Returns the background color of selected text of the combination of
* this format and the given theme.
*/
QColor selectedBackgroundColor(const Theme &theme) const;
/** Returns @c true if the combination of this format and the given theme
* results in bold text formatting.
*/
bool isBold(const Theme &theme) const;
/** Returns @c true if the combination of this format and the given theme
* results in italic text formatting.
*/
bool isItalic(const Theme &theme) const;
/** Returns @c true if the combination of this format and the given theme
* results in underlined text.
*/
bool isUnderline(const Theme &theme) const;
/** Returns @c true if the combination of this format and the given theme
* results in struck through text.
*/
bool isStrikeThrough(const Theme &theme) const;
/**
* Returns whether characters with this format should be spell checked.
*/
bool spellCheck() const;
/** Returns @c true if the syntax definition file sets a value for the bold text
* attribute and, therefore, overrides the theme and the default formatting
* style. If the return is @p true, this value is obtained by isBold().
* @see isBold()
* @since 5.62
*/
bool hasBoldOverride() const;
/** Returns @c true if the syntax definition file sets a value for the italic text
* attribute and, therefore, overrides the theme and the default formatting style.
* If the return is @p true, this value is obtained by isItalic().
* @see isItalic()
* @since 5.62
*/
bool hasItalicOverride() const;
/** Returns @c true if the syntax definition file sets a value for the underlined
* text attribute and, therefore, overrides the theme and the default formatting
* style. If the return is @p true, this value is obtained by isUnderline().
* @see isUnderline()
* @since 5.62
*/
bool hasUnderlineOverride() const;
/** Returns @c true if the syntax definition file specifies a value for the
* struck through text attribute. If the return is @p true, this value
* is obtained by isStrikeThrough().
* @see isStrikeThrough()
* @since 5.62
*/
bool hasStrikeThroughOverride() const;
/** Returns @c true if the syntax definition file sets a value for the foreground
* text color attribute and, therefore, overrides the theme and the default formatting
* style. If the return is @p true, this value is obtained by textColor().
* @see textColor(), hasTextColor()
* @since 5.62
*/
bool hasTextColorOverride() const;
/** Returns @c true if the syntax definition file sets a value for the background
* color attribute and, therefore, overrides the theme and the default formatting
* style. If the return is @p true, this value is obtained by backgroundColor().
* @see backgroundColor(), hasBackgroundColor()
* @since 5.62
*/
bool hasBackgroundColorOverride() const;
/** Returns @c true if the syntax definition file specifies a value for the
* selected text color attribute. If the return is @p true, this value is
* obtained by selectedTextColor().
* @see selectedTextColor()
* @since 5.62
*/
bool hasSelectedTextColorOverride() const;
/** Returns @c true if the syntax definition file specifies a value for the
* selected background color attribute. If the return is @p true, this
* value is obtained by selectedBackgroundColor().
* @see selectedBackgroundColor()
* @since 5.62
*/
bool hasSelectedBackgroundColorOverride() const;
private:
friend class FormatPrivate;
QExplicitlySharedDataPointer<FormatPrivate> d;
};
}
QT_BEGIN_NAMESPACE
Q_DECLARE_TYPEINFO(KSyntaxHighlighting::Format, Q_RELOCATABLE_TYPE);
QT_END_NAMESPACE
#endif // KSYNTAXHIGHLIGHTING_FORMAT_H
@@ -0,0 +1,51 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_FORMAT_P_H
#define KSYNTAXHIGHLIGHTING_FORMAT_P_H
#include "textstyledata_p.h"
#include "theme.h"
#include <QSharedData>
#include <QString>
QT_BEGIN_NAMESPACE
class QXmlStreamReader;
QT_END_NAMESPACE
namespace KSyntaxHighlighting
{
class FormatPrivate : public QSharedData
{
public:
FormatPrivate() = default;
static FormatPrivate *detachAndGet(Format &format);
static std::intptr_t ptrId(const Format &format)
{
return std::intptr_t(format.d.data());
}
TextStyleData styleOverride(const Theme &theme) const;
void load(QXmlStreamReader &reader);
using StyleColor = QRgb(TextStyleData::*);
using ThemeColor = QRgb (Theme::*)(Theme::TextStyle) const;
bool hasColor(const Theme &theme, StyleColor styleColor, ThemeColor themeColor) const;
QColor color(const Theme &theme, StyleColor styleColor, ThemeColor themeColor) const;
QString definitionName;
QString name;
TextStyleData style;
Theme::TextStyle defaultStyle = Theme::Normal;
int id = 0;
bool spellCheck = true;
};
}
#endif
@@ -0,0 +1,362 @@
/*
SPDX-FileCopyrightText: 2021 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#include "highlightingdata_p.hpp"
#include "ksyntaxhighlighting_logging.h"
#include "xml_p.h"
#include <QXmlStreamReader>
#include <QStringView>
using namespace KSyntaxHighlighting;
template<class Data, class... Args>
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<class Data1, class Data2, class Visitor>
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<decltype(data1)>;
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<decltype(data1)>;
new (&data1) Data(data2);
});
type = other.type;
}
HighlightingContextData::Rule::~Rule()
{
dataRuleVisit(type, data, data, [](auto &data, auto &) {
using Data = std::remove_reference_t<decltype(data)>;
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;
}
}
}
@@ -0,0 +1,195 @@
/*
SPDX-FileCopyrightText: 2021 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_HIGHLIGHTING_DATA_P_H
#define KSYNTAXHIGHLIGHTING_HIGHLIGHTING_DATA_P_H
#include <QString>
#include <vector>
QT_BEGIN_NAMESPACE
class QXmlStreamReader;
QT_END_NAMESPACE
namespace KSyntaxHighlighting
{
/**
* Represents the raw xml data of a context and its rules.
* After resolving contexts, members of this class are no longer
* use and the instance can be freed to recover used memory.
*/
class HighlightingContextData
{
public:
void load(const QString &defName, QXmlStreamReader &reader);
struct Rule {
enum class Type : quint8 {
Unknown,
AnyChar,
Detect2Chars,
DetectChar,
HlCOct,
IncludeRules,
Int,
Keyword,
LineContinue,
RangeDetect,
RegExpr,
StringDetect,
WordDetect,
Float,
HlCStringChar,
DetectIdentifier,
DetectSpaces,
HlCChar,
HlCHex,
};
struct AnyChar {
QString chars;
};
struct Detect2Chars {
QChar char1;
QChar char2;
};
struct DetectChar {
QChar char1;
bool dynamic;
};
struct WordDelimiters {
QString additionalDeliminator;
QString weakDeliminator;
};
struct Float {
WordDelimiters wordDelimiters;
};
struct HlCHex {
WordDelimiters wordDelimiters;
};
struct HlCOct {
WordDelimiters wordDelimiters;
};
struct IncludeRules {
QString contextName;
bool includeAttribute;
};
struct Int {
WordDelimiters wordDelimiters;
};
struct Keyword {
QString name;
WordDelimiters wordDelimiters;
Qt::CaseSensitivity caseSensitivityOverride;
bool hasCaseSensitivityOverride;
};
struct LineContinue {
QChar char1;
};
struct RangeDetect {
QChar begin;
QChar end;
};
struct RegExpr {
QString pattern;
Qt::CaseSensitivity caseSensitivity;
bool isMinimal;
bool dynamic;
};
struct StringDetect {
QString string;
Qt::CaseSensitivity caseSensitivity;
bool dynamic;
};
struct WordDetect {
QString word;
WordDelimiters wordDelimiters;
Qt::CaseSensitivity caseSensitivity;
};
union Data {
AnyChar anyChar;
Detect2Chars detect2Chars;
DetectChar detectChar;
HlCOct hlCOct;
IncludeRules includeRules;
Int detectInt;
Keyword keyword;
LineContinue lineContinue;
RangeDetect rangeDetect;
RegExpr regExpr;
StringDetect stringDetect;
WordDetect wordDetect;
Float detectFloat;
HlCHex hlCHex;
Data() noexcept
{
}
~Data()
{
}
};
struct Common {
QString contextName;
QString attributeName;
QString beginRegionName;
QString endRegionName;
int column = -1;
bool firstNonSpace = false;
bool lookAhead = false;
};
Type type = Type::Unknown;
Common common;
Data data;
Rule() noexcept;
Rule(Rule &&other) noexcept;
Rule(const Rule &other);
~Rule();
// since nothing is deleted in the rules vector, these functions do not need to be implemented
Rule &operator=(Rule &&other) = delete;
Rule &operator=(const Rule &other) = delete;
};
QString name;
/**
* attribute name, to lookup our format
*/
QString attribute;
QString lineEndContext;
QString lineEmptyContext;
QString fallthroughContext;
std::vector<Rule> rules;
bool stopEmptyLineContextSwitchLoop = false;
bool noIndentationBasedFolding = false;
};
}
#endif
@@ -0,0 +1,253 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2018 Christoph Cullmann <cullmann@kde.org>
SPDX-License-Identifier: MIT
*/
#include "htmlhighlighter.h"
#include "abstracthighlighter_p.h"
#include "definition.h"
#include "definition_p.h"
#include "format.h"
#include "ksyntaxhighlighting_logging.h"
#include "state.h"
#include "theme.h"
#include <QFile>
#include <QFileInfo>
#include <QIODevice>
#include <QTextStream>
using namespace KSyntaxHighlighting;
class KSyntaxHighlighting::HtmlHighlighterPrivate : public AbstractHighlighterPrivate
{
public:
std::unique_ptr<QTextStream> out;
std::unique_ptr<QFile> file;
QString currentLine;
std::vector<QString> htmlStyles;
Theme::EditorColorRole bgRole = Theme::BackgroundColor;
};
HtmlHighlighter::HtmlHighlighter()
: AbstractHighlighter(new HtmlHighlighterPrivate())
{
}
HtmlHighlighter::~HtmlHighlighter()
{
}
void HtmlHighlighter::setBackgroundRole(Theme::EditorColorRole bgRole)
{
Q_D(HtmlHighlighter);
d->bgRole = bgRole;
}
void HtmlHighlighter::setOutputFile(const QString &fileName)
{
Q_D(HtmlHighlighter);
d->file.reset(new QFile(fileName));
if (!d->file->open(QFile::WriteOnly | QFile::Truncate)) {
qCWarning(Log) << "Failed to open output file" << fileName << ":" << d->file->errorString();
return;
}
d->out.reset(new QTextStream(d->file.get()));
d->out->setEncoding(QStringConverter::Utf8);
}
void HtmlHighlighter::setOutputFile(FILE *fileHandle)
{
Q_D(HtmlHighlighter);
d->out.reset(new QTextStream(fileHandle, QIODevice::WriteOnly));
d->out->setEncoding(QStringConverter::Utf8);
}
void HtmlHighlighter::highlightFile(const QString &fileName, const QString &title)
{
QFile f(fileName);
if (!f.open(QFile::ReadOnly)) {
qCWarning(Log) << "Failed to open input file" << fileName << ":" << f.errorString();
return;
}
if (title.isEmpty()) {
QFileInfo fi(fileName);
highlightData(&f, fi.fileName());
} else {
highlightData(&f, title);
}
}
namespace
{
/**
* @brief toHtmlRgba
* Converts QRgb -> #RRGGBBAA if there is an alpha channel
* otherwise it will just return the hexcode. This is because QColor
* outputs #AARRGGBB, whereas browser support #RRGGBBAA.
*/
struct HtmlColor {
HtmlColor(QRgb argb)
{
static const char16_t *digits = u"0123456789abcdef";
hexcode[0] = u'#';
hexcode[1] = digits[qRed(argb) >> 4];
hexcode[2] = digits[qRed(argb) & 0xf];
hexcode[3] = digits[qGreen(argb) >> 4];
hexcode[4] = digits[qGreen(argb) & 0xf];
hexcode[5] = digits[qBlue(argb) >> 4];
hexcode[6] = digits[qBlue(argb) & 0xf];
if (qAlpha(argb) == 0xff) {
len = 7;
} else {
hexcode[7] = digits[qAlpha(argb) >> 4];
hexcode[8] = digits[qAlpha(argb) & 0xf];
len = 9;
}
}
QStringView sv() const
{
return QStringView(hexcode, len);
}
private:
QChar hexcode[9];
qsizetype len;
};
}
void HtmlHighlighter::highlightData(QIODevice *dev, const QString &title)
{
Q_D(HtmlHighlighter);
if (!d->out) {
qCWarning(Log) << "No output stream defined!";
return;
}
QString htmlTitle;
if (title.isEmpty()) {
htmlTitle = QStringLiteral("KSyntaxHighlighter");
} else {
htmlTitle = title.toHtmlEscaped();
}
const auto &theme = d->m_theme;
const auto &definition = d->m_definition;
const bool useSelectedText = d->bgRole == Theme::TextSelection;
auto definitions = definition.includedDefinitions();
definitions.append(definition);
const auto mainTextColor = [&] {
if (useSelectedText) {
const auto fg = theme.selectedTextColor(Theme::Normal);
if (fg) {
return fg;
}
}
return theme.textColor(Theme::Normal);
}();
const auto mainBgColor = theme.editorColor(d->bgRole);
int maxId = 0;
for (const auto &definition : std::as_const(definitions)) {
for (const auto &format : std::as_const(DefinitionData::get(definition)->formats)) {
maxId = qMax(maxId, format.id());
}
}
d->htmlStyles.clear();
// htmlStyles must not be empty for applyFormat to work even with a definition without any context
d->htmlStyles.resize(maxId + 1);
// initialize htmlStyles
for (const auto &definition : std::as_const(definitions)) {
for (const auto &format : std::as_const(DefinitionData::get(definition)->formats)) {
auto &buffer = d->htmlStyles[format.id()];
const auto textColor = useSelectedText ? format.selectedTextColor(theme).rgba() : format.textColor(theme).rgba();
if (textColor && textColor != mainTextColor) {
buffer += QStringLiteral("color:") + HtmlColor(textColor).sv() + u';';
}
const auto bgColor = useSelectedText ? format.selectedBackgroundColor(theme).rgba() : format.backgroundColor(theme).rgba();
if (bgColor && bgColor != mainBgColor) {
buffer += QStringLiteral("background-color:") + HtmlColor(bgColor).sv() + u';';
}
if (format.isBold(theme)) {
buffer += QStringLiteral("font-weight:bold;");
}
if (format.isItalic(theme)) {
buffer += QStringLiteral("font-style:italic;");
}
if (format.isUnderline(theme)) {
buffer += QStringLiteral("text-decoration:underline;");
}
if (format.isStrikeThrough(theme)) {
buffer += QStringLiteral("text-decoration:line-through;");
}
if (!buffer.isEmpty()) {
buffer.insert(0, QStringLiteral("<span style=\""));
// replace last ';'
buffer.back() = u'"';
buffer += u'>';
}
}
}
State state;
*d->out << "<!DOCTYPE html>\n";
*d->out << "<html><head>\n";
*d->out << "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n";
*d->out << "<title>" << htmlTitle << "</title>\n";
*d->out << "<meta name=\"generator\" content=\"KF5::SyntaxHighlighting - Definition (" << definition.name() << ") - Theme (" << theme.name() << ")\"/>\n";
*d->out << "</head><body";
*d->out << " style=\"background-color:" << HtmlColor(mainBgColor).sv();
*d->out << ";color:" << HtmlColor(mainTextColor).sv();
*d->out << "\"><pre>\n";
QTextStream in(dev);
while (in.readLineInto(&d->currentLine)) {
state = highlightLine(d->currentLine, state);
*d->out << "\n";
}
*d->out << "</pre></body></html>\n";
d->out->flush();
d->out.reset();
d->file.reset();
}
void HtmlHighlighter::applyFormat(int offset, int length, const Format &format)
{
if (length == 0) {
return;
}
Q_D(HtmlHighlighter);
auto const &htmlStyle = d->htmlStyles[format.id()];
if (!htmlStyle.isEmpty()) {
*d->out << htmlStyle;
}
for (QChar ch : QStringView(d->currentLine).sliced(offset, length)) {
if (ch == u'<')
*d->out << QStringLiteral("&lt;");
else if (ch == u'&')
*d->out << QStringLiteral("&amp;");
else
*d->out << ch;
}
if (!htmlStyle.isEmpty()) {
*d->out << QStringLiteral("</span>");
}
}
@@ -0,0 +1,46 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_HTMLHIGHLIGHTER_H
#define KSYNTAXHIGHLIGHTING_HTMLHIGHLIGHTER_H
#include "abstracthighlighter.h"
#include "ksyntaxhighlighting_export.h"
#include "theme.h"
#include <QString>
QT_BEGIN_NAMESPACE
class QIODevice;
QT_END_NAMESPACE
namespace KSyntaxHighlighting
{
class HtmlHighlighterPrivate;
class KSYNTAXHIGHLIGHTING_EXPORT HtmlHighlighter : public AbstractHighlighter
{
public:
HtmlHighlighter();
~HtmlHighlighter() override;
void highlightFile(const QString &fileName, const QString &title = QString());
void highlightData(QIODevice *device, const QString &title = QString());
void setOutputFile(const QString &fileName);
void setOutputFile(FILE *fileHandle);
void setBackgroundRole(Theme::EditorColorRole bgRole);
protected:
void applyFormat(int offset, int length, const Format &format) override;
private:
Q_DECLARE_PRIVATE(HtmlHighlighter)
};
}
#endif // KSYNTAXHIGHLIGHTING_HTMLHIGHLIGHTER_H
@@ -0,0 +1,147 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#include "definition_p.h"
#include "keywordlist_p.h"
#include "ksyntaxhighlighting_logging.h"
#include "repository.h"
#include <QXmlStreamReader>
#include <algorithm>
using namespace KSyntaxHighlighting;
namespace
{
struct KeywordComparator {
Qt::CaseSensitivity caseSensitive;
bool operator()(QStringView a, QStringView b) const
{
if (a.size() < b.size()) {
return true;
}
if (a.size() > b.size()) {
return false;
}
return a.compare(b, caseSensitive) < 0;
}
};
}
bool KeywordList::contains(QStringView str, Qt::CaseSensitivity caseSensitive) const
{
/**
* get right vector to search in
*/
const auto &vectorToSearch = (caseSensitive == Qt::CaseSensitive) ? m_keywordsSortedCaseSensitive : m_keywordsSortedCaseInsensitive;
/**
* search with right predicate
*/
return std::binary_search(vectorToSearch.begin(), vectorToSearch.end(), str, KeywordComparator{caseSensitive});
}
void KeywordList::load(QXmlStreamReader &reader)
{
Q_ASSERT(reader.name() == QLatin1String("list"));
Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement);
m_name = reader.attributes().value(QLatin1String("name")).toString();
while (!reader.atEnd()) {
switch (reader.tokenType()) {
case QXmlStreamReader::StartElement:
if (reader.name() == QLatin1String("item")) {
m_keywords.append(reader.readElementText().trimmed());
reader.readNextStartElement();
break;
} else if (reader.name() == QLatin1String("include")) {
m_includes.append(reader.readElementText().trimmed());
reader.readNextStartElement();
break;
}
reader.readNext();
break;
case QXmlStreamReader::EndElement:
reader.readNext();
return;
default:
reader.readNext();
break;
}
}
}
void KeywordList::setCaseSensitivity(Qt::CaseSensitivity caseSensitive)
{
/**
* remember default case-sensitivity and init lookup for it
*/
m_caseSensitive = caseSensitive;
initLookupForCaseSensitivity(m_caseSensitive);
}
void KeywordList::initLookupForCaseSensitivity(Qt::CaseSensitivity caseSensitive)
{
/**
* get right vector to sort, if non-empty, we are done
*/
auto &vectorToSort = (caseSensitive == Qt::CaseSensitive) ? m_keywordsSortedCaseSensitive : m_keywordsSortedCaseInsensitive;
if (!vectorToSort.empty()) {
return;
}
/**
* fill vector with refs to keywords
*/
vectorToSort.assign(m_keywords.constBegin(), m_keywords.constEnd());
/**
* sort with right predicate
*/
std::sort(vectorToSort.begin(), vectorToSort.end(), KeywordComparator{caseSensitive});
}
void KeywordList::resolveIncludeKeywords(DefinitionData &def)
{
while (!m_includes.isEmpty()) {
const auto kw_include = std::move(m_includes.back());
m_includes.pop_back();
const auto idx = kw_include.indexOf(QLatin1String("##"));
KeywordList *keywords = nullptr;
if (idx >= 0) {
auto defName = kw_include.sliced(idx + 2);
auto includeDef = def.repo->definitionForName(defName);
if (includeDef.isValid()) {
auto listName = kw_include.sliced(0, idx);
auto defData = DefinitionData::get(includeDef);
defData->load(DefinitionData::OnlyKeywords(true));
keywords = defData->keywordList(listName);
} else {
qCWarning(Log) << "Unable to resolve external include keyword for definition" << defName << "in" << def.name;
}
} else {
keywords = def.keywordList(kw_include);
}
if (keywords) {
if (this != keywords) {
keywords->resolveIncludeKeywords(def);
}
m_keywords += keywords->m_keywords;
} else {
qCWarning(Log) << "Unresolved include keyword" << kw_include << "in" << def.name;
}
}
}
@@ -0,0 +1,107 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_KEYWORDLIST_P_H
#define KSYNTAXHIGHLIGHTING_KEYWORDLIST_P_H
#include <QString>
#include <QStringList>
#include <QStringView>
#include <vector>
QT_BEGIN_NAMESPACE
class QXmlStreamReader;
QT_END_NAMESPACE
namespace KSyntaxHighlighting
{
class Repository;
class DefinitionData;
class KeywordList
{
public:
KeywordList() = default;
~KeywordList() = default;
bool isEmpty() const
{
return m_keywords.isEmpty();
}
const QString &name() const
{
return m_name;
}
const QStringList &keywords() const
{
return m_keywords;
}
Qt::CaseSensitivity caseSensitivity() const
{
return m_caseSensitive;
}
void setKeywordList(const QStringList &keywords)
{
m_keywords = keywords;
m_keywordsSortedCaseSensitive.clear();
m_keywordsSortedCaseInsensitive.clear();
initLookupForCaseSensitivity(m_caseSensitive);
}
/** Checks if @p str is a keyword in this list. */
bool contains(QStringView str) const
{
return contains(str, m_caseSensitive);
}
/** Checks if @p str is a keyword in this list, overriding the global case-sensitivity setting. */
bool contains(QStringView str, Qt::CaseSensitivity caseSensitive) const;
void load(QXmlStreamReader &reader);
void setCaseSensitivity(Qt::CaseSensitivity caseSensitive);
void initLookupForCaseSensitivity(Qt::CaseSensitivity caseSensitive);
void resolveIncludeKeywords(DefinitionData &def);
private:
/**
* name of keyword list as in XML
*/
QString m_name;
/**
* raw list of keywords, as seen in XML (but trimmed)
*/
QStringList m_keywords;
/**
* raw list of include keywords, as seen in XML (but trimmed)
*/
QStringList m_includes;
/**
* default case-sensitivity setting
*/
Qt::CaseSensitivity m_caseSensitive = Qt::CaseSensitive;
/**
* case-sensitive sorted string references to m_keywords for lookup
*/
std::vector<QStringView> m_keywordsSortedCaseSensitive;
/**
* case-insensitive sorted string references to m_keywords for lookup
*/
std::vector<QStringView> m_keywordsSortedCaseInsensitive;
};
}
#endif // KSYNTAXHIGHLIGHTING_KEYWORDLIST_P_H
@@ -0,0 +1,95 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_MATCHRESULT_P_H
#define KSYNTAXHIGHLIGHTING_MATCHRESULT_P_H
#include <QStringList>
namespace KSyntaxHighlighting
{
/**
* Storage for match result of a Rule.
* Heavily used internally during highlightLine, therefore completely inline.
*/
class MatchResult
{
public:
/**
* Match at given offset found.
* @param offset offset of match
*/
MatchResult(const int offset)
: m_offset(offset)
{
}
/**
* Match at given offset found with additional skip offset.
*/
explicit MatchResult(const int offset, const int skipOffset)
: m_offset(offset)
, m_skipOffset(skipOffset)
{
}
/**
* Match at given offset found with additional captures.
* @param offset offset of match
* @param captures captures of the match
*/
explicit MatchResult(const int offset, QStringList &&captures)
: m_offset(offset)
, m_captures(std::move(captures))
{
}
/**
* Offset of the match
* @return offset of the match
*/
int offset() const
{
return m_offset;
}
/**
* Skip offset of the match
* @return skip offset of the match, no match possible until this offset is reached
*/
int skipOffset() const
{
return m_skipOffset;
}
/**
* Captures of the match.
* @return captured text of this match
*/
QStringList &captures()
{
return m_captures;
}
private:
/**
* match offset, filled in all constructors
*/
int m_offset;
/**
* skip offset, optional
*/
int m_skipOffset = 0;
/**
* captures, optional
*/
QStringList m_captures;
};
}
#endif // KSYNTAXHIGHLIGHTING_MATCHRESULT_P_H
@@ -0,0 +1,437 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#include "repository.h"
#include "definition.h"
#include "definition_p.h"
#include "repository_p.h"
#include "theme.h"
#include "themedata_p.h"
#include "wildcardmatcher.h"
#include <QCborMap>
#include <QCborValue>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QPalette>
#include <QString>
#include <QStringView>
#ifndef NO_STANDARD_PATHS
#include <QStandardPaths>
#endif
#include <algorithm>
#include <iterator>
#include <limits>
using namespace KSyntaxHighlighting;
namespace
{
QString fileNameFromFilePath(const QString &filePath)
{
return QFileInfo{filePath}.fileName();
}
auto anyWildcardMatches(QStringView str)
{
return [str](const Definition &def) {
const auto strings = def.extensions();
return std::any_of(strings.cbegin(), strings.cend(), [str](QStringView wildcard) {
return WildcardMatcher::exactMatch(str, wildcard);
});
};
}
auto anyMimeTypeEquals(QStringView mimeTypeName)
{
return [mimeTypeName](const Definition &def) {
const auto strings = def.mimeTypes();
return std::any_of(strings.cbegin(), strings.cend(), [mimeTypeName](QStringView name) {
return mimeTypeName == name;
});
};
}
// The two function templates below take defs - a map sorted by highlighting name - to be deterministic and independent of translations.
template<typename UnaryPredicate>
Definition findHighestPriorityDefinitionIf(const QList<Definition> &defs, UnaryPredicate predicate)
{
const Definition *match = nullptr;
auto matchPriority = std::numeric_limits<int>::lowest();
for (const Definition &def : defs) {
const auto defPriority = def.priority();
if (defPriority > matchPriority && predicate(def)) {
match = &def;
matchPriority = defPriority;
}
}
return match == nullptr ? Definition{} : *match;
}
template<typename UnaryPredicate>
QList<Definition> findDefinitionsIf(const QList<Definition> &defs, UnaryPredicate predicate)
{
QList<Definition> matches;
std::copy_if(defs.cbegin(), defs.cend(), std::back_inserter(matches), predicate);
std::stable_sort(matches.begin(), matches.end(), [](const Definition &lhs, const Definition &rhs) {
return lhs.priority() > rhs.priority();
});
return matches;
}
} // unnamed namespace
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
for (const auto &def : std::as_const(d->m_flatDefs)) {
DefinitionData::get(def)->repo = nullptr;
}
}
Definition Repository::definitionForName(const QString &defName) const
{
return d->m_fullDefs.value(defName.toLower());
}
Definition Repository::definitionForFileName(const QString &fileName) const
{
return findHighestPriorityDefinitionIf(d->m_flatDefs, anyWildcardMatches(fileNameFromFilePath(fileName)));
}
QList<Definition> Repository::definitionsForFileName(const QString &fileName) const
{
return findDefinitionsIf(d->m_flatDefs, anyWildcardMatches(fileNameFromFilePath(fileName)));
}
Definition Repository::definitionForMimeType(const QString &mimeType) const
{
return findHighestPriorityDefinitionIf(d->m_flatDefs, anyMimeTypeEquals(mimeType));
}
QList<Definition> Repository::definitionsForMimeType(const QString &mimeType) const
{
return findDefinitionsIf(d->m_flatDefs, anyMimeTypeEquals(mimeType));
}
QList<Definition> Repository::definitions() const
{
return d->m_sortedDefs;
}
QList<Theme> Repository::themes() const
{
return d->m_themes;
}
static auto lowerBoundTheme(const QList<KSyntaxHighlighting::Theme> &themes, QStringView themeName)
{
return std::lower_bound(themes.begin(), themes.end(), themeName, [](const Theme &lhs, QStringView rhs) {
return lhs.name() < rhs;
});
}
Theme Repository::theme(const QString &themeName) const
{
const auto &themes = d->m_themes;
const auto it = lowerBoundTheme(themes, themeName);
if (it != themes.end() && (*it).name() == themeName) {
return *it;
}
return Theme();
}
Theme Repository::defaultTheme(Repository::DefaultTheme t) const
{
if (t == DarkTheme) {
return theme(QStringLiteral("Breeze Dark"));
}
return theme(QStringLiteral("Breeze Light"));
}
Theme Repository::themeForPalette(const QPalette &palette) const
{
const auto base = palette.color(QPalette::Base);
const auto highlight = palette.color(QPalette::Highlight).rgb();
// find themes with matching background and highlight colors
const Theme *firstMatchingTheme = nullptr;
for (const auto &theme : std::as_const(d->m_themes)) {
const auto background = theme.editorColor(Theme::EditorColorRole::BackgroundColor);
if (background == base.rgb()) {
// find theme with a matching highlight color
auto selection = theme.editorColor(Theme::EditorColorRole::TextSelection);
if (selection == highlight) {
return theme;
}
if (!firstMatchingTheme) {
firstMatchingTheme = &theme;
}
}
}
if (firstMatchingTheme) {
return *firstMatchingTheme;
}
// fallback to just use the default light or dark theme
return defaultTheme((base.lightness() < 128) ? Repository::DarkTheme : Repository::LightTheme);
}
void RepositoryPrivate::load(Repository *repo)
{
// always add invalid default "None" highlighting
m_defs.emplace(QString(), Definition());
// do lookup in standard paths, if not disabled
#ifndef NO_STANDARD_PATHS
// do lookup in installed path when has no syntax resource
#ifndef HAS_SYNTAX_RESOURCE
for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
QStringLiteral("org.kde.syntax-highlighting/syntax-bundled"),
QStandardPaths::LocateDirectory)) {
if (!loadSyntaxFolderFromIndex(repo, dir)) {
loadSyntaxFolder(repo, dir);
}
}
#endif
for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
QStringLiteral("org.kde.syntax-highlighting/syntax"),
QStandardPaths::LocateDirectory)) {
loadSyntaxFolder(repo, dir);
}
#endif
// default resources are always used, this is the one location that has a index cbor file
loadSyntaxFolderFromIndex(repo, QStringLiteral(":/org.kde.syntax-highlighting/syntax"));
// extra resources provided by 3rdparty libraries/applications
loadSyntaxFolder(repo, QStringLiteral(":/org.kde.syntax-highlighting/syntax-addons"));
// user given extra paths
for (const auto &path : std::as_const(m_customSearchPaths)) {
loadSyntaxFolder(repo, path + QStringLiteral("/syntax"));
}
computeAlternativeDefLists();
// load themes
// do lookup in standard paths, if not disabled
#ifndef NO_STANDARD_PATHS
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"));
// extra resources provided by 3rdparty libraries/applications
loadThemeFolder(QStringLiteral(":/org.kde.syntax-highlighting/themes-addons"));
// user given extra paths
for (const auto &path : std::as_const(m_customSearchPaths)) {
loadThemeFolder(path + QStringLiteral("/themes"));
}
}
void RepositoryPrivate::computeAlternativeDefLists()
{
m_flatDefs.clear();
m_flatDefs.reserve(m_defs.size());
for (const auto &[_, def] : m_defs) {
m_flatDefs.push_back(def);
}
m_sortedDefs = m_flatDefs;
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;
});
m_fullDefs.clear();
for (const auto &def : std::as_const(m_sortedDefs)) {
m_fullDefs.insert(def.name().toLower(), def);
const auto &alternativeNames = def.alternativeNames();
for (const auto &altName : alternativeNames) {
m_fullDefs.insert(altName.toLower(), def);
}
}
}
void RepositoryPrivate::loadSyntaxFolder(Repository *repo, const QString &path)
{
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(std::move(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(QCborValue::fromCbor(indexFile.readAll()));
const auto index = indexDoc.toMap();
for (auto it = index.begin(); it != index.end(); ++it) {
if (!it.value().isMap()) {
continue;
}
const auto fileName = QString(path + QLatin1Char('/') + it.key().toString());
const auto defMap = it.value().toMap();
Definition def;
auto defData = DefinitionData::get(def);
defData->repo = repo;
if (defData->loadMetaData(fileName, defMap)) {
addDefinition(std::move(def));
}
}
return true;
}
void RepositoryPrivate::addDefinition(Definition &&def)
{
const auto [it, inserted] = m_defs.try_emplace(def.name(), std::move(def));
if (inserted) {
return;
}
if (it->second.version() >= def.version()) {
return;
}
it->second = std::move(def);
}
void RepositoryPrivate::loadThemeFolder(const QString &path)
{
QDirIterator it(path, QStringList() << QLatin1String("*.theme"), QDir::Files);
while (it.hasNext()) {
auto themeData = std::unique_ptr<ThemeData>(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 &constThemes = m_themes;
const auto themeName = theme.name();
const auto constIt = lowerBoundTheme(constThemes, themeName);
const auto index = constIt - constThemes.begin();
if (constIt == constThemes.end() || (*constIt).name() != themeName) {
m_themes.insert(index, theme);
return;
}
if (themeRevision(*constIt) < themeRevision(theme)) {
m_themes[index] = theme;
}
}
int 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();
}
Q_ASSERT(m_foldingRegionId < std::numeric_limits<int>::max());
m_foldingRegionIds.insert(qMakePair(defName, foldName), ++m_foldingRegionId);
return m_foldingRegionId;
}
int RepositoryPrivate::nextFormatId()
{
Q_ASSERT(m_formatId < std::numeric_limits<int>::max());
return ++m_formatId;
}
void Repository::reload()
{
Q_EMIT aboutToReload();
for (const auto &def : std::as_const(d->m_flatDefs)) {
DefinitionData::get(def)->clear();
}
d->m_defs.clear();
d->m_flatDefs.clear();
d->m_sortedDefs.clear();
d->m_fullDefs.clear();
d->m_themes.clear();
d->m_foldingRegionId = 0;
d->m_foldingRegionIds.clear();
d->m_formatId = 0;
d->load(this);
Q_EMIT reloaded();
}
void Repository::addCustomSearchPath(const QString &path)
{
Q_EMIT aboutToReload();
d->m_customSearchPaths.append(path);
d->loadThemeFolder(path + QStringLiteral("/themes"));
d->loadSyntaxFolder(this, path + QStringLiteral("/syntax"));
d->computeAlternativeDefLists();
Q_EMIT reloaded();
}
QList<QString> Repository::customSearchPaths() const
{
return d->m_customSearchPaths;
}
#include "moc_repository.cpp"
@@ -0,0 +1,288 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_REPOSITORY_H
#define KSYNTAXHIGHLIGHTING_REPOSITORY_H
#include "ksyntaxhighlighting_export.h"
#include <QList>
#include <QObject>
#include <QtGlobal>
#include <memory>
QT_BEGIN_NAMESPACE
class QString;
class QPalette;
QT_END_NAMESPACE
/**
* @namespace KSyntaxHighlighting
*
* Syntax highlighting engine for Kate syntax definitions.
* In order to access the syntax highlighting Definition files, use the
* class Repository.
*
* @see Repository
*/
namespace KSyntaxHighlighting
{
class Definition;
class RepositoryPrivate;
class Theme;
/**
* @brief Syntax highlighting repository.
*
* @section repo_intro Introduction
*
* The Repository gives access to all syntax Definitions available on the
* system, typically described in *.xml files. The Definition files are read
* from the resource that is compiled into the executable, and from the file
* system. If a Definition exists in the resource and on the file system,
* then the one from the file system is chosen.
*
* @section repo_access Definitions and Themes
*
* Typically, only one instance of the Repository is needed. This single
* instance can be thought of as a singleton you keep alive throughout the
* lifetime of your application. Then, either call definitionForName() with the
* given language name (e.g. "QML" or "Java"), or definitionForFileName() to
* obtain a Definition based on the filename/mimetype of the file. The
* function definitions() returns a list of all available syntax Definition%s.
*
* In addition to Definitions, the Repository also provides a list of Themes.
* A Theme is defined by a set of default text style colors as well as editor
* colors. These colors together provide all required colros for drawing all
* primitives of a text editor. All available Theme%s can be queried through
* themes(), and a Theme with a specific name is obtained through theme().
* Additionally, defaultTheme() provides a way to obtain a default theme for
* either a light or a black color theme.
*
* @section repo_search_paths Search Paths
*
* All highlighting Definition and Theme files are compiled into the shared
* KSyntaxHighlighting library by using the Qt resource system. Loading
* additional files from disk is supported as well.
*
* Loading syntax Definition files works as follows:
*
* -# First, all syntax highlighting files are loaded that are located in
* QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("org.kde.syntax-highlighting/syntax"), QStandardPaths::LocateDirectory);
* Under Unix, this uses $XDG_DATA_HOME and $XDG_DATA_DIRS, which could
* map to $HOME/.local5/share/org.kde.syntax-highlighting/syntax and
* /usr/share/org.kde.syntax-highlighting/syntax.
*
* -# Then, all files compiled into the library through resources are loaded.
* The internal resource path is ":/org.kde.syntax-highlighting/syntax".
* This path should never be touched by other applications.
*
* -# Then, all custom files compiled into resources are loaded.
* The resource path is ":/org.kde.syntax-highlighting/syntax-addons".
* This path can be used by other libraries/applications to bundle specialized definitions.
* Per default this path isn't used by the framework itself.
*
* -# Finally, the search path can be extended by calling addCustomSearchPath().
* A custom search path can either be a path on disk or again a path to
* a Qt resource.
*
* Similarly, loading Theme files works as follows:
*
* -# First, all Theme files are loaded that are located in
* QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("org.kde.syntax-highlighting/themes"), QStandardPaths::LocateDirectory);
* Under Unix, this uses $XDG_DATA_HOME and $XDG_DATA_DIRS, which could
* map to $HOME/.local5/share/org.kde.syntax-highlighting/themes and
* /usr/share/org.kde.syntax-highlighting/themes.
*
* -# Then, all files compiled into the library through resources are loaded.
* The internal resource path is ":/org.kde.syntax-highlighting/themes".
* This path should never be touched by other applications.
*
* -# Then, all custom files compiled into resources are loaded.
* The resource path is ":/org.kde.syntax-highlighting/themes-addons".
* This path can be used by other libraries/applications to bundle specialized themes.
* Per default this path isn't used by the framework itself.
*
* -# Finally, all Theme%s located in the paths added addCustomSearchPath()
* are loaded.
*
* @note Whenever a Definition or a Theme exists twice, the variant with
* higher version is used.
*
* @note The QStandardPaths lookup can be disabled by compiling the framework with the -DNO_STANDARD_PATHS define.
*
* @see Definition, Theme, AbstractHighlighter
* @since 5.28
*/
class KSYNTAXHIGHLIGHTING_EXPORT Repository : public QObject
{
Q_OBJECT
Q_PROPERTY(QList<KSyntaxHighlighting::Definition> definitions READ definitions NOTIFY reloaded)
Q_PROPERTY(QList<KSyntaxHighlighting::Theme> themes READ themes NOTIFY reloaded)
public:
/**
* Create a new syntax definition repository.
* This will read the meta data information of all available syntax
* definition, which is a moderately expensive operation, it's therefore
* recommended to keep a single instance of Repository around as long
* as you need highlighting in your application.
*/
Repository();
~Repository();
/**
* Returns the Definition named @p defName.
*
* If no Definition is found, Definition::isValid() of the returned instance
* returns false.
*
* @note The search is @e Qt::CaseInsensitive using untranslated names or
* alternative names. For instance, the cpp.xml definition file sets
* its name to @e C++ with an alternative name of @e CPP. Therefore, the
* strings "C++", "c++", "CPP" and "cpp" will all return a valid
* Definition file.
*/
Q_INVOKABLE KSyntaxHighlighting::Definition definitionForName(const QString &defName) const;
/**
* Returns the best matching Definition for the file named @p fileName.
* The match is performed based on the \e extensions and @e mimetype of
* the definition files. If multiple matches are found, the one with the
* highest priority is returned.
*
* If no match is found, Definition::isValid() of the returned instance
* returns false.
*/
Q_INVOKABLE KSyntaxHighlighting::Definition definitionForFileName(const QString &fileName) const;
/**
* Returns all Definition%s for the file named @p fileName sorted by priority.
* The match is performed based on the \e extensions and @e mimetype of
* the definition files.
*
* @since 5.56
*/
Q_INVOKABLE QList<KSyntaxHighlighting::Definition> definitionsForFileName(const QString &fileName) const;
/**
* Returns the best matching Definition to the type named @p mimeType
*
* If no match is found, Definition::isValid() of the returned instance
* returns false.
*
* @since 5.50
*/
Q_INVOKABLE KSyntaxHighlighting::Definition definitionForMimeType(const QString &mimeType) const;
/**
* Returns all Definition%s to the type named @p mimeType sorted by priority
*
* @since 5.56
*/
Q_INVOKABLE QList<KSyntaxHighlighting::Definition> definitionsForMimeType(const QString &mimeType) const;
/**
* Returns all available Definition%s.
* Definition%ss are ordered by translated section and translated names,
* for consistent displaying.
*/
Q_INVOKABLE QList<KSyntaxHighlighting::Definition> definitions() const;
/**
* Returns all available color themes.
* The returned list should never be empty.
*/
Q_INVOKABLE QList<KSyntaxHighlighting::Theme> themes() const;
/**
* Returns the theme called @p themeName.
* If the requested theme cannot be found, the retunred Theme is invalid,
* see Theme::isValid().
*/
Q_INVOKABLE KSyntaxHighlighting::Theme theme(const QString &themeName) const;
/**
* Built-in default theme types.
* @see defaultTheme()
*/
enum DefaultTheme {
//! Theme with a light background color.
LightTheme,
//! Theme with a dark background color.
DarkTheme
};
Q_ENUM(DefaultTheme)
/**
* Returns a default theme instance of the given type.
* The returned Theme is guaranteed to be a valid theme.
* @since 5.79
*/
Q_INVOKABLE KSyntaxHighlighting::Theme defaultTheme(DefaultTheme t = LightTheme) const;
/**
* Returns the best matching theme for the given palette
* @since 5.79
**/
Theme themeForPalette(const QPalette &palette) const;
/**
* Reloads the repository.
* This is a moderately expensive operations and should thus only be
* triggered when the installed syntax definition files changed.
*/
void reload();
/**
* Add a custom search path to the repository.
* This path will be searched in addition to the usual locations for syntax
* and theme definition files. Both locations on disk as well as Qt
* resource paths are supported.
*
* @note Internally, the two sub-folders @p path/syntax as well as
* @p path/themes are searched for additional Definition%s and
* Theme%s. Do not append @e syntax or @e themes to @p path
* yourself.
*
* @note Calling this triggers a reload() of the repository.
*
* @since 5.39
*/
void addCustomSearchPath(const QString &path);
/**
* Returns the list of custom search paths added to the repository.
* By default, this list is empty.
*
* @see addCustomSearchPath()
* @since 5.39
*/
QList<QString> customSearchPaths() const;
Q_SIGNALS:
/**
* This signal is emitted before the reload is started.
* @since 6.0
*/
void aboutToReload();
/**
* This signal is emitted when the reload is finished.
* @since 6.0
*/
void reloaded();
private:
Q_DISABLE_COPY(Repository)
friend class RepositoryPrivate;
std::unique_ptr<RepositoryPrivate> d;
};
}
#endif // KSYNTAXHIGHLIGHTING_REPOSITORY_H
@@ -0,0 +1,67 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_REPOSITORY_P_H
#define KSYNTAXHIGHLIGHTING_REPOSITORY_P_H
#include <QHash>
#include <QList>
#include <QString>
#include <map>
#include "dynamicregexpcache_p.h"
namespace KSyntaxHighlighting
{
class Definition;
class Repository;
class Theme;
class RepositoryPrivate
{
public:
RepositoryPrivate() = default;
static RepositoryPrivate *get(Repository *repo);
void load(Repository *repo);
void loadSyntaxFolder(Repository *repo, const QString &path);
bool loadSyntaxFolderFromIndex(Repository *repo, const QString &path);
void computeAlternativeDefLists();
void addDefinition(Definition &&def);
void loadThemeFolder(const QString &path);
void addTheme(const Theme &theme);
int foldingRegionId(const QString &defName, const QString &foldName);
int nextFormatId();
QList<QString> m_customSearchPaths;
// sorted map to have deterministic iteration order
std::map<QString, Definition> m_defs;
// flat version of m_defs for speed up iterations for e.g. definitionsForFileName
QList<Definition> m_flatDefs;
// map relating all names and alternative names, case insensitively to the correct definition.
QHash<QString, Definition> m_fullDefs;
// this vector is sorted by translated sections/names
QList<Definition> m_sortedDefs;
QList<Theme> m_themes;
QHash<QPair<QString, QString>, int> m_foldingRegionIds;
int m_foldingRegionId = 0;
int m_formatId = 0;
DynamicRegexpCache m_dynamicRegexpCache;
};
}
#endif
@@ -0,0 +1,754 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2018 Christoph Cullmann <cullmann@kde.org>
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen+kde@gmail.com>
SPDX-License-Identifier: MIT
*/
#include "context_p.h"
#include "definition_p.h"
#include "dynamicregexpcache_p.h"
#include "ksyntaxhighlighting_logging.h"
#include "rule_p.h"
#include "worddelimiters_p.h"
#include "xml_p.h"
using namespace KSyntaxHighlighting;
// QChar::isDigit() match any digit in unicode (romain numeral, etc)
static bool isDigit(QChar c)
{
return (c <= QLatin1Char('9') && QLatin1Char('0') <= c);
}
static bool isOctalChar(QChar c)
{
return (c <= QLatin1Char('7') && QLatin1Char('0') <= c);
}
static bool isHexChar(QChar c)
{
return isDigit(c) || (c <= QLatin1Char('f') && QLatin1Char('a') <= c) || (c <= QLatin1Char('F') && QLatin1Char('A') <= c);
}
static int matchEscapedChar(QStringView text, int offset)
{
if (text.at(offset) != QLatin1Char('\\') || text.size() < offset + 2) {
return offset;
}
const auto c = text.at(offset + 1);
switch (c.unicode()) {
// control chars
case 'a':
case 'b':
case 'e':
case 'f':
case 'n':
case 'r':
case 't':
case 'v':
case '"':
case '\'':
case '?':
case '\\':
return offset + 2;
// hex encoded character
case 'x':
if (offset + 2 < text.size() && isHexChar(text.at(offset + 2))) {
if (offset + 3 < text.size() && isHexChar(text.at(offset + 3))) {
return offset + 4;
}
return offset + 3;
}
return offset;
// octal encoding, simple \0 is OK, too, unlike simple \x above
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
if (offset + 2 < text.size() && isOctalChar(text.at(offset + 2))) {
if (offset + 3 < text.size() && isOctalChar(text.at(offset + 3))) {
return offset + 4;
}
return offset + 3;
}
return offset + 2;
}
return offset;
}
static QString replaceCaptures(const QString &pattern, const QStringList &captures, bool quote)
{
auto result = pattern;
for (int i = captures.size(); i >= 1; --i) {
result.replace(QLatin1Char('%') + QString::number(i), quote ? QRegularExpression::escape(captures.at(i - 1)) : captures.at(i - 1));
}
return result;
}
static MatchResult matchString(QStringView pattern, QStringView text, int offset, Qt::CaseSensitivity caseSensitivity)
{
if (offset + pattern.size() <= text.size() && text.sliced(offset, pattern.size()).compare(pattern, caseSensitivity) == 0) {
return offset + pattern.size();
}
return offset;
}
static void resolveAdditionalWordDelimiters(WordDelimiters &wordDelimiters, const HighlightingContextData::Rule::WordDelimiters &delimiters)
{
// cache for DefinitionData::wordDelimiters, is accessed VERY often
if (!delimiters.additionalDeliminator.isEmpty() || !delimiters.weakDeliminator.isEmpty()) {
wordDelimiters.append(QStringView(delimiters.additionalDeliminator));
wordDelimiters.remove(QStringView(delimiters.weakDeliminator));
}
}
Rule::~Rule() = default;
const IncludeRules *Rule::castToIncludeRules() const
{
if (m_type != Type::IncludeRules) {
return nullptr;
}
return static_cast<const IncludeRules *>(this);
}
bool Rule::resolveCommon(DefinitionData &def, const HighlightingContextData::Rule &ruleData, QStringView lookupContextName)
{
switch (ruleData.type) {
// IncludeRules uses this with a different semantic
case HighlightingContextData::Rule::Type::IncludeRules:
m_type = Type::IncludeRules;
return true;
case HighlightingContextData::Rule::Type::LineContinue:
m_type = Type::LineContinue;
break;
default:
m_type = Type::OtherRule;
break;
}
/**
* try to get our format from the definition we stem from
*/
if (!ruleData.common.attributeName.isEmpty()) {
m_attributeFormat = def.formatByName(ruleData.common.attributeName);
if (!m_attributeFormat.isValid()) {
qCWarning(Log) << "Rule: Unknown format" << ruleData.common.attributeName << "in context" << lookupContextName << "of definition" << def.name;
}
}
m_firstNonSpace = ruleData.common.firstNonSpace;
m_lookAhead = ruleData.common.lookAhead;
m_column = ruleData.common.column;
if (!ruleData.common.beginRegionName.isEmpty()) {
m_beginRegion = FoldingRegion(FoldingRegion::Begin, def.foldingRegionId(ruleData.common.beginRegionName));
}
if (!ruleData.common.endRegionName.isEmpty()) {
m_endRegion = FoldingRegion(FoldingRegion::End, def.foldingRegionId(ruleData.common.endRegionName));
}
m_context.resolve(def, ruleData.common.contextName);
return !(m_lookAhead && m_context.isStay());
}
static Rule::Ptr createRule(DefinitionData &def, const HighlightingContextData::Rule &ruleData, QStringView lookupContextName)
{
using Type = HighlightingContextData::Rule::Type;
switch (ruleData.type) {
case Type::AnyChar:
return std::make_shared<AnyChar>(ruleData.data.anyChar);
case Type::DetectChar:
return std::make_shared<DetectChar>(ruleData.data.detectChar);
case Type::Detect2Chars:
return std::make_shared<Detect2Chars>(ruleData.data.detect2Chars);
case Type::IncludeRules:
return std::make_shared<IncludeRules>(ruleData.data.includeRules);
case Type::Int:
return std::make_shared<Int>(def, ruleData.data.detectInt);
case Type::Keyword:
return KeywordListRule::create(def, ruleData.data.keyword, lookupContextName);
case Type::LineContinue:
return std::make_shared<LineContinue>(ruleData.data.lineContinue);
case Type::RangeDetect:
return std::make_shared<RangeDetect>(ruleData.data.rangeDetect);
case Type::RegExpr:
if (!ruleData.data.regExpr.dynamic) {
return std::make_shared<RegExpr>(ruleData.data.regExpr);
} else {
return std::make_shared<DynamicRegExpr>(ruleData.data.regExpr);
}
case Type::StringDetect:
if (ruleData.data.stringDetect.dynamic) {
return std::make_shared<DynamicStringDetect>(ruleData.data.stringDetect);
}
return std::make_shared<StringDetect>(ruleData.data.stringDetect);
case Type::WordDetect:
return std::make_shared<WordDetect>(def, ruleData.data.wordDetect);
case Type::Float:
return std::make_shared<Float>(def, ruleData.data.detectFloat);
case Type::HlCOct:
return std::make_shared<HlCOct>(def, ruleData.data.hlCOct);
case Type::HlCStringChar:
return std::make_shared<HlCStringChar>();
case Type::DetectIdentifier:
return std::make_shared<DetectIdentifier>();
case Type::DetectSpaces:
return std::make_shared<DetectSpaces>();
case Type::HlCChar:
return std::make_shared<HlCChar>();
case Type::HlCHex:
return std::make_shared<HlCHex>(def, ruleData.data.hlCHex);
case Type::Unknown:;
}
return Rule::Ptr(nullptr);
}
Rule::Ptr Rule::create(DefinitionData &def, const HighlightingContextData::Rule &ruleData, QStringView lookupContextName)
{
auto rule = createRule(def, ruleData, lookupContextName);
if (rule && !rule->resolveCommon(def, ruleData, lookupContextName)) {
rule.reset();
}
return rule;
}
AnyChar::AnyChar(const HighlightingContextData::Rule::AnyChar &data)
: m_chars(data.chars)
{
}
MatchResult AnyChar::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
if (m_chars.contains(text.at(offset))) {
return offset + 1;
}
return offset;
}
DetectChar::DetectChar(const HighlightingContextData::Rule::DetectChar &data)
: m_char(data.char1)
, m_captureIndex((data.dynamic ? data.char1.digitValue() : 0) - 1)
{
m_dynamic = data.dynamic;
}
MatchResult DetectChar::doMatch(QStringView text, int offset, const QStringList &captures, DynamicRegexpCache &) const
{
if (m_dynamic) {
if (m_captureIndex == -1 || 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;
}
Detect2Chars::Detect2Chars(const HighlightingContextData::Rule::Detect2Chars &data)
: m_char1(data.char1)
, m_char2(data.char2)
{
}
MatchResult Detect2Chars::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
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(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
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(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
while (offset < text.size() && text.at(offset).isSpace()) {
++offset;
}
return offset;
}
Float::Float(DefinitionData &def, const HighlightingContextData::Rule::Float &data)
: m_wordDelimiters(def.wordDelimiters)
{
resolveAdditionalWordDelimiters(m_wordDelimiters, data.wordDelimiters);
}
MatchResult Float::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
if (offset > 0 && !m_wordDelimiters.contains(text.at(offset - 1))) {
return offset;
}
auto newOffset = offset;
while (newOffset < text.size() && isDigit(text.at(newOffset))) {
++newOffset;
}
if (newOffset >= text.size() || text.at(newOffset) != QLatin1Char('.')) {
return offset;
}
++newOffset;
while (newOffset < text.size() && isDigit(text.at(newOffset))) {
++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() && isDigit(text.at(expOffset))) {
++expOffset;
foundExpDigit = true;
}
if (!foundExpDigit) {
return newOffset;
}
return expOffset;
}
MatchResult HlCChar::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
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;
}
HlCHex::HlCHex(DefinitionData &def, const HighlightingContextData::Rule::HlCHex &data)
: m_wordDelimiters(def.wordDelimiters)
{
resolveAdditionalWordDelimiters(m_wordDelimiters, data.wordDelimiters);
}
MatchResult HlCHex::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
if (offset > 0 && !m_wordDelimiters.contains(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;
}
HlCOct::HlCOct(DefinitionData &def, const HighlightingContextData::Rule::HlCOct &data)
: m_wordDelimiters(def.wordDelimiters)
{
resolveAdditionalWordDelimiters(m_wordDelimiters, data.wordDelimiters);
}
MatchResult HlCOct::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
if (offset > 0 && !m_wordDelimiters.contains(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(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
return matchEscapedChar(text, offset);
}
IncludeRules::IncludeRules(const HighlightingContextData::Rule::IncludeRules &data)
: m_contextName(data.contextName)
, m_includeAttribute(data.includeAttribute)
{
}
MatchResult IncludeRules::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
Q_UNUSED(text);
qCWarning(Log) << "Unresolved include rule";
return offset;
}
Int::Int(DefinitionData &def, const HighlightingContextData::Rule::Int &data)
: m_wordDelimiters(def.wordDelimiters)
{
resolveAdditionalWordDelimiters(m_wordDelimiters, data.wordDelimiters);
}
MatchResult Int::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
if (offset > 0 && !m_wordDelimiters.contains(text.at(offset - 1))) {
return offset;
}
while (offset < text.size() && isDigit(text.at(offset))) {
++offset;
}
return offset;
}
Rule::Ptr KeywordListRule::create(DefinitionData &def, const HighlightingContextData::Rule::Keyword &data, QStringView lookupContextName)
{
/**
* get our keyword list, if not found => bail out
*/
auto *keywordList = def.keywordList(data.name);
if (!keywordList) {
qCWarning(Log) << "Rule: Unknown keyword list" << data.name << "in context" << lookupContextName << "of definition" << def.name;
return Rule::Ptr();
}
if (keywordList->isEmpty()) {
return Rule::Ptr();
}
/**
* we might overwrite the case sensitivity
* then we need to init the list for lookup of that sensitivity setting
*/
if (data.hasCaseSensitivityOverride) {
keywordList->initLookupForCaseSensitivity(data.caseSensitivityOverride);
}
return std::make_shared<KeywordListRule>(*keywordList, def, data);
}
KeywordListRule::KeywordListRule(const KeywordList &keywordList, DefinitionData &def, const HighlightingContextData::Rule::Keyword &data)
: m_wordDelimiters(def.wordDelimiters)
, m_keywordList(keywordList)
, m_caseSensitivity(data.hasCaseSensitivityOverride ? data.caseSensitivityOverride : keywordList.caseSensitivity())
{
resolveAdditionalWordDelimiters(m_wordDelimiters, data.wordDelimiters);
m_hasSkipOffset = true;
}
MatchResult KeywordListRule::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
auto newOffset = offset;
while (text.size() > newOffset && !m_wordDelimiters.contains(text.at(newOffset))) {
++newOffset;
}
if (newOffset == offset) {
return offset;
}
if (m_keywordList.contains(text.sliced(offset, newOffset - offset), m_caseSensitivity)) {
return newOffset;
}
// we don't match, but we can skip until newOffset as we can't start a keyword in-between
return MatchResult(offset, newOffset);
}
LineContinue::LineContinue(const HighlightingContextData::Rule::LineContinue &data)
: m_char(data.char1)
{
}
MatchResult LineContinue::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
if (offset == text.size() - 1 && text.at(offset) == m_char) {
return offset + 1;
}
return offset;
}
RangeDetect::RangeDetect(const HighlightingContextData::Rule::RangeDetect &data)
: m_begin(data.begin)
, m_end(data.end)
{
}
MatchResult RangeDetect::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
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;
}
static QRegularExpression::PatternOptions makePattenOptions(const HighlightingContextData::Rule::RegExpr &data)
{
return (data.isMinimal ? QRegularExpression::InvertedGreedinessOption : QRegularExpression::NoPatternOption)
| (data.caseSensitivity == Qt::CaseInsensitive ? QRegularExpression::CaseInsensitiveOption : QRegularExpression::NoPatternOption)
// DontCaptureOption is removed by resolve() when necessary
| QRegularExpression::DontCaptureOption
// ensure Unicode support is enabled
| QRegularExpression::UseUnicodePropertiesOption;
}
static void resolveRegex(QRegularExpression &regexp, Context *context)
{
bool enableCapture = context && context->hasDynamicRule();
// disable DontCaptureOption when reference a context with dynamic rule or
// with invalid regex because DontCaptureOption with back reference capture is an error
if (enableCapture || !regexp.isValid()) {
regexp.setPatternOptions(regexp.patternOptions() & ~QRegularExpression::DontCaptureOption);
}
if (!regexp.isValid()) {
qCDebug(Log) << "Invalid regexp:" << regexp.pattern();
}
}
static MatchResult regexMatch(const QRegularExpression &regexp, QStringView text, int offset)
{
/**
* match the pattern
*/
const auto result = regexp.matchView(text, offset, QRegularExpression::NormalMatch, QRegularExpression::DontCheckSubjectStringMatchOption);
if (result.capturedStart() == offset) {
/**
* we only need to compute the captured texts if we have real capture groups
* highlightings should only address %1..%.., see e.g. replaceCaptures
* DetectChar ignores %0, too
*/
int lastCapturedIndex = result.lastCapturedIndex();
if (lastCapturedIndex > 0) {
QStringList captures;
captures.reserve(lastCapturedIndex);
// ignore the capturing group number 0
for (int i = 1; i <= lastCapturedIndex; ++i)
captures.push_back(result.captured(i));
return MatchResult(offset + result.capturedLength(), std::move(captures));
}
/**
* else: ignore the implicit 0 group we always capture, no need to allocate stuff for that
*/
return MatchResult(offset + result.capturedLength());
}
/**
* no match
* we can always compute the skip offset as the highlighter will invalidate the cache for changed captures for dynamic rules!
*/
return MatchResult(offset, result.capturedStart());
}
RegExpr::RegExpr(const HighlightingContextData::Rule::RegExpr &data)
: m_regexp(data.pattern, makePattenOptions(data))
{
m_hasSkipOffset = true;
}
void RegExpr::resolve()
{
m_isResolved = true;
resolveRegex(m_regexp, context().context());
}
MatchResult RegExpr::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
if (Q_UNLIKELY(!m_isResolved)) {
const_cast<RegExpr *>(this)->resolve();
}
return regexMatch(m_regexp, text, offset);
}
DynamicRegExpr::DynamicRegExpr(const HighlightingContextData::Rule::RegExpr &data)
: m_pattern(data.pattern)
, m_patternOptions(makePattenOptions(data))
{
m_dynamic = true;
m_hasSkipOffset = true;
}
void DynamicRegExpr::resolve()
{
m_isResolved = true;
QRegularExpression regexp(m_pattern, m_patternOptions);
resolveRegex(regexp, context().context());
m_patternOptions = regexp.patternOptions();
}
MatchResult DynamicRegExpr::doMatch(QStringView text, int offset, const QStringList &captures, DynamicRegexpCache &dynamicRegexpCache) const
{
if (Q_UNLIKELY(!m_isResolved)) {
const_cast<DynamicRegExpr *>(this)->resolve();
}
/**
* create new pattern with right instantiation
*/
auto pattern = replaceCaptures(m_pattern, captures, true);
auto &regexp = dynamicRegexpCache.compileRegexp(std::move(pattern), m_patternOptions);
return regexMatch(regexp, text, offset);
}
StringDetect::StringDetect(const HighlightingContextData::Rule::StringDetect &data)
: m_string(data.string)
, m_caseSensitivity(data.caseSensitivity)
{
}
MatchResult StringDetect::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
return matchString(m_string, text, offset, m_caseSensitivity);
}
DynamicStringDetect::DynamicStringDetect(const HighlightingContextData::Rule::StringDetect &data)
: m_string(data.string)
, m_caseSensitivity(data.caseSensitivity)
{
m_dynamic = true;
}
MatchResult DynamicStringDetect::doMatch(QStringView text, int offset, const QStringList &captures, DynamicRegexpCache &) const
{
/**
* for dynamic case: create new pattern with right instantiation
*/
const auto pattern = replaceCaptures(m_string, captures, false);
return matchString(pattern, text, offset, m_caseSensitivity);
}
WordDetect::WordDetect(DefinitionData &def, const HighlightingContextData::Rule::WordDetect &data)
: m_wordDelimiters(def.wordDelimiters)
, m_word(data.word)
, m_caseSensitivity(data.caseSensitivity)
{
resolveAdditionalWordDelimiters(m_wordDelimiters, data.wordDelimiters);
}
MatchResult WordDetect::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
{
if (text.size() - offset < m_word.size()) {
return offset;
}
/**
* detect delimiter characters on the inner and outer boundaries of the string
* NOTE: m_word isn't empty
*/
if (offset > 0 && !m_wordDelimiters.contains(text.at(offset - 1)) && !m_wordDelimiters.contains(text.at(offset))) {
return offset;
}
if (text.sliced(offset, m_word.size()).compare(m_word, m_caseSensitivity) != 0) {
return offset;
}
if (text.size() == offset + m_word.size() || m_wordDelimiters.contains(text.at(offset + m_word.size()))
|| m_wordDelimiters.contains(text.at(offset + m_word.size() - 1))) {
return offset + m_word.size();
}
return offset;
}
@@ -0,0 +1,369 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_RULE_P_H
#define KSYNTAXHIGHLIGHTING_RULE_P_H
#include "contextswitch_p.h"
#include "foldingregion.h"
#include "format.h"
#include "highlightingdata_p.hpp"
#include "keywordlist_p.h"
#include "matchresult_p.h"
#include "worddelimiters_p.h"
#include <QRegularExpression>
#include <QString>
#include <memory>
namespace KSyntaxHighlighting
{
class WordDelimiters;
class DefinitionData;
class IncludeRules;
class DynamicRegexpCache;
class Rule
{
public:
Rule() = default;
virtual ~Rule();
typedef std::shared_ptr<Rule> Ptr;
const Format &attributeFormat() const
{
return m_attributeFormat;
}
const ContextSwitch &context() const
{
return m_context;
}
bool isLookAhead() const
{
return m_lookAhead;
}
bool isDynamic() const
{
return m_dynamic;
}
bool firstNonSpace() const
{
return m_firstNonSpace;
}
int requiredColumn() const
{
return m_column;
}
const FoldingRegion &beginRegion() const
{
return m_beginRegion;
}
const FoldingRegion &endRegion() const
{
return m_endRegion;
}
const IncludeRules *castToIncludeRules() const;
bool isLineContinue() const
{
return m_type == Type::LineContinue;
}
// If true, then the rule uses the skipOffset parameter of MatchResult.
// This is used by AbstractHighlighter::highlightLine() to look for a rule
// in the skipOffsets cache only if it can be found there.
bool hasSkipOffset() const
{
return m_hasSkipOffset;
}
virtual MatchResult doMatch(QStringView text, int offset, const QStringList &captures, DynamicRegexpCache &dynamicRegexpCache) const = 0;
static Rule::Ptr create(DefinitionData &def, const HighlightingContextData::Rule &ruleData, QStringView lookupContextName);
private:
Q_DISABLE_COPY(Rule)
bool resolveCommon(DefinitionData &def, const HighlightingContextData::Rule &ruleData, QStringView lookupContextName);
enum class Type : quint8 {
OtherRule,
LineContinue,
IncludeRules,
};
private:
Format m_attributeFormat;
ContextSwitch m_context;
int m_column = -1;
FoldingRegion m_beginRegion;
FoldingRegion m_endRegion;
Type m_type;
bool m_firstNonSpace = false;
bool m_lookAhead = false;
protected:
bool m_hasSkipOffset = false;
bool m_dynamic = false;
};
class AnyChar final : public Rule
{
public:
AnyChar(const HighlightingContextData::Rule::AnyChar &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
WordDelimiters m_chars;
};
class DetectChar final : public Rule
{
public:
DetectChar(const HighlightingContextData::Rule::DetectChar &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
QChar m_char;
int m_captureIndex = 0;
};
class Detect2Chars final : public Rule
{
public:
Detect2Chars(const HighlightingContextData::Rule::Detect2Chars &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
QChar m_char1;
QChar m_char2;
};
class DetectIdentifier final : public Rule
{
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
};
class DetectSpaces final : public Rule
{
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
};
class Float final : public Rule
{
public:
Float(DefinitionData &def, const HighlightingContextData::Rule::Float &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
WordDelimiters m_wordDelimiters;
};
class IncludeRules final : public Rule
{
public:
IncludeRules(const HighlightingContextData::Rule::IncludeRules &data);
const QString &contextName() const
{
return m_contextName;
}
bool includeAttribute() const
{
return m_includeAttribute;
}
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
QString m_contextName;
bool m_includeAttribute;
};
class Int final : public Rule
{
public:
Int(DefinitionData &def, const HighlightingContextData::Rule::Int &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
WordDelimiters m_wordDelimiters;
};
class HlCChar final : public Rule
{
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
};
class HlCHex final : public Rule
{
public:
HlCHex(DefinitionData &def, const HighlightingContextData::Rule::HlCHex &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
WordDelimiters m_wordDelimiters;
};
class HlCOct final : public Rule
{
public:
HlCOct(DefinitionData &def, const HighlightingContextData::Rule::HlCOct &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
WordDelimiters m_wordDelimiters;
};
class HlCStringChar final : public Rule
{
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
};
class KeywordListRule final : public Rule
{
public:
KeywordListRule(const KeywordList &keywordList, DefinitionData &def, const HighlightingContextData::Rule::Keyword &data);
static Rule::Ptr create(DefinitionData &def, const HighlightingContextData::Rule::Keyword &data, QStringView lookupContextName);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
WordDelimiters m_wordDelimiters;
const KeywordList &m_keywordList;
Qt::CaseSensitivity m_caseSensitivity;
};
class LineContinue final : public Rule
{
public:
LineContinue(const HighlightingContextData::Rule::LineContinue &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
QChar m_char;
};
class RangeDetect final : public Rule
{
public:
RangeDetect(const HighlightingContextData::Rule::RangeDetect &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
QChar m_begin;
QChar m_end;
};
class RegExpr final : public Rule
{
public:
RegExpr(const HighlightingContextData::Rule::RegExpr &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
void resolve();
QRegularExpression m_regexp;
bool m_isResolved = false;
};
class DynamicRegExpr final : public Rule
{
public:
DynamicRegExpr(const HighlightingContextData::Rule::RegExpr &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
void resolve();
QString m_pattern;
QRegularExpression::PatternOptions m_patternOptions;
bool m_isResolved = false;
};
class StringDetect final : public Rule
{
public:
StringDetect(const HighlightingContextData::Rule::StringDetect &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
QString m_string;
Qt::CaseSensitivity m_caseSensitivity;
};
class DynamicStringDetect final : public Rule
{
public:
DynamicStringDetect(const HighlightingContextData::Rule::StringDetect &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
QString m_string;
Qt::CaseSensitivity m_caseSensitivity;
};
class WordDetect final : public Rule
{
public:
WordDetect(DefinitionData &def, const HighlightingContextData::Rule::WordDetect &data);
protected:
MatchResult doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const override;
private:
WordDelimiters m_wordDelimiters;
QString m_word;
Qt::CaseSensitivity m_caseSensitivity;
};
}
#endif // KSYNTAXHIGHLIGHTING_RULE_P_H
@@ -0,0 +1,84 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2018 Christoph Cullmann <cullmann@kde.org>
SPDX-License-Identifier: MIT
*/
#include "state.h"
#include "state_p.h"
#include "context_p.h"
#include <QStringList>
using namespace KSyntaxHighlighting;
StateData *StateData::reset(State &state)
{
auto *p = new StateData();
state.d.reset(p);
return p;
}
StateData *StateData::detach(State &state)
{
state.d.detach();
return state.d.data();
}
void StateData::push(Context *context, QStringList &&captures)
{
Q_ASSERT(context);
m_contextStack.push_back(StackValue{context, std::move(captures)});
}
bool StateData::pop(int popCount)
{
// nop if nothing to pop
if (popCount <= 0) {
return true;
}
// keep the initial context alive in any case
Q_ASSERT(!m_contextStack.empty());
const bool initialContextSurvived = int(m_contextStack.size()) > popCount;
m_contextStack.resize(std::max(1, int(m_contextStack.size()) - popCount));
return initialContextSurvived;
}
State::State() = default;
State::State(State &&other) noexcept = default;
State::State(const State &other) noexcept = default;
State::~State() = default;
State &State::operator=(State &&other) noexcept = default;
State &State::operator=(const State &other) noexcept = default;
bool State::operator==(const State &other) const
{
// use pointer equal as shortcut for shared states
return (d == other.d) || (d && other.d && d->m_contextStack == other.d->m_contextStack && d->m_defId == other.d->m_defId);
}
bool State::operator!=(const State &other) const
{
return !(*this == other);
}
bool State::indentationBasedFoldingEnabled() const
{
if (!d || d->m_contextStack.empty()) {
return false;
}
return d->m_contextStack.back().context->indentationBasedFoldingEnabled();
}
std::size_t KSyntaxHighlighting::qHash(const State &state, std::size_t seed)
{
return state.d ? qHashMulti(seed, *state.d) : 0;
}
@@ -0,0 +1,74 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_STATE_H
#define KSYNTAXHIGHLIGHTING_STATE_H
#include "ksyntaxhighlighting_export.h"
#include <QExplicitlySharedDataPointer>
#include <QHash>
namespace KSyntaxHighlighting
{
class State;
class StateData;
KSYNTAXHIGHLIGHTING_EXPORT std::size_t qHash(const State &state, std::size_t seed = 0);
/** Opaque handle to the state of the highlighting engine.
* This needs to be fed into AbstractHighlighter for every line of text
* and allows concrete highlighter implementations to store state per
* line for fast re-highlighting of specific lines (e.g. during editing).
*
* @since 5.28
*/
class KSYNTAXHIGHLIGHTING_EXPORT State
{
public:
/** Creates an initial state, ie. what should be used for the first line
* in a document.
*/
State();
State(State &&other) noexcept;
State(const State &other) noexcept;
~State();
State &operator=(State &&rhs) noexcept;
State &operator=(const State &rhs) noexcept;
/** Compares two states for equality.
* For two equal states and identical text input, AbstractHighlighter
* guarantees to produce equal results. This can be used to only
* re-highlight as many lines as necessary during editing.
*/
bool operator==(const State &other) const;
/** Compares two states for inequality.
* This is the opposite of operator==().
*/
bool operator!=(const State &other) const;
/**
* Returns whether or not indentation-based folding is enabled in this state.
* When using a Definition with indentation-based folding, use
* this method to check if indentation-based folding has been
* suspended in the current line.
*
* @see Definition::indentationBasedFoldingEnabled()
*/
bool indentationBasedFoldingEnabled() const;
private:
friend class StateData;
friend KSYNTAXHIGHLIGHTING_EXPORT std::size_t qHash(const State &, std::size_t);
QExplicitlySharedDataPointer<StateData> d;
};
}
QT_BEGIN_NAMESPACE
Q_DECLARE_TYPEINFO(KSyntaxHighlighting::State, Q_RELOCATABLE_TYPE);
QT_END_NAMESPACE
#endif // KSYNTAXHIGHLIGHTING_STATE_H
@@ -0,0 +1,95 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2018 Christoph Cullmann <cullmann@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_STATE_P_H
#define KSYNTAXHIGHLIGHTING_STATE_P_H
#include <vector>
#include <QSharedData>
#include <QStringList>
namespace KSyntaxHighlighting
{
class Context;
class StateData : public QSharedData
{
friend class State;
friend class AbstractHighlighter;
friend std::size_t qHash(const StateData &, std::size_t);
public:
StateData() = default;
static StateData *reset(State &state);
static StateData *detach(State &state);
static StateData *get(const State &state)
{
return state.d.data();
}
std::size_t size() const
{
return m_contextStack.size();
}
void push(Context *context, QStringList &&captures);
/**
* Pop the number of elements given from the top of the current stack.
* Will not pop the initial element.
* @param popCount number of elements to pop
* @return false if one has tried to pop the initial context, else true
*/
bool pop(int popCount);
Context *topContext() const
{
return m_contextStack.back().context;
}
const QStringList &topCaptures() const
{
return m_contextStack.back().captures;
}
struct StackValue {
Context *context;
QStringList captures;
bool operator==(const StackValue &other) const
{
return context == other.context && captures == other.captures;
}
};
private:
/**
* definition id to filter out invalid states
*/
uint64_t m_defId = 0;
/**
* the context stack combines the active context + valid captures
*/
std::vector<StackValue> m_contextStack;
};
inline std::size_t qHash(const StateData::StackValue &stackValue, std::size_t seed = 0)
{
return qHashMulti(seed, stackValue.context, stackValue.captures);
}
inline std::size_t qHash(const StateData &k, std::size_t seed = 0)
{
return qHashMulti(seed, k.m_defId, qHashRange(k.m_contextStack.begin(), k.m_contextStack.end(), seed));
}
}
#endif
@@ -0,0 +1,271 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#include "syntaxhighlighter.h"
#include "abstracthighlighter_p.h"
#include "definition.h"
#include "definition_p.h"
#include "foldingregion.h"
#include "format.h"
#include "format_p.h"
#include "state.h"
#include "theme.h"
#include "themedata_p.h"
Q_DECLARE_METATYPE(QTextBlock)
using namespace KSyntaxHighlighting;
namespace KSyntaxHighlighting
{
class TextBlockUserData : public QTextBlockUserData
{
public:
State state;
QList<FoldingRegion> foldingRegions;
};
class SyntaxHighlighterPrivate : public AbstractHighlighterPrivate
{
public:
static FoldingRegion foldingRegion(const QTextBlock &startBlock);
void initTextFormat(QTextCharFormat &tf, const Format &format);
void computeTextFormats();
struct TextFormat {
QTextCharFormat tf;
/**
* id to check that the format belongs to the definition
*/
std::intptr_t ptrId;
};
QList<FoldingRegion> foldingRegions;
std::vector<TextFormat> tfs;
};
}
FoldingRegion SyntaxHighlighterPrivate::foldingRegion(const QTextBlock &startBlock)
{
const auto data = dynamic_cast<TextBlockUserData *>(startBlock.userData());
if (!data) {
return FoldingRegion();
}
for (int i = data->foldingRegions.size() - 1; i >= 0; --i) {
if (data->foldingRegions.at(i).type() == FoldingRegion::Begin) {
return data->foldingRegions.at(i);
}
}
return FoldingRegion();
}
void SyntaxHighlighterPrivate::initTextFormat(QTextCharFormat &tf, const Format &format)
{
// always set the foreground color to avoid palette issues
tf.setForeground(format.textColor(m_theme));
if (format.hasBackgroundColor(m_theme)) {
tf.setBackground(format.backgroundColor(m_theme));
}
if (format.isBold(m_theme)) {
tf.setFontWeight(QFont::Bold);
}
if (format.isItalic(m_theme)) {
tf.setFontItalic(true);
}
if (format.isUnderline(m_theme)) {
tf.setFontUnderline(true);
}
if (format.isStrikeThrough(m_theme)) {
tf.setFontStrikeOut(true);
}
}
void SyntaxHighlighterPrivate::computeTextFormats()
{
auto definitions = m_definition.includedDefinitions();
definitions.append(m_definition);
int maxId = 0;
for (const auto &definition : std::as_const(definitions)) {
for (const auto &format : std::as_const(DefinitionData::get(definition)->formats)) {
maxId = qMax(maxId, format.id());
}
}
tfs.clear();
tfs.resize(maxId + 1);
// initialize tfs
for (const auto &definition : std::as_const(definitions)) {
for (const auto &format : std::as_const(DefinitionData::get(definition)->formats)) {
auto &tf = tfs[format.id()];
tf.ptrId = FormatPrivate::ptrId(format);
initTextFormat(tf.tf, format);
}
}
}
SyntaxHighlighter::SyntaxHighlighter(QObject *parent)
: QSyntaxHighlighter(parent)
, AbstractHighlighter(new SyntaxHighlighterPrivate)
{
qRegisterMetaType<QTextBlock>();
}
SyntaxHighlighter::SyntaxHighlighter(QTextDocument *document)
: QSyntaxHighlighter(document)
, AbstractHighlighter(new SyntaxHighlighterPrivate)
{
qRegisterMetaType<QTextBlock>();
}
SyntaxHighlighter::~SyntaxHighlighter()
{
}
void SyntaxHighlighter::setDefinition(const Definition &def)
{
Q_D(SyntaxHighlighter);
const auto needsRehighlight = d->m_definition != def;
if (DefinitionData::get(d->m_definition) != DefinitionData::get(def)) {
d->m_definition = def;
d->tfs.clear();
}
if (needsRehighlight) {
rehighlight();
}
}
void SyntaxHighlighter::setTheme(const Theme &theme)
{
Q_D(SyntaxHighlighter);
if (ThemeData::get(d->m_theme) != ThemeData::get(theme)) {
d->m_theme = theme;
d->tfs.clear();
}
}
bool SyntaxHighlighter::startsFoldingRegion(const QTextBlock &startBlock) const
{
return SyntaxHighlighterPrivate::foldingRegion(startBlock).type() == FoldingRegion::Begin;
}
QTextBlock SyntaxHighlighter::findFoldingRegionEnd(const QTextBlock &startBlock) const
{
const auto region = SyntaxHighlighterPrivate::foldingRegion(startBlock);
auto block = startBlock;
int depth = 1;
while (block.isValid()) {
block = block.next();
const auto data = dynamic_cast<TextBlockUserData *>(block.userData());
if (!data) {
continue;
}
for (const auto &foldingRegion : std::as_const(data->foldingRegions)) {
if (foldingRegion.id() != region.id()) {
continue;
}
if (foldingRegion.type() == FoldingRegion::End) {
--depth;
} else if (foldingRegion.type() == FoldingRegion::Begin) {
++depth;
}
if (depth == 0) {
return block;
}
}
}
return QTextBlock();
}
void SyntaxHighlighter::highlightBlock(const QString &text)
{
Q_D(SyntaxHighlighter);
static const State emptyState;
const State *previousState = &emptyState;
if (currentBlock().position() > 0) {
const auto prevBlock = currentBlock().previous();
const auto prevData = dynamic_cast<TextBlockUserData *>(prevBlock.userData());
if (prevData) {
previousState = &prevData->state;
}
}
d->foldingRegions.clear();
auto newState = highlightLine(text, *previousState);
auto data = dynamic_cast<TextBlockUserData *>(currentBlockUserData());
if (!data) { // first time we highlight this
data = new TextBlockUserData;
data->state = std::move(newState);
data->foldingRegions = d->foldingRegions;
setCurrentBlockUserData(data);
return;
}
if (data->state == newState && data->foldingRegions == d->foldingRegions) { // we ended up in the same state, so we are done here
return;
}
data->state = std::move(newState);
data->foldingRegions = d->foldingRegions;
const auto nextBlock = currentBlock().next();
if (nextBlock.isValid()) {
QMetaObject::invokeMethod(this, "rehighlightBlock", Qt::QueuedConnection, Q_ARG(QTextBlock, nextBlock));
}
}
void SyntaxHighlighter::applyFormat(int offset, int length, const Format &format)
{
if (length == 0) {
return;
}
Q_D(SyntaxHighlighter);
if (Q_UNLIKELY(d->tfs.empty())) {
d->computeTextFormats();
}
const auto id = static_cast<std::size_t>(format.id());
// This doesn't happen when format comes from the definition.
// But as the user can override the function to pass any format, this is a possible scenario.
if (id < d->tfs.size() && d->tfs[id].ptrId == FormatPrivate::ptrId(format)) {
QSyntaxHighlighter::setFormat(offset, length, d->tfs[id].tf);
} else {
QTextCharFormat tf;
d->initTextFormat(tf, format);
QSyntaxHighlighter::setFormat(offset, length, tf);
}
}
void SyntaxHighlighter::applyFolding(int offset, int length, FoldingRegion region)
{
Q_UNUSED(offset);
Q_UNUSED(length);
Q_D(SyntaxHighlighter);
if (region.type() == FoldingRegion::Begin) {
d->foldingRegions.push_back(region);
}
if (region.type() == FoldingRegion::End) {
for (int i = d->foldingRegions.size() - 1; i >= 0; --i) {
if (d->foldingRegions.at(i).id() != region.id() || d->foldingRegions.at(i).type() != FoldingRegion::Begin) {
continue;
}
d->foldingRegions.remove(i);
return;
}
d->foldingRegions.push_back(region);
}
}
#include "moc_syntaxhighlighter.cpp"
@@ -0,0 +1,69 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_QSYNTAXHIGHLIGHTER_H
#define KSYNTAXHIGHLIGHTING_QSYNTAXHIGHLIGHTER_H
#include "ksyntaxhighlighting_export.h"
#include "abstracthighlighter.h"
#include <QSyntaxHighlighter>
namespace KSyntaxHighlighting
{
class SyntaxHighlighterPrivate;
/** A QSyntaxHighlighter implementation for use with QTextDocument.
* This supports partial re-highlighting during editing and
* tracks syntax-based code folding regions.
*
* @since 5.28
*/
class KSYNTAXHIGHLIGHTING_EXPORT SyntaxHighlighter : public QSyntaxHighlighter, public AbstractHighlighter
{
Q_OBJECT
public:
explicit SyntaxHighlighter(QObject *parent = nullptr);
explicit SyntaxHighlighter(QTextDocument *document);
~SyntaxHighlighter() override;
void setDefinition(const Definition &def) override;
void setTheme(const Theme &theme) override;
/** Returns whether there is a folding region beginning at @p startBlock.
* This only considers syntax-based folding regions,
* not indention-based ones as e.g. found in Python.
*
* @see findFoldingRegionEnd
*/
bool startsFoldingRegion(const QTextBlock &startBlock) const;
/** Finds the end of the folding region starting at @p startBlock.
* If multiple folding regions begin at @p startBlock, the end of
* the last/innermost one is returned.
* This returns an invalid block if no folding region end is found,
* which typically indicates an unterminated region and thus folding
* until the document end.
* This method performs a sequential search starting at @p startBlock
* for the matching folding region end, which is a potentially expensive
* operation.
*
* @see startsFoldingRegion
*/
QTextBlock findFoldingRegionEnd(const QTextBlock &startBlock) const;
protected:
void highlightBlock(const QString &text) override;
void applyFormat(int offset, int length, const Format &format) override;
void applyFolding(int offset, int length, FoldingRegion region) override;
private:
Q_DECLARE_PRIVATE_D(AbstractHighlighter::d_ptr, SyntaxHighlighter)
};
}
#endif // KSYNTAXHIGHLIGHTING_QSYNTAXHIGHLIGHTER_H
@@ -0,0 +1,47 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_TEXTSTYLEDATA_P_H
#define KSYNTAXHIGHLIGHTING_TEXTSTYLEDATA_P_H
#include <QColor>
namespace KSyntaxHighlighting
{
class TextStyleData
{
public:
// Constructor initializing all data.
TextStyleData() noexcept
: bold(false)
, italic(false)
, underline(false)
, strikeThrough(false)
, hasBold(false)
, hasItalic(false)
, hasUnderline(false)
, hasStrikeThrough(false)
{
}
QRgb textColor = 0x0;
QRgb backgroundColor = 0x0;
QRgb selectedTextColor = 0x0;
QRgb selectedBackgroundColor = 0x0;
bool bold : 1;
bool italic : 1;
bool underline : 1;
bool strikeThrough : 1;
bool hasBold : 1;
bool hasItalic : 1;
bool hasUnderline : 1;
bool hasStrikeThrough : 1;
};
}
#endif
@@ -0,0 +1,107 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2022 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#include "theme.h"
#include "themedata_p.h"
#include <QCoreApplication>
using namespace KSyntaxHighlighting;
static QExplicitlySharedDataPointer<ThemeData> &sharedDefaultThemeData()
{
static QExplicitlySharedDataPointer<ThemeData> data(new ThemeData);
return data;
}
Theme::Theme()
: m_data(sharedDefaultThemeData())
{
}
Theme::Theme(const Theme &copy) = default;
Theme::Theme(ThemeData *data)
: m_data(data)
{
}
Theme::~Theme() = default;
Theme &Theme::operator=(const Theme &other) = default;
bool Theme::isValid() const
{
return m_data.data() != sharedDefaultThemeData().data();
}
QString Theme::name() const
{
return m_data->name();
}
QString Theme::translatedName() const
{
return isValid() ? QCoreApplication::instance()->translate("Theme", m_data->name().toUtf8().constData()) : QString();
}
bool Theme::isReadOnly() const
{
return m_data->isReadOnly();
}
QString Theme::filePath() const
{
return m_data->filePath();
}
QRgb Theme::textColor(TextStyle style) const
{
return m_data->textColor(style);
}
QRgb Theme::selectedTextColor(TextStyle style) const
{
return m_data->selectedTextColor(style);
}
QRgb Theme::backgroundColor(TextStyle style) const
{
return m_data->backgroundColor(style);
}
QRgb Theme::selectedBackgroundColor(TextStyle style) const
{
return m_data->selectedBackgroundColor(style);
}
bool Theme::isBold(TextStyle style) const
{
return m_data->isBold(style);
}
bool Theme::isItalic(TextStyle style) const
{
return m_data->isItalic(style);
}
bool Theme::isUnderline(TextStyle style) const
{
return m_data->isUnderline(style);
}
bool Theme::isStrikeThrough(TextStyle style) const
{
return m_data->isStrikeThrough(style);
}
QRgb Theme::editorColor(EditorColorRole role) const
{
return m_data->editorColor(role);
}
#include "moc_theme.cpp"
@@ -0,0 +1,357 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_THEME_H
#define KSYNTAXHIGHLIGHTING_THEME_H
#include "ksyntaxhighlighting_export.h"
#include <QColor>
#include <QExplicitlySharedDataPointer>
#include <QTypeInfo>
#include <qobjectdefs.h>
namespace KSyntaxHighlighting
{
class ThemeData;
class RepositoryPrivate;
/**
* Color theme definition used for highlighting.
*
* @section theme_intro Introduction
*
* The Theme provides a full color theme for painting the highlighted text.
* One Theme is defined either as a *.theme file on disk, or as a file compiled
* into the SyntaxHighlighting library by using Qt's resource system. Each
* Theme has a unique name(), including a translatedName() if put into the UI.
* Themes shipped by default are typically read-only, see isReadOnly().
*
* A Theme defines two sets of colors:
* - Text colors, including foreground and background colors, colors for
* selected text, and properties such as bold and italic. These colors are
* used e.g. by the SyntaxHighlighter.
* - Editor colors, including a background color for the entire editor widget,
* the line number color, code folding colors, etc.
*
* @section theme_text_colors Text Colors and the Class Format
*
* The text colors are used for syntax highlighting.
* // TODO: elaborate more and explain relation to Format class
*
* @section theme_editor_colors Editor Colors
*
* If you want to use the SyntaxHighlighting framework to write your own text
* editor, you also need to paint the background of the editing widget. In
* addition, the editor may support showing line numbers, a folding bar, a
* highlight for the current text line, and similar features. All these colors
* are defined in terms of the "editor colors" and accessible by calling
* editorColor() with the desired enum EditorColorRole.
*
* @section theme_access Accessing a Theme
*
* All available Theme%s are accessed through the Repository. These themes are
* typically valid themes. If you create a Theme on your own, isValid() will
* return @e false, and all colors provided by this Theme are in fact invalid
* and therefore unusable.
*
* @see Format
* @since 5.28
*/
class KSYNTAXHIGHLIGHTING_EXPORT Theme
{
Q_GADGET
Q_PROPERTY(QString name READ name)
Q_PROPERTY(QString translatedName READ translatedName)
public:
/**
* Default styles that can be referenced from syntax definition XML files.
* Make sure to choose readable colors with good contrast especially in
* combination with the EditorColorRole%s.
*/
enum TextStyle {
//! Default text style for normal text and source code without
//! special highlighting.
Normal = 0,
//! Text style for language keywords.
Keyword,
//! Text style for function definitions and function calls.
Function,
//! Text style for variables, if applicable. For instance, variables in
//! PHP typically start with a '$', so all identifiers following the
//! pattern $foo are highlighted as variable.
Variable,
//! Text style for control flow highlighting, such as @e if, @e then,
//! @e else, @e return, or @e continue.
ControlFlow,
//! Text style for operators such as +, -, *, / and :: etc.
Operator,
//! Text style for built-in language classes and functions.
BuiltIn,
//! Text style for well-known extensions, such as Qt or boost.
Extension,
//! Text style for preprocessor statements.
Preprocessor,
//! Text style for attributes of functions or objects, e.g. \@override
//! in Java, or __declspec(...) and __attribute__((...)) in C++.
Attribute,
//! Text style for single characters such as 'a'.
Char,
//! Text style for escaped characters in strings, such as "hello\n".
SpecialChar,
//! Text style for strings, for instance "hello world".
String,
//! Text style for verbatim strings such as HERE docs.
VerbatimString,
//! Text style for special strings such as regular expressions in
//! ECMAScript or the LaTeX math mode.
SpecialString,
//! Text style for includes, imports, modules, or LaTeX packages.
Import,
//! Text style for data types such as int, char, float etc.
DataType,
//! Text style for decimal values.
DecVal,
//! Text style for numbers with base other than 10.
BaseN,
//! Text style for floating point numbers.
Float,
//! Text style for language constants, e.g. True, False, None in Python
//! or nullptr in C/C++.
Constant,
//! Text style for normal comments.
Comment,
//! Text style for comments that reflect API documentation, such as
//! doxygen /** */ comments.
Documentation,
//! Text style for annotations in comments, such as \@param in Doxygen
//! or JavaDoc.
Annotation,
//! Text style that refers to variables in a comment, such as after
//! \@param \<identifier\> in Doxygen or JavaDoc.
CommentVar,
//! Text style for region markers, typically defined by BEGIN/END.
RegionMarker,
//! Text style for information, such as the keyword \@note in Doxygen.
Information,
//! Text style for warnings, such as the keyword \@warning in Doxygen.
Warning,
//! Text style for comment specials such as TODO and WARNING in
//! comments.
Alert,
//! Text style indicating wrong syntax.
Error,
//! Text style for attributes that do not match any of the other default
//! styles.
Others
};
Q_ENUM(TextStyle)
/**
* Editor color roles, used to paint line numbers, editor background etc.
* The colors typically should have good contrast with the colors used
* in the TextStyle%s.
*/
enum EditorColorRole {
//! Background color for the editing area.
BackgroundColor = 0,
//! Background color for selected text.
TextSelection,
//! Background color for the line of the current text cursor.
CurrentLine,
//! Background color for matching text while searching.
SearchHighlight,
//! Background color for replaced text for a search & replace action.
ReplaceHighlight,
//! Background color for matching bracket pairs (including quotes)
BracketMatching,
//! Foreground color for visualizing tabs and trailing spaces.
TabMarker,
//! Color used to underline spell check errors.
SpellChecking,
//! Color used to draw vertical indentation levels, typically a line.
IndentationLine,
//! Background color for the icon border.
IconBorder,
//! Background colors for code folding regions in the text area, as well
//! as code folding indicators in the code folding border.
CodeFolding,
//! Foreground color for drawing the line numbers. This should have a
//! good contrast with the IconBorder background color.
LineNumbers,
//! Foreground color for drawing the current line number. This should
//! have a good contrast with the IconBorder background color.
CurrentLineNumber,
//! Color used in the icon border to indicate dynamically wrapped lines.
//! This color should have a good contrast with the IconBorder
//! background color.
WordWrapMarker,
//! Color used to draw a vertical line for marking changed lines.
ModifiedLines,
//! Color used to draw a vertical line for marking saved lines.
SavedLines,
//! Line color used to draw separator lines, e.g. at column 80 in the
//! text editor area.
Separator,
//! Background color for bookmarks.
MarkBookmark,
//! Background color for active breakpoints.
MarkBreakpointActive,
//! Background color for a reached breakpoint.
MarkBreakpointReached,
//! Background color for inactive (disabled) breakpoints.
MarkBreakpointDisabled,
//! Background color for marking the current execution position.
MarkExecution,
//! Background color for general warning marks.
MarkWarning,
//! Background color for general error marks.
MarkError,
//! Background color for text templates (snippets).
TemplateBackground,
//! Background color for all editable placeholders in text templates.
TemplatePlaceholder,
//! Background color for the currently active placeholder in text
//! templates.
TemplateFocusedPlaceholder,
//! Background color for read-only placeholders in text templates.
TemplateReadOnlyPlaceholder
};
Q_ENUM(EditorColorRole)
/**
* Default constructor, creating an invalid Theme, see isValid().
*/
Theme();
/**
* Copy constructor, sharing the Theme data with @p copy.
*/
Theme(const Theme &copy);
/**
* Destructor.
*/
~Theme();
/**
* Assignment operator, sharing the Theme data with @p other.
*/
Theme &operator=(const Theme &other);
/**
* Returns @c true if this is a valid Theme.
* If the theme is invalid, none of the returned colors are well-defined.
*/
bool isValid() const;
/**
* Returns the unique name of this Theme.
* @see translatedName()
*/
QString name() const;
/**
* Returns the translated name of this Theme. The translated name can be
* used in the user interface.
*/
QString translatedName() const;
/**
* Returns @c true if this Theme is read-only.
*
* A Theme is read-only, if the filePath() points to a non-writable file.
* This is typically the case for Themes that are compiled into the executable
* as resource file, as well as for theme files that are installed in read-only
* system locations (e.g. /usr/share/).
*/
bool isReadOnly() const;
/**
* Returns the full path and file name to this Theme.
* Themes from the Qt resource return the Qt resource path.
* Themes from disk return the local path.
*
* If the theme is invalid (isValid()), an empty string is returned.
*/
QString filePath() const;
/**
* Returns the text color to be used for @p style.
* @c 0 is returned for styles that do not specify a text color,
* use the default text color in that case.
*/
QRgb textColor(TextStyle style) const;
/**
* Returns the selected text color to be used for @p style.
* @c 0 is returned for styles that do not specify a selected text color,
* use the default textColor() in that case.
*/
QRgb selectedTextColor(TextStyle style) const;
/**
* Returns the background color to be used for @p style.
* @c 0 is returned for styles that do not specify a background color,
* use the default background color in that case.
*/
QRgb backgroundColor(TextStyle style) const;
/**
* Returns the background color to be used for selected text for @p style.
* @c 0 is returned for styles that do not specify a background color,
* use the default backgroundColor() in that case.
*/
QRgb selectedBackgroundColor(TextStyle style) const;
/**
* Returns whether the given style should be shown in bold.
*/
bool isBold(TextStyle style) const;
/**
* Returns whether the given style should be shown in italic.
*/
bool isItalic(TextStyle style) const;
/**
* Returns whether the given style should be shown underlined.
*/
bool isUnderline(TextStyle style) const;
/**
* Returns whether the given style should be shown struck through.
*/
bool isStrikeThrough(TextStyle style) const;
public:
/**
* Returns the editor color for the requested @p role.
*/
QRgb editorColor(EditorColorRole role) const;
private:
/**
* Constructor taking a shared ThemeData instance.
*/
KSYNTAXHIGHLIGHTING_NO_EXPORT explicit Theme(ThemeData *data);
friend class RepositoryPrivate;
friend class ThemeData;
private:
/**
* Shared data holder.
*/
QExplicitlySharedDataPointer<ThemeData> m_data;
};
}
QT_BEGIN_NAMESPACE
Q_DECLARE_TYPEINFO(KSyntaxHighlighting::Theme, Q_RELOCATABLE_TYPE);
QT_END_NAMESPACE
#endif // KSYNTAXHIGHLIGHTING_THEME_H
@@ -0,0 +1,286 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2016 Dominik Haumann <dhaumann@kde.org>
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#include "ksyntaxhighlighting_logging.h"
#include "themedata_p.h"
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QMetaEnum>
using namespace KSyntaxHighlighting;
ThemeData::ThemeData()
{
memset(m_editorColors, 0, sizeof(m_editorColors));
m_textStyles.resize(QMetaEnum::fromType<Theme::TextStyle>().keyCount());
}
/**
* Convert QJsonValue @p val into a color, if possible. Valid colors are only
* in hex format: #aarrggbb. On error, returns 0x00000000.
*/
static inline QRgb readColor(const QJsonValue &val)
{
const QRgb unsetColor = 0;
if (!val.isString()) {
return unsetColor;
}
const QString str = val.toString();
if (str.isEmpty() || str[0] != QLatin1Char('#')) {
return unsetColor;
}
const QColor color(str);
return color.isValid() ? color.rgba() : unsetColor;
}
static inline TextStyleData readThemeData(const QJsonObject &obj)
{
TextStyleData td;
td.textColor = readColor(obj.value(QLatin1String("text-color")));
td.backgroundColor = readColor(obj.value(QLatin1String("background-color")));
td.selectedTextColor = readColor(obj.value(QLatin1String("selected-text-color")));
td.selectedBackgroundColor = readColor(obj.value(QLatin1String("selected-background-color")));
auto val = obj.value(QLatin1String("bold"));
if (val.isBool()) {
td.bold = val.toBool();
td.hasBold = true;
}
val = obj.value(QLatin1String("italic"));
if (val.isBool()) {
td.italic = val.toBool();
td.hasItalic = true;
}
val = obj.value(QLatin1String("underline"));
if (val.isBool()) {
td.underline = val.toBool();
td.hasUnderline = true;
}
val = obj.value(QLatin1String("strike-through"));
if (val.isBool()) {
td.strikeThrough = val.toBool();
td.hasStrikeThrough = true;
}
return td;
}
bool ThemeData::load(const QString &filePath)
{
// flag first as done for the error cases
m_completelyLoaded = true;
QFile loadFile(filePath);
if (!loadFile.open(QIODevice::ReadOnly)) {
return false;
}
const QByteArray jsonData = loadFile.readAll();
// look for metadata object
int metaDataStart = jsonData.indexOf("\"metadata\"");
int start = jsonData.indexOf('{', metaDataStart);
int end = jsonData.indexOf("}", metaDataStart);
if (start < 0 || end < 0) {
qCWarning(Log) << "Failed to parse theme file" << filePath << ":"
<< "no metadata object found";
return false;
}
QJsonParseError parseError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData.sliced(start, (end + 1) - start), &parseError);
if (parseError.error != QJsonParseError::NoError) {
qCWarning(Log) << "Failed to parse theme file" << filePath << ":" << parseError.errorString();
return false;
}
m_filePath = filePath;
// we need more data later
m_completelyLoaded = false;
// read metadata
QJsonObject metadata = jsonDoc.object();
m_name = metadata.value(QLatin1String("name")).toString();
m_revision = metadata.value(QLatin1String("revision")).toInt();
return true;
}
void ThemeData::loadComplete()
{
if (m_completelyLoaded) {
return;
}
m_completelyLoaded = true;
QFile loadFile(m_filePath);
if (!loadFile.open(QIODevice::ReadOnly)) {
return;
}
const QByteArray jsonData = loadFile.readAll();
QJsonParseError parseError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qCWarning(Log) << "Failed to parse theme file" << m_filePath << ":" << parseError.errorString();
return;
}
QJsonObject obj = jsonDoc.object();
// read text styles
const auto metaEnumStyle = QMetaEnum::fromType<Theme::TextStyle>();
const QJsonObject textStyles = obj.value(QLatin1String("text-styles")).toObject();
for (int i = 0; i < metaEnumStyle.keyCount(); ++i) {
Q_ASSERT(i == metaEnumStyle.value(i));
m_textStyles[i] = readThemeData(textStyles.value(QLatin1String(metaEnumStyle.key(i))).toObject());
}
// read editor colors
const auto metaEnumColor = QMetaEnum::fromType<Theme::EditorColorRole>();
const QJsonObject editorColors = obj.value(QLatin1String("editor-colors")).toObject();
for (int i = 0; i < metaEnumColor.keyCount(); ++i) {
Q_ASSERT(i == metaEnumColor.value(i));
m_editorColors[i] = readColor(editorColors.value(QLatin1String(metaEnumColor.key(i))));
}
// if we have no new key around for Theme::BackgroundColor => use old variants to be compatible
if (!editorColors.contains(QLatin1String(metaEnumColor.key(Theme::BackgroundColor)))) {
m_editorColors[Theme::BackgroundColor] = readColor(editorColors.value(QLatin1String("background-color")));
m_editorColors[Theme::TextSelection] = readColor(editorColors.value(QLatin1String("selection")));
m_editorColors[Theme::CurrentLine] = readColor(editorColors.value(QLatin1String("current-line")));
m_editorColors[Theme::SearchHighlight] = readColor(editorColors.value(QLatin1String("search-highlight")));
m_editorColors[Theme::ReplaceHighlight] = readColor(editorColors.value(QLatin1String("replace-highlight")));
m_editorColors[Theme::BracketMatching] = readColor(editorColors.value(QLatin1String("bracket-matching")));
m_editorColors[Theme::TabMarker] = readColor(editorColors.value(QLatin1String("tab-marker")));
m_editorColors[Theme::SpellChecking] = readColor(editorColors.value(QLatin1String("spell-checking")));
m_editorColors[Theme::IndentationLine] = readColor(editorColors.value(QLatin1String("indentation-line")));
m_editorColors[Theme::IconBorder] = readColor(editorColors.value(QLatin1String("icon-border")));
m_editorColors[Theme::CodeFolding] = readColor(editorColors.value(QLatin1String("code-folding")));
m_editorColors[Theme::LineNumbers] = readColor(editorColors.value(QLatin1String("line-numbers")));
m_editorColors[Theme::CurrentLineNumber] = readColor(editorColors.value(QLatin1String("current-line-number")));
m_editorColors[Theme::WordWrapMarker] = readColor(editorColors.value(QLatin1String("word-wrap-marker")));
m_editorColors[Theme::ModifiedLines] = readColor(editorColors.value(QLatin1String("modified-lines")));
m_editorColors[Theme::SavedLines] = readColor(editorColors.value(QLatin1String("saved-lines")));
m_editorColors[Theme::Separator] = readColor(editorColors.value(QLatin1String("separator")));
m_editorColors[Theme::MarkBookmark] = readColor(editorColors.value(QLatin1String("mark-bookmark")));
m_editorColors[Theme::MarkBreakpointActive] = readColor(editorColors.value(QLatin1String("mark-breakpoint-active")));
m_editorColors[Theme::MarkBreakpointReached] = readColor(editorColors.value(QLatin1String("mark-breakpoint-reached")));
m_editorColors[Theme::MarkBreakpointDisabled] = readColor(editorColors.value(QLatin1String("mark-breakpoint-disabled")));
m_editorColors[Theme::MarkExecution] = readColor(editorColors.value(QLatin1String("mark-execution")));
m_editorColors[Theme::MarkWarning] = readColor(editorColors.value(QLatin1String("mark-warning")));
m_editorColors[Theme::MarkError] = readColor(editorColors.value(QLatin1String("mark-error")));
m_editorColors[Theme::TemplateBackground] = readColor(editorColors.value(QLatin1String("template-background")));
m_editorColors[Theme::TemplatePlaceholder] = readColor(editorColors.value(QLatin1String("template-placeholder")));
m_editorColors[Theme::TemplateFocusedPlaceholder] = readColor(editorColors.value(QLatin1String("template-focused-placeholder")));
m_editorColors[Theme::TemplateReadOnlyPlaceholder] = readColor(editorColors.value(QLatin1String("template-read-only-placeholder")));
}
// read per-definition style overrides
const auto customStyles = obj.value(QLatin1String("custom-styles")).toObject();
for (auto it = customStyles.begin(); it != customStyles.end(); ++it) {
const auto obj = it.value().toObject();
auto &overrideStyle = m_textStyleOverrides[it.key()];
for (auto it2 = obj.begin(); it2 != obj.end(); ++it2) {
overrideStyle.insert(it2.key(), readThemeData(it2.value().toObject()));
}
}
return;
}
QString ThemeData::name() const
{
return m_name;
}
int ThemeData::revision() const
{
return m_revision;
}
bool ThemeData::isReadOnly() const
{
return !QFileInfo(m_filePath).isWritable();
}
QString ThemeData::filePath() const
{
return m_filePath;
}
TextStyleData ThemeData::textStyle(Theme::TextStyle style) const
{
if (!m_completelyLoaded) {
const_cast<ThemeData *>(this)->loadComplete();
}
return m_textStyles[style];
}
QRgb ThemeData::textColor(Theme::TextStyle style) const
{
return textStyle(style).textColor;
}
QRgb ThemeData::selectedTextColor(Theme::TextStyle style) const
{
return textStyle(style).selectedTextColor;
}
QRgb ThemeData::backgroundColor(Theme::TextStyle style) const
{
return textStyle(style).backgroundColor;
}
QRgb ThemeData::selectedBackgroundColor(Theme::TextStyle style) const
{
return textStyle(style).selectedBackgroundColor;
}
bool ThemeData::isBold(Theme::TextStyle style) const
{
return textStyle(style).bold;
}
bool ThemeData::isItalic(Theme::TextStyle style) const
{
return textStyle(style).italic;
}
bool ThemeData::isUnderline(Theme::TextStyle style) const
{
return textStyle(style).underline;
}
bool ThemeData::isStrikeThrough(Theme::TextStyle style) const
{
return textStyle(style).strikeThrough;
}
QRgb ThemeData::editorColor(Theme::EditorColorRole role) const
{
if (!m_completelyLoaded) {
const_cast<ThemeData *>(this)->loadComplete();
}
Q_ASSERT(static_cast<int>(role) >= 0 && static_cast<int>(role) <= static_cast<int>(Theme::TemplateReadOnlyPlaceholder));
return m_editorColors[role];
}
TextStyleData ThemeData::textStyleOverride(const QString &definitionName, const QString &attributeName) const
{
if (!m_completelyLoaded) {
const_cast<ThemeData *>(this)->loadComplete();
}
auto it = m_textStyleOverrides.find(definitionName);
if (it != m_textStyleOverrides.end()) {
return it->value(attributeName);
}
return TextStyleData();
}
@@ -0,0 +1,168 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-FileCopyrightText: 2016 Dominik Haumann <dhaumann@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_THEMEDATA_P_H
#define KSYNTAXHIGHLIGHTING_THEMEDATA_P_H
#include "textstyledata_p.h"
#include "theme.h"
#include <QHash>
#include <QSharedData>
#include <vector>
namespace KSyntaxHighlighting
{
/**
* Data container for a Theme.
*/
class ThemeData : public QSharedData
{
public:
static ThemeData *get(const Theme &theme)
{
return theme.m_data.data();
}
/**
* Default constructor, creating an uninitialized ThemeData instance.
*/
ThemeData();
/**
* Load the Theme data from the file @p filePath.
* Note, that @p filePath either is a local file, or a qt resource location.
*/
bool load(const QString &filePath);
void loadComplete();
/**
* Returns the unique name of this Theme.
*/
QString name() const;
/**
* Returns the revision of this Theme.
* The revision in a .theme file should be increased with every change.
*/
int revision() const;
/**
* Returns @c true if this Theme is read-only.
* Typically, themes that are shipped by default are read-only.
*/
bool isReadOnly() const;
/**
* Returns the full path and filename to this Theme.
* Themes from the Qt resource return the Qt resource path.
* Themes from disk return the local path.
*
* If the theme is invalid (isValid()), an empty string is returned.
*/
QString filePath() const;
/**
* Returns the text color to be used for @p style.
* @c 0 is returned for styles that do not specify a text color,
* use the default text color in that case.
*/
QRgb textColor(Theme::TextStyle style) const;
/**
* Returns the text color for selected to be used for @p style.
* @c 0 is returned for styles that do not specify a selected text color,
* use the textColor() in that case.
*/
QRgb selectedTextColor(Theme::TextStyle style) const;
/**
* Returns the background color to be used for @p style.
* @c 0 is returned for styles that do not specify a background color,
* use the default background color in that case.
*/
QRgb backgroundColor(Theme::TextStyle style) const;
/**
* Returns the background color for selected text to be used for @p style.
* @c 0 is returned for styles that do not specify a selected background
* color, use the default backgroundColor() in that case.
*/
QRgb selectedBackgroundColor(Theme::TextStyle style) const;
/**
* Returns whether the given style should be shown in bold.
*/
bool isBold(Theme::TextStyle style) const;
/**
* Returns whether the given style should be shown in italic.
*/
bool isItalic(Theme::TextStyle style) const;
/**
* Returns whether the given style should be shown underlined.
*/
bool isUnderline(Theme::TextStyle style) const;
/**
* Returns whether the given style should be shown struck through.
*/
bool isStrikeThrough(Theme::TextStyle style) const;
public:
/**
* Returns the editor color for the requested @p role.
*/
QRgb editorColor(Theme::EditorColorRole role) const;
/**
* Returns the TextStyle override of a specific "itemData" with attributeName
* in the syntax definition called definitionName.
*
* If no override exists, a valid TextStyleData with the respective default
* TextStyle will be used, so the returned value is always valid.
*/
TextStyleData textStyleOverride(const QString &definitionName, const QString &attributeName) const;
/**
* Returns the TextStyle data for the given @p style.
*/
TextStyleData textStyle(Theme::TextStyle style) const;
private:
int m_revision = 0;
QString m_name;
//! Path to the file where the theme came from.
//! This is either a resource location (":/themes/Default.theme"), or a file
//! on disk (in a read-only or a writeable location).
QString m_filePath;
// default is loaded, we have no file per default, set to false on start of load
bool m_completelyLoaded = true;
//! TextStyles
std::vector<TextStyleData> m_textStyles;
//! style overrides for individual itemData entries
//! definition name -> attribute name -> style
QHash<QString, QHash<QString, TextStyleData>> m_textStyleOverrides;
//! Editor area colors
QRgb m_editorColors[Theme::TemplateReadOnlyPlaceholder + 1];
};
}
QT_BEGIN_NAMESPACE
Q_DECLARE_TYPEINFO(KSyntaxHighlighting::TextStyleData, Q_RELOCATABLE_TYPE);
QT_END_NAMESPACE
#endif // KSYNTAXHIGHLIGHTING_THEMEDATA_P_H
@@ -0,0 +1,66 @@
/*
SPDX-FileCopyrightText: 2007 Sebastian Pipping <webmaster@hartwork.org>
SPDX-License-Identifier: MIT
*/
#include "wildcardmatcher.h"
using namespace KSyntaxHighlighting;
#include <QChar>
namespace
{
bool wildcardMatch(QStringView candidate, QStringView wildcard, int candidatePosFromRight, int wildcardPosFromRight)
{
for (; wildcardPosFromRight >= 0; wildcardPosFromRight--) {
const auto ch = wildcard.at(wildcardPosFromRight).unicode();
switch (ch) {
case L'*':
if (candidatePosFromRight == -1) {
break;
}
if (wildcardPosFromRight == 0) {
return true;
}
// Eat all we can and go back as far as we have to
for (int j = -1; j <= candidatePosFromRight; j++) {
if (wildcardMatch(candidate, wildcard, j, wildcardPosFromRight - 1)) {
return true;
}
}
return false;
case L'?':
if (candidatePosFromRight == -1) {
return false;
}
candidatePosFromRight--;
break;
default:
if (candidatePosFromRight == -1) {
return false;
}
const auto candidateCh = candidate.at(candidatePosFromRight).unicode();
if (candidateCh == ch) {
candidatePosFromRight--;
} else {
return false;
}
}
}
return candidatePosFromRight == -1;
}
} // unnamed namespace
bool WildcardMatcher::exactMatch(QStringView candidate, QStringView wildcard)
{
return ::wildcardMatch(candidate, wildcard, candidate.length() - 1, wildcard.length() - 1);
}
@@ -0,0 +1,33 @@
/*
SPDX-FileCopyrightText: 2007 Sebastian Pipping <webmaster@hartwork.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_WILDCARDMATCHER_H
#define KSYNTAXHIGHLIGHTING_WILDCARDMATCHER_H
#include "ksyntaxhighlighting_export.h"
#include <QStringView>
namespace KSyntaxHighlighting
{
namespace WildcardMatcher
{
/**
* Matches a string against a given wildcard case-sensitively.
* The wildcard supports '*' (".*" in regex) and '?' ("." in regex), not more.
*
* @param candidate Text to match
* @param wildcard Wildcard to use
* @return True for an exact match, false otherwise
*
* @since 5.86
*/
KSYNTAXHIGHLIGHTING_EXPORT bool exactMatch(QStringView candidate, QStringView wildcard);
}
}
#endif // KSYNTAXHIGHLIGHTING_WILDCARDMATCHER_H
@@ -0,0 +1,54 @@
/*
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#include "worddelimiters_p.h"
using namespace KSyntaxHighlighting;
WordDelimiters::WordDelimiters()
: asciiDelimiters{}
{
for (const char *p = "\t !%&()*+,-./:;<=>?[\\]^{|}~"; *p; ++p) {
asciiDelimiters.set(*p);
}
}
WordDelimiters::WordDelimiters(QStringView str)
: asciiDelimiters{}
{
append(str);
}
bool WordDelimiters::contains(QChar c) const
{
if (c.unicode() < 128) {
return asciiDelimiters.test(c.unicode());
}
// perf tells contains is MUCH faster than binary search here, very short array
return notAsciiDelimiters.contains(c);
}
void WordDelimiters::append(QStringView s)
{
for (QChar c : s) {
if (c.unicode() < 128) {
asciiDelimiters.set(c.unicode());
} else {
notAsciiDelimiters.append(c);
}
}
}
void WordDelimiters::remove(QStringView s)
{
for (QChar c : s) {
if (c.unicode() < 128) {
asciiDelimiters.set(c.unicode(), false);
} else {
notAsciiDelimiters.remove(c);
}
}
}
@@ -0,0 +1,63 @@
/*
SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_WORDDELIMITERS_P_H
#define KSYNTAXHIGHLIGHTING_WORDDELIMITERS_P_H
#include <QString>
#include <bitset>
namespace KSyntaxHighlighting
{
/**
* Represents a list of character that separates 2 words.
*
* Default delimiters are .():!+*,-<=>%&/;?[]^{|}~\, space (' ') and tabulator ('\t').
*
* @see Rule
* @since 5.74
*/
class WordDelimiters
{
public:
WordDelimiters();
/**
* Initialize with a default delimiters.
*/
explicit WordDelimiters(QStringView str);
/**
* Returns @c true if @p c is a word delimiter; otherwise returns @c false.
*/
bool contains(QChar c) const;
/**
* Appends each character of @p s to word delimiters.
*/
void append(QStringView s);
/**
* Removes each character of @p s from word delimiters.
*/
void remove(QStringView c);
private:
/**
* An array which represents ascii characters for very fast lookup.
* The character is used as an index and the value @c true indicates a word delimiter.
*/
std::bitset<128> asciiDelimiters;
/**
* Contains characters that are not ascii and is empty for most syntax definition.
*/
QString notAsciiDelimiters;
};
}
#endif
@@ -0,0 +1,26 @@
/*
SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KSYNTAXHIGHLIGHTING_XML_P_H
#define KSYNTAXHIGHLIGHTING_XML_P_H
#include <QString>
namespace KSyntaxHighlighting
{
/** Utilities for XML parsing. */
namespace Xml
{
/** Parse a xs:boolean attribute. */
inline bool attrToBool(QStringView str)
{
return str == QStringLiteral("1") || str.compare(QStringLiteral("true"), Qt::CaseInsensitive) == 0;
}
}
}
#endif
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2018 Eike Hein <hein@kde.org>
# SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
# SPDX-License-Identifier: BSD-3-Clause
ecm_add_qml_module(kquicksyntaxhighlightingplugin URI "org.kde.syntaxhighlighting")
target_sources(kquicksyntaxhighlightingplugin PRIVATE
kquicksyntaxhighlightingplugin.cpp
kquicksyntaxhighlighter.cpp
)
target_link_libraries(kquicksyntaxhighlightingplugin PRIVATE
KF6SyntaxHighlighting
Qt6::Quick
)
ecm_finalize_qml_module(kquicksyntaxhighlightingplugin DESTINATION ${KDE_INSTALL_QMLDIR})
@@ -0,0 +1,114 @@
/*
SPDX-FileCopyrightText: 2018 Eike Hein <hein@kde.org>
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#include "kquicksyntaxhighlighter.h"
#include <KSyntaxHighlighting/Repository>
#include <KSyntaxHighlighting/SyntaxHighlighter>
#include <QGuiApplication>
#include <QPalette>
#include <QQuickTextDocument>
#include <QTextDocument>
using namespace KSyntaxHighlighting;
extern Repository *defaultRepository();
KQuickSyntaxHighlighter::KQuickSyntaxHighlighter(QObject *parent)
: QObject(parent)
, m_textEdit(nullptr)
, m_highlighter(new KSyntaxHighlighting::SyntaxHighlighter(this))
{
}
KQuickSyntaxHighlighter::~KQuickSyntaxHighlighter() = default;
QObject *KQuickSyntaxHighlighter::textEdit() const
{
return m_textEdit;
}
void KQuickSyntaxHighlighter::setTextEdit(QObject *textEdit)
{
if (m_textEdit != textEdit) {
m_textEdit = textEdit;
m_highlighter->setDocument(m_textEdit->property("textDocument").value<QQuickTextDocument *>()->textDocument());
}
}
QVariant KQuickSyntaxHighlighter::definition() const
{
return QVariant::fromValue(m_definition);
}
void KQuickSyntaxHighlighter::setDefinition(const QVariant &definition)
{
Definition def;
if (definition.userType() == QMetaType::QString) {
def = unwrappedRepository()->definitionForName(definition.toString());
} else {
def = definition.value<Definition>();
}
if (m_definition != def) {
m_definition = def;
m_highlighter->setTheme(m_theme.isValid() ? m_theme : unwrappedRepository()->themeForPalette(QGuiApplication::palette()));
m_highlighter->setDefinition(def);
Q_EMIT definitionChanged();
}
}
QVariant KQuickSyntaxHighlighter::theme() const
{
return QVariant::fromValue(m_theme);
}
void KQuickSyntaxHighlighter::setTheme(const QVariant &theme)
{
Theme t;
if (theme.userType() == QMetaType::QString) {
t = unwrappedRepository()->theme(theme.toString());
} else if (theme.userType() == QMetaType::Int) {
t = unwrappedRepository()->defaultTheme(static_cast<Repository::DefaultTheme>(theme.toInt()));
} else {
t = theme.value<Theme>();
}
if (m_theme.name() != t.name()) {
m_theme = t;
m_highlighter->setTheme(m_theme);
m_highlighter->rehighlight();
Q_EMIT themeChanged();
}
}
Repository *KQuickSyntaxHighlighter::repository() const
{
return m_repository;
}
void KQuickSyntaxHighlighter::setRepository(Repository *repository)
{
if (m_repository == repository) {
return;
}
m_repository = repository;
Q_EMIT repositoryChanged();
}
Repository *KQuickSyntaxHighlighter::unwrappedRepository() const
{
if (m_repository) {
return m_repository;
}
return defaultRepository();
}
#include "moc_kquicksyntaxhighlighter.cpp"
@@ -0,0 +1,64 @@
/*
SPDX-FileCopyrightText: 2018 Eike Hein <hein@kde.org>
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KQUICKSYNTAXHIGHLIGHTER_H
#define KQUICKSYNTAXHIGHLIGHTER_H
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Theme>
#include <QObject>
#include <QVariant>
namespace KSyntaxHighlighting
{
class Repository;
class SyntaxHighlighter;
}
class KQuickSyntaxHighlighter : public QObject
{
Q_OBJECT
Q_PROPERTY(QObject *textEdit READ textEdit WRITE setTextEdit NOTIFY textEditChanged)
Q_PROPERTY(QVariant definition READ definition WRITE setDefinition NOTIFY definitionChanged)
Q_PROPERTY(QVariant theme READ theme WRITE setTheme NOTIFY themeChanged)
Q_PROPERTY(KSyntaxHighlighting::Repository *repository READ repository WRITE setRepository NOTIFY repositoryChanged)
public:
explicit KQuickSyntaxHighlighter(QObject *parent = nullptr);
~KQuickSyntaxHighlighter() override;
QObject *textEdit() const;
void setTextEdit(QObject *textEdit);
QVariant definition() const;
void setDefinition(const QVariant &definition);
QVariant theme() const;
void setTheme(const QVariant &theme);
KSyntaxHighlighting::Repository *repository() const;
void setRepository(KSyntaxHighlighting::Repository *repository);
Q_SIGNALS:
void textEditChanged() const;
void definitionChanged() const;
void themeChanged();
void repositoryChanged();
private:
KSyntaxHighlighting::Repository *unwrappedRepository() const;
QObject *m_textEdit;
KSyntaxHighlighting::Definition m_definition;
KSyntaxHighlighting::Theme m_theme;
KSyntaxHighlighting::Repository *m_repository = nullptr;
KSyntaxHighlighting::SyntaxHighlighter *m_highlighter = nullptr;
};
#endif // KQUICKSYNTAXHIGHLIGHTER_H
@@ -0,0 +1,46 @@
/*
SPDX-FileCopyrightText: 2018 Eike Hein <hein@kde.org>
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: MIT
*/
#include "kquicksyntaxhighlightingplugin.h"
#include "kquicksyntaxhighlighter.h"
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Repository>
#include <KSyntaxHighlighting/Theme>
#include <memory>
using namespace KSyntaxHighlighting;
Repository *defaultRepository()
{
static std::unique_ptr<Repository> s_instance;
if (!s_instance) {
s_instance = std::make_unique<Repository>();
}
return s_instance.get();
}
void KQuickSyntaxHighlightingPlugin::registerTypes(const char *uri)
{
Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.syntaxhighlighting"));
qRegisterMetaType<Definition>();
qRegisterMetaType<QList<Definition>>();
qRegisterMetaType<Theme>();
qRegisterMetaType<QList<Theme>>();
qmlRegisterType<KQuickSyntaxHighlighter>(uri, 1, 0, "SyntaxHighlighter");
qmlRegisterUncreatableMetaObject(Definition::staticMetaObject, uri, 1, 0, "Definition", {});
qmlRegisterUncreatableMetaObject(Theme::staticMetaObject, uri, 1, 0, "Theme", {});
qmlRegisterSingletonType<Repository>(uri, 1, 0, "Repository", [](auto engine, auto scriptEngine) {
(void)engine;
auto repo = defaultRepository();
scriptEngine->setObjectOwnership(repo, QJSEngine::CppOwnership);
return defaultRepository();
});
}
#include "moc_kquicksyntaxhighlightingplugin.cpp"
@@ -0,0 +1,22 @@
/*
SPDX-FileCopyrightText: 2018 Eike Hein <hein@kde.org>
SPDX-License-Identifier: MIT
*/
#ifndef KQUICKSYNTAXHIGHLIGHTINGPLUGIN_H
#define KQUICKSYNTAXHIGHLIGHTINGPLUGIN_H
#include <QQmlEngine>
#include <QQmlExtensionPlugin>
class KQuickSyntaxHighlightingPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
public:
void registerTypes(const char *uri) override;
};
#endif // KQUICKSYNTAXHIGHLIGHTINGPLUGIN_H