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(strings.cbegin(), strings.cend(), [str](QStringView wildcard) {
47 return WildcardMatcher::exactMatch(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(strings.cbegin(), strings.cend(), [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(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(d->m_sortedDefs)) {
116 DefinitionData::get(def)->repo = nullptr;
117 }
118}
119
120Definition Repository::definitionForName(const QString &defName) const
121{
122 return d->m_defs.value(defName);
123}
124
125Definition Repository::definitionForFileName(const QString &fileName) const
126{
127 return findHighestPriorityDefinitionIf(d->m_defs, anyWildcardMatches(fileNameFromFilePath(fileName)));
128}
129
130QList<Definition> Repository::definitionsForFileName(const QString &fileName) const
131{
132 return findDefinitionsIf(d->m_defs, anyWildcardMatches(fileNameFromFilePath(fileName)));
133}
134
135Definition Repository::definitionForMimeType(const QString &mimeType) const
136{
137 return findHighestPriorityDefinitionIf(d->m_defs, anyMimeTypeEquals(mimeType));
138}
139
140QList<Definition> Repository::definitionsForMimeType(const QString &mimeType) const
141{
142 return findDefinitionsIf(d->m_defs, anyMimeTypeEquals(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(themes.begin(), themes.end(), themeName, [](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(QPalette::Base);
183
184 // find themes with matching background colors
185 QList<const KSyntaxHighlighting::Theme *> matchingThemes;
186 for (const auto &theme : std::as_const(d->m_themes)) {
187 const auto background = theme.editorColor(KSyntaxHighlighting::Theme::EditorColorRole::BackgroundColor);
188 if (background == base.rgb()) {
189 matchingThemes.append(&theme);
190 }
191 }
192 if (!matchingThemes.empty()) {
193 // if there's multiple, search for one with a matching highlight color
194 const auto highlight = palette.color(QPalette::Highlight);
195 for (const auto *theme : std::as_const(matchingThemes)) {
196 auto selection = theme->editorColor(KSyntaxHighlighting::Theme::EditorColorRole::TextSelection);
197 if (selection == highlight.rgb()) {
198 return *theme;
199 }
200 }
201 return *matchingThemes.first();
202 }
203
204 // fallback to just use the default light or dark theme
205 return defaultTheme((base.lightness() < 128) ? KSyntaxHighlighting::Repository::DarkTheme : KSyntaxHighlighting::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(QStandardPaths::GenericDataLocation,
227 QStringLiteral("org.kde.syntax-highlighting/syntax"),
228 QStandardPaths::LocateDirectory)) {
229 loadSyntaxFolder(repo, dir);
230 }
231
232 // backward compatibility with Kate
233 for (const auto &dir :
234 QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("katepart5/syntax"), QStandardPaths::LocateDirectory)) {
235 loadSyntaxFolder(repo, dir);
236 }
237#endif
238
239 // default resources are always used, this is the one location that has a index cbor file
240 loadSyntaxFolderFromIndex(repo, path: QStringLiteral(":/org.kde.syntax-highlighting/syntax"));
241
242 // extra resources provided by 3rdparty libraries/applications
243 loadSyntaxFolder(repo, path: QStringLiteral(":/org.kde.syntax-highlighting/syntax-addons"));
244
245 // user given extra paths
246 for (const auto &path : std::as_const(m_customSearchPaths)) {
247 loadSyntaxFolder(repo, path + QStringLiteral("/syntax"));
248 }
249
250 m_sortedDefs.reserve(m_defs.size());
251 for (auto it = m_defs.constBegin(); it != m_defs.constEnd(); ++it) {
252 m_sortedDefs.push_back(it.value());
253 }
254 std::sort(m_sortedDefs.begin(), m_sortedDefs.end(), [](const Definition &left, const Definition &right) {
255 auto comparison = left.translatedSection().compare(right.translatedSection(), Qt::CaseInsensitive);
256 if (comparison == 0) {
257 comparison = left.translatedName().compare(right.translatedName(), Qt::CaseInsensitive);
258 }
259 return comparison < 0;
260 });
261
262 // load themes
263
264 // do lookup in standard paths, if not disabled
265#ifndef NO_STANDARD_PATHS
266 for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
267 QStringLiteral("org.kde.syntax-highlighting/themes"),
268 QStandardPaths::LocateDirectory)) {
269 loadThemeFolder(dir);
270 }
271#endif
272
273 // default resources are always used
274 loadThemeFolder(path: QStringLiteral(":/org.kde.syntax-highlighting/themes"));
275
276 // extra resources provided by 3rdparty libraries/applications
277 loadThemeFolder(path: QStringLiteral(":/org.kde.syntax-highlighting/themes-addons"));
278
279 // user given extra paths
280 for (const auto &path : std::as_const(m_customSearchPaths)) {
281 loadThemeFolder(path + QStringLiteral("/themes"));
282 }
283}
284
285void RepositoryPrivate::loadSyntaxFolder(Repository *repo, const QString &path)
286{
287 QDirIterator it(path, QStringList() << QLatin1String("*.xml"), QDir::Files);
288 while (it.hasNext()) {
289 Definition def;
290 auto defData = DefinitionData::get(def);
291 defData->repo = repo;
292 if (defData->loadMetaData(it.next())) {
293 addDefinition(def);
294 }
295 }
296}
297
298bool RepositoryPrivate::loadSyntaxFolderFromIndex(Repository *repo, const QString &path)
299{
300 QFile indexFile(path + QLatin1String("/index.katesyntax"));
301 if (!indexFile.open(QFile::ReadOnly)) {
302 return false;
303 }
304
305 const auto indexDoc(QCborValue::fromCbor(indexFile.readAll()));
306 const auto index = indexDoc.toMap();
307 for (auto it = index.begin(); it != index.end(); ++it) {
308 if (!it.value().isMap()) {
309 continue;
310 }
311 const auto fileName = QString(path + QLatin1Char('/') + it.key().toString());
312 const auto defMap = it.value().toMap();
313 Definition def;
314 auto defData = DefinitionData::get(def);
315 defData->repo = repo;
316 if (defData->loadMetaData(fileName, defMap)) {
317 addDefinition(def);
318 }
319 }
320
321 return true;
322}
323
324void RepositoryPrivate::addDefinition(const Definition &def)
325{
326 const auto it = m_defs.constFind(def.name());
327 if (it == m_defs.constEnd()) {
328 m_defs.insert(def.name(), def);
329 return;
330 }
331
332 if (it.value().version() >= def.version()) {
333 return;
334 }
335 m_defs.insert(def.name(), def);
336}
337
338void RepositoryPrivate::loadThemeFolder(const QString &path)
339{
340 QDirIterator it(path, QStringList() << QLatin1String("*.theme"), QDir::Files);
341 while (it.hasNext()) {
342 auto themeData = std::unique_ptr<ThemeData>(new ThemeData);
343 if (themeData->load(filePath: it.next())) {
344 addTheme(theme: Theme(themeData.release()));
345 }
346 }
347}
348
349static int themeRevision(const Theme &theme)
350{
351 auto data = ThemeData::get(theme);
352 return data->revision();
353}
354
355void RepositoryPrivate::addTheme(const Theme &theme)
356{
357 const auto &constThemes = m_themes;
358 const auto themeName = theme.name();
359 const auto constIt = lowerBoundTheme(constThemes, themeName);
360 const auto index = constIt - constThemes.begin();
361 if (constIt == constThemes.end() || (*constIt).name() != themeName) {
362 m_themes.insert(index, theme);
363 return;
364 }
365 if (themeRevision(*constIt) < themeRevision(theme)) {
366 m_themes[index] = theme;
367 }
368}
369
370int RepositoryPrivate::foldingRegionId(const QString &defName, const QString &foldName)
371{
372 const auto it = m_foldingRegionIds.constFind(qMakePair(defName, foldName));
373 if (it != m_foldingRegionIds.constEnd()) {
374 return it.value();
375 }
376 Q_ASSERT(m_foldingRegionId < std::numeric_limits<int>::max());
377 m_foldingRegionIds.insert(qMakePair(defName, foldName), ++m_foldingRegionId);
378 return m_foldingRegionId;
379}
380
381int RepositoryPrivate::nextFormatId()
382{
383 Q_ASSERT(m_formatId < std::numeric_limits<int>::max());
384 return ++m_formatId;
385}
386
387void Repository::reload()
388{
389 Q_EMIT aboutToReload();
390
391 for (const auto &def : std::as_const(d->m_sortedDefs)) {
392 DefinitionData::get(def)->clear();
393 }
394 d->m_defs.clear();
395 d->m_sortedDefs.clear();
396
397 d->m_themes.clear();
398
399 d->m_foldingRegionId = 0;
400 d->m_foldingRegionIds.clear();
401
402 d->m_formatId = 0;
403
404 d->load(repo: this);
405
406 Q_EMIT reloaded();
407}
408
409void Repository::addCustomSearchPath(const QString &path)
410{
411 d->m_customSearchPaths.append(path);
412 reload();
413}
414
415QList<QString> Repository::customSearchPaths() const
416{
417 return d->m_customSearchPaths;
418}
419
420#include "moc_repository.cpp"
421

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