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

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