1/*
2 SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: MIT
5*/
6
7#include "repository.h"
8#include "definition.h"
9#include "definition_p.h"
10#include "ksyntaxhighlighting_logging.h"
11#include "repository_p.h"
12#include "theme.h"
13#include "themedata_p.h"
14#include "wildcardmatcher.h"
15
16#include <QCborMap>
17#include <QCborValue>
18#include <QDirIterator>
19#include <QFile>
20#include <QFileInfo>
21#include <QPalette>
22#include <QString>
23#include <QStringView>
24
25#ifndef NO_STANDARD_PATHS
26#include <QStandardPaths>
27#endif
28
29#include <algorithm>
30#include <iterator>
31#include <limits>
32
33using namespace KSyntaxHighlighting;
34
35namespace
36{
37QString fileNameFromFilePath(const QString &filePath)
38{
39 return QFileInfo{filePath}.fileName();
40}
41
42auto anyWildcardMatches(QStringView str)
43{
44 return [str](const Definition &def) {
45 const auto strings = def.extensions();
46 return std::any_of(first: strings.cbegin(), last: strings.cend(), pred: [str](QStringView wildcard) {
47 return WildcardMatcher::exactMatch(candidate: str, wildcard);
48 });
49 };
50}
51
52auto anyMimeTypeEquals(QStringView mimeTypeName)
53{
54 return [mimeTypeName](const Definition &def) {
55 const auto strings = def.mimeTypes();
56 return std::any_of(first: strings.cbegin(), last: strings.cend(), pred: [mimeTypeName](QStringView name) {
57 return mimeTypeName == name;
58 });
59 };
60}
61
62// The two function templates below take defs - a map sorted by highlighting name - to be deterministic and independent of translations.
63
64template<typename UnaryPredicate>
65Definition findHighestPriorityDefinitionIf(const QMap<QString, Definition> &defs, UnaryPredicate predicate)
66{
67 const Definition *match = nullptr;
68 auto matchPriority = std::numeric_limits<int>::lowest();
69 for (const Definition &def : defs) {
70 const auto defPriority = def.priority();
71 if (defPriority > matchPriority && predicate(def)) {
72 match = &def;
73 matchPriority = defPriority;
74 }
75 }
76 return match == nullptr ? Definition{} : *match;
77}
78
79template<typename UnaryPredicate>
80QList<Definition> findDefinitionsIf(const QMap<QString, Definition> &defs, UnaryPredicate predicate)
81{
82 QList<Definition> matches;
83 std::copy_if(defs.cbegin(), defs.cend(), std::back_inserter(x&: matches), predicate);
84 std::stable_sort(matches.begin(), matches.end(), [](const Definition &lhs, const Definition &rhs) {
85 return lhs.priority() > rhs.priority();
86 });
87 return matches;
88}
89} // unnamed namespace
90
91static void initResource()
92{
93#ifdef HAS_SYNTAX_RESOURCE
94 Q_INIT_RESOURCE(syntax_data);
95#endif
96 Q_INIT_RESOURCE(theme_data);
97}
98
99RepositoryPrivate *RepositoryPrivate::get(Repository *repo)
100{
101 return repo->d.get();
102}
103
104Repository::Repository()
105 : d(new RepositoryPrivate)
106{
107 initResource();
108 d->load(repo: this);
109}
110
111Repository::~Repository()
112{
113 // reset repo so we can detect in still alive definition instances
114 // that the repo was deleted
115 for (const auto &def : std::as_const(t&: d->m_sortedDefs)) {
116 DefinitionData::get(def)->repo = nullptr;
117 }
118}
119
120Definition Repository::definitionForName(const QString &defName) const
121{
122 return d->m_fullDefs.value(key: defName.toLower());
123}
124
125Definition Repository::definitionForFileName(const QString &fileName) const
126{
127 return findHighestPriorityDefinitionIf(defs: d->m_defs, predicate: anyWildcardMatches(str: fileNameFromFilePath(filePath: fileName)));
128}
129
130QList<Definition> Repository::definitionsForFileName(const QString &fileName) const
131{
132 return findDefinitionsIf(defs: d->m_defs, predicate: anyWildcardMatches(str: fileNameFromFilePath(filePath: fileName)));
133}
134
135Definition Repository::definitionForMimeType(const QString &mimeType) const
136{
137 return findHighestPriorityDefinitionIf(defs: d->m_defs, predicate: anyMimeTypeEquals(mimeTypeName: mimeType));
138}
139
140QList<Definition> Repository::definitionsForMimeType(const QString &mimeType) const
141{
142 return findDefinitionsIf(defs: d->m_defs, predicate: anyMimeTypeEquals(mimeTypeName: mimeType));
143}
144
145QList<Definition> Repository::definitions() const
146{
147 return d->m_sortedDefs;
148}
149
150QList<Theme> Repository::themes() const
151{
152 return d->m_themes;
153}
154
155static auto lowerBoundTheme(const QList<KSyntaxHighlighting::Theme> &themes, QStringView themeName)
156{
157 return std::lower_bound(first: themes.begin(), last: themes.end(), val: themeName, comp: [](const Theme &lhs, QStringView rhs) {
158 return lhs.name() < rhs;
159 });
160}
161
162Theme Repository::theme(const QString &themeName) const
163{
164 const auto &themes = d->m_themes;
165 const auto it = lowerBoundTheme(themes, themeName);
166 if (it != themes.end() && (*it).name() == themeName) {
167 return *it;
168 }
169 return Theme();
170}
171
172Theme Repository::defaultTheme(Repository::DefaultTheme t) const
173{
174 if (t == DarkTheme) {
175 return theme(QStringLiteral("Breeze Dark"));
176 }
177 return theme(QStringLiteral("Breeze Light"));
178}
179
180Theme Repository::themeForPalette(const QPalette &palette) const
181{
182 const auto base = palette.color(cr: QPalette::Base);
183 const auto highlight = palette.color(cr: QPalette::Highlight).rgb();
184
185 // find themes with matching background and highlight colors
186 const Theme *firstMatchingTheme = nullptr;
187 for (const auto &theme : std::as_const(t&: d->m_themes)) {
188 const auto background = theme.editorColor(role: Theme::EditorColorRole::BackgroundColor);
189 if (background == base.rgb()) {
190 // find theme with a matching highlight color
191 auto selection = theme.editorColor(role: Theme::EditorColorRole::TextSelection);
192 if (selection == highlight) {
193 return theme;
194 }
195 if (!firstMatchingTheme) {
196 firstMatchingTheme = &theme;
197 }
198 }
199 }
200 if (firstMatchingTheme) {
201 return *firstMatchingTheme;
202 }
203
204 // fallback to just use the default light or dark theme
205 return defaultTheme(t: (base.lightness() < 128) ? Repository::DarkTheme : Repository::LightTheme);
206}
207
208void RepositoryPrivate::load(Repository *repo)
209{
210 // always add invalid default "None" highlighting
211 addDefinition(def: Definition());
212
213 // do lookup in standard paths, if not disabled
214#ifndef NO_STANDARD_PATHS
215 // do lookup in installed path when has no syntax resource
216#ifndef HAS_SYNTAX_RESOURCE
217 for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
218 QStringLiteral("org.kde.syntax-highlighting/syntax-bundled"),
219 QStandardPaths::LocateDirectory)) {
220 if (!loadSyntaxFolderFromIndex(repo, dir)) {
221 loadSyntaxFolder(repo, dir);
222 }
223 }
224#endif
225
226 for (const auto &dir : QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation,
227 QStringLiteral("org.kde.syntax-highlighting/syntax"),
228 options: QStandardPaths::LocateDirectory)) {
229 loadSyntaxFolder(repo, path: dir);
230 }
231#endif
232
233 // default resources are always used, this is the one location that has a index cbor file
234 loadSyntaxFolderFromIndex(repo, QStringLiteral(":/org.kde.syntax-highlighting/syntax"));
235
236 // extra resources provided by 3rdparty libraries/applications
237 loadSyntaxFolder(repo, QStringLiteral(":/org.kde.syntax-highlighting/syntax-addons"));
238
239 // user given extra paths
240 for (const auto &path : std::as_const(t&: m_customSearchPaths)) {
241 loadSyntaxFolder(repo, path: path + QStringLiteral("/syntax"));
242 }
243
244 m_sortedDefs.reserve(asize: m_defs.size());
245 for (auto it = m_defs.constBegin(); it != m_defs.constEnd(); ++it) {
246 m_sortedDefs.push_back(t: it.value());
247 }
248 std::sort(first: m_sortedDefs.begin(), last: m_sortedDefs.end(), comp: [](const Definition &left, const Definition &right) {
249 auto comparison = left.translatedSection().compare(s: right.translatedSection(), cs: Qt::CaseInsensitive);
250 if (comparison == 0) {
251 comparison = left.translatedName().compare(s: right.translatedName(), cs: Qt::CaseInsensitive);
252 }
253 return comparison < 0;
254 });
255
256 for (auto it = m_sortedDefs.constBegin(); it != m_sortedDefs.constEnd(); ++it) {
257 m_fullDefs.insert(key: it->name().toLower(), value: *it);
258 for (const auto &altName : it->alternativeNames()) {
259 m_fullDefs.insert(key: altName.toLower(), value: *it);
260 }
261 }
262
263 // load themes
264
265 // do lookup in standard paths, if not disabled
266#ifndef NO_STANDARD_PATHS
267 for (const auto &dir : QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation,
268 QStringLiteral("org.kde.syntax-highlighting/themes"),
269 options: QStandardPaths::LocateDirectory)) {
270 loadThemeFolder(path: dir);
271 }
272#endif
273
274 // default resources are always used
275 loadThemeFolder(QStringLiteral(":/org.kde.syntax-highlighting/themes"));
276
277 // extra resources provided by 3rdparty libraries/applications
278 loadThemeFolder(QStringLiteral(":/org.kde.syntax-highlighting/themes-addons"));
279
280 // user given extra paths
281 for (const auto &path : std::as_const(t&: m_customSearchPaths)) {
282 loadThemeFolder(path: path + QStringLiteral("/themes"));
283 }
284}
285
286void RepositoryPrivate::loadSyntaxFolder(Repository *repo, const QString &path)
287{
288 QDirIterator it(path, QStringList() << QLatin1String("*.xml"), QDir::Files);
289 while (it.hasNext()) {
290 Definition def;
291 auto defData = DefinitionData::get(def);
292 defData->repo = repo;
293 if (defData->loadMetaData(definitionFileName: it.next())) {
294 addDefinition(def);
295 }
296 }
297}
298
299bool RepositoryPrivate::loadSyntaxFolderFromIndex(Repository *repo, const QString &path)
300{
301 QFile indexFile(path + QLatin1String("/index.katesyntax"));
302 if (!indexFile.open(flags: QFile::ReadOnly)) {
303 return false;
304 }
305
306 const auto indexDoc(QCborValue::fromCbor(ba: indexFile.readAll()));
307 const auto index = indexDoc.toMap();
308 for (auto it = index.begin(); it != index.end(); ++it) {
309 if (!it.value().isMap()) {
310 continue;
311 }
312 const auto fileName = QString(path + QLatin1Char('/') + it.key().toString());
313 const auto defMap = it.value().toMap();
314 Definition def;
315 auto defData = DefinitionData::get(def);
316 defData->repo = repo;
317 if (defData->loadMetaData(fileName, obj: defMap)) {
318 addDefinition(def);
319 }
320 }
321
322 return true;
323}
324
325void RepositoryPrivate::addDefinition(const Definition &def)
326{
327 const auto it = m_defs.constFind(key: def.name());
328 if (it == m_defs.constEnd()) {
329 m_defs.insert(key: def.name(), value: def);
330 return;
331 }
332
333 if (it.value().version() >= def.version()) {
334 return;
335 }
336 m_defs.insert(key: def.name(), value: def);
337}
338
339void RepositoryPrivate::loadThemeFolder(const QString &path)
340{
341 QDirIterator it(path, QStringList() << QLatin1String("*.theme"), QDir::Files);
342 while (it.hasNext()) {
343 auto themeData = std::unique_ptr<ThemeData>(new ThemeData);
344 if (themeData->load(filePath: it.next())) {
345 addTheme(theme: Theme(themeData.release()));
346 }
347 }
348}
349
350static int themeRevision(const Theme &theme)
351{
352 auto data = ThemeData::get(theme);
353 return data->revision();
354}
355
356void RepositoryPrivate::addTheme(const Theme &theme)
357{
358 const auto &constThemes = m_themes;
359 const auto themeName = theme.name();
360 const auto constIt = lowerBoundTheme(themes: constThemes, themeName);
361 const auto index = constIt - constThemes.begin();
362 if (constIt == constThemes.end() || (*constIt).name() != themeName) {
363 m_themes.insert(i: index, t: theme);
364 return;
365 }
366 if (themeRevision(theme: *constIt) < themeRevision(theme)) {
367 m_themes[index] = theme;
368 }
369}
370
371int RepositoryPrivate::foldingRegionId(const QString &defName, const QString &foldName)
372{
373 const auto it = m_foldingRegionIds.constFind(key: qMakePair(value1: defName, value2: foldName));
374 if (it != m_foldingRegionIds.constEnd()) {
375 return it.value();
376 }
377 Q_ASSERT(m_foldingRegionId < std::numeric_limits<int>::max());
378 m_foldingRegionIds.insert(key: qMakePair(value1: defName, value2: foldName), value: ++m_foldingRegionId);
379 return m_foldingRegionId;
380}
381
382int RepositoryPrivate::nextFormatId()
383{
384 Q_ASSERT(m_formatId < std::numeric_limits<int>::max());
385 return ++m_formatId;
386}
387
388void Repository::reload()
389{
390 Q_EMIT aboutToReload();
391
392 for (const auto &def : std::as_const(t&: d->m_sortedDefs)) {
393 DefinitionData::get(def)->clear();
394 }
395 d->m_defs.clear();
396 d->m_sortedDefs.clear();
397 d->m_fullDefs.clear();
398
399 d->m_themes.clear();
400
401 d->m_foldingRegionId = 0;
402 d->m_foldingRegionIds.clear();
403
404 d->m_formatId = 0;
405
406 d->load(repo: this);
407
408 Q_EMIT reloaded();
409}
410
411void Repository::addCustomSearchPath(const QString &path)
412{
413 d->m_customSearchPaths.append(t: path);
414 reload();
415}
416
417QList<QString> Repository::customSearchPaths() const
418{
419 return d->m_customSearchPaths;
420}
421
422#include "moc_repository.cpp"
423

source code of syntax-highlighting/src/lib/repository.cpp