TextMateLib 1.0
Modern C++ implementation of the TextMate syntax highlighting engine
Loading...
Searching...
No Matches
theme.cpp
1#include "theme.h"
2#include <algorithm>
3#include <sstream>
4
5namespace tml {
6
7// ScopeStack implementation
8
9ScopeStack::ScopeStack(ScopeStack* parent_, const ScopeName& scopeName_)
10 : parent(parent_), scopeName(scopeName_) {
11}
12
13ScopeStack::~ScopeStack() {
14 // Note: parent is not owned, don't delete
15}
16
17ScopeStack* ScopeStack::push(ScopeStack* path, const std::vector<ScopeName>& scopeNames) {
18 for (const auto& name : scopeNames) {
19 path = new ScopeStack(path, name);
20 }
21 return path;
22}
23
24ScopeStack* ScopeStack::from(const std::vector<ScopeName>& segments) {
25 ScopeStack* result = nullptr;
26 for (const auto& segment : segments) {
27 result = new ScopeStack(result, segment);
28 }
29 return result;
30}
31
32ScopeStack* ScopeStack::push(const ScopeName& scopeName_) {
33 return new ScopeStack(this, scopeName_);
34}
35
36std::vector<ScopeName> ScopeStack::getSegments() const {
37 std::vector<ScopeName> result;
38 const ScopeStack* item = this;
39 while (item) {
40 result.push_back(item->scopeName);
41 item = item->parent;
42 }
43 std::reverse(result.begin(), result.end());
44 return result;
45}
46
47std::string ScopeStack::toString() const {
48 std::vector<ScopeName> segments = getSegments();
49 std::string result;
50 for (size_t i = 0; i < segments.size(); i++) {
51 if (i > 0) result += " ";
52 result += segments[i];
53 }
54 return result;
55}
56
57bool ScopeStack::extends(const ScopeStack* other) const {
58 if (this == other) {
59 return true;
60 }
61 if (parent == nullptr) {
62 return false;
63 }
64 return parent->extends(other);
65}
66
67std::vector<ScopeName>* ScopeStack::getExtensionIfDefined(const ScopeStack* base) const {
68 std::vector<ScopeName>* result = new std::vector<ScopeName>();
69 const ScopeStack* item = this;
70 while (item && item != base) {
71 result->push_back(item->scopeName);
72 item = item->parent;
73 }
74 if (item == base) {
75 std::reverse(result->begin(), result->end());
76 return result;
77 }
78 delete result;
79 return nullptr;
80}
81
82// StyleAttributes implementation
83
84StyleAttributes::StyleAttributes(int fontStyle_, int foregroundId_, int backgroundId_)
85 : fontStyle(fontStyle_), foregroundId(foregroundId_), backgroundId(backgroundId_) {
86}
87
88// ParsedThemeRule implementation
89
90ParsedThemeRule::ParsedThemeRule(const ScopeName& scope_,
91 std::vector<ScopeName>* parentScopes_,
92 int index_,
93 int fontStyle_,
94 const std::string* foreground_,
95 const std::string* background_)
96 : scope(scope_), parentScopes(parentScopes_), index(index_), fontStyle(fontStyle_),
97 foreground(foreground_ ? new std::string(*foreground_) : nullptr),
98 background(background_ ? new std::string(*background_) : nullptr) {
99}
100
101ParsedThemeRule::~ParsedThemeRule() {
102 delete parentScopes;
103 delete foreground;
104 delete background;
105}
106
107// Parse theme
108
109std::vector<ParsedThemeRule*> parseTheme(const IRawTheme* source) {
110 std::vector<ParsedThemeRule*> result;
111
112 if (!source || source->settings.empty()) {
113 return result;
114 }
115
116 for (size_t i = 0; i < source->settings.size(); i++) {
117 const IRawThemeSetting* entry = source->settings[i];
118
119 std::vector<std::string> scopes;
120 if (entry->scope != nullptr && !entry->scope->empty()) {
121 scopes = *entry->scope;
122 } else if (!entry->scopeString.empty()) {
123 std::string scopeStr = entry->scopeString;
124
125 // Remove leading and trailing commas
126 size_t start = scopeStr.find_first_not_of(',');
127 size_t end = scopeStr.find_last_not_of(',');
128 if (start != std::string::npos && end != std::string::npos) {
129 scopeStr = scopeStr.substr(start, end - start + 1);
130 }
131
132 // Split by comma
133 size_t pos = 0;
134 while ((pos = scopeStr.find(',')) != std::string::npos) {
135 scopes.push_back(scopeStr.substr(0, pos));
136 scopeStr.erase(0, pos + 1);
137 }
138 if (!scopeStr.empty()) {
139 scopes.push_back(scopeStr);
140 }
141 } else {
142 scopes.push_back("");
143 }
144
145 int fontStyle = static_cast<int>(FontStyle::NotSet);
146 if (entry->settings.fontStyle != nullptr) {
147 fontStyle = static_cast<int>(FontStyle::None);
148
149 std::string fontStyleStr = *entry->settings.fontStyle;
150 std::istringstream iss(fontStyleStr);
151 std::string segment;
152 while (iss >> segment) {
153 if (segment == "italic") {
154 fontStyle |= static_cast<int>(FontStyle::Italic);
155 } else if (segment == "bold") {
156 fontStyle |= static_cast<int>(FontStyle::Bold);
157 } else if (segment == "underline") {
158 fontStyle |= static_cast<int>(FontStyle::Underline);
159 } else if (segment == "strikethrough") {
160 fontStyle |= static_cast<int>(FontStyle::Strikethrough);
161 }
162 }
163 }
164
165 std::string* foreground = nullptr;
166 if (entry->settings.foreground != nullptr && isValidHexColor(*entry->settings.foreground)) {
167 foreground = new std::string(*entry->settings.foreground);
168 }
169
170 std::string* background = nullptr;
171 if (entry->settings.background != nullptr && isValidHexColor(*entry->settings.background)) {
172 background = new std::string(*entry->settings.background);
173 }
174
175 for (const auto& scopeStr : scopes) {
176 std::string trimmedScope = scopeStr;
177 // Trim whitespace
178 trimmedScope.erase(0, trimmedScope.find_first_not_of(" \t\n\r"));
179 trimmedScope.erase(trimmedScope.find_last_not_of(" \t\n\r") + 1);
180
181 // Split by spaces
182 std::vector<std::string> segments;
183 std::istringstream iss(trimmedScope);
184 std::string segment;
185 while (iss >> segment) {
186 segments.push_back(segment);
187 }
188
189 std::string scope = segments.empty() ? "" : segments.back();
190 std::vector<ScopeName>* parentScopes = nullptr;
191 if (segments.size() > 1) {
192 parentScopes = new std::vector<ScopeName>(segments.begin(), segments.end() - 1);
193 std::reverse(parentScopes->begin(), parentScopes->end());
194 }
195
196 result.push_back(new ParsedThemeRule(
197 scope,
198 parentScopes,
199 i,
200 fontStyle,
201 foreground,
202 background
203 ));
204 }
205
206 delete foreground;
207 delete background;
208 }
209
210 return result;
211}
212
213// Font style to string
214
215std::string fontStyleToString(int fontStyle) {
216 if (fontStyle == static_cast<int>(FontStyle::NotSet)) {
217 return "not set";
218 }
219
220 std::string style;
221 if (fontStyle & static_cast<int>(FontStyle::Italic)) {
222 style += "italic ";
223 }
224 if (fontStyle & static_cast<int>(FontStyle::Bold)) {
225 style += "bold ";
226 }
227 if (fontStyle & static_cast<int>(FontStyle::Underline)) {
228 style += "underline ";
229 }
230 if (fontStyle & static_cast<int>(FontStyle::Strikethrough)) {
231 style += "strikethrough ";
232 }
233 if (style.empty()) {
234 style = "none";
235 } else {
236 // Remove trailing space
237 style = style.substr(0, style.length() - 1);
238 }
239 return style;
240}
241
242// ColorMap implementation
243
244ColorMap::ColorMap(const std::vector<std::string>* colorMap)
245 : _lastColorId(0) {
246
247 if (colorMap != nullptr && !colorMap->empty()) {
248 _isFrozen = true;
249 for (size_t i = 0; i < colorMap->size(); i++) {
250 std::string upperColor = (*colorMap)[i];
251 std::transform(upperColor.begin(), upperColor.end(), upperColor.begin(), ::toupper);
252 _color2id[upperColor] = i;
253 _id2color.push_back((*colorMap)[i]);
254 }
255 _lastColorId = colorMap->size() - 1;
256 } else {
257 _isFrozen = false;
258 }
259}
260
261int ColorMap::getId(const std::string* color) {
262 if (color == nullptr) {
263 return 0;
264 }
265
266 std::string upperColor = *color;
267 std::transform(upperColor.begin(), upperColor.end(), upperColor.begin(), ::toupper);
268
269 auto it = _color2id.find(upperColor);
270 if (it != _color2id.end()) {
271 return it->second;
272 }
273
274 if (_isFrozen) {
275 // In frozen mode, we should throw an error
276 // For now, return 0
277 return 0;
278 }
279
280 int value = ++_lastColorId;
281 _color2id[upperColor] = value;
282 if (_id2color.size() <= static_cast<size_t>(value)) {
283 _id2color.resize(value + 1);
284 }
285 _id2color[value] = *color;
286 return value;
287}
288
289std::vector<std::string> ColorMap::getColorMap() const {
290 return _id2color;
291}
292
293// ThemeTrieElementRule implementation
294
295ThemeTrieElementRule::ThemeTrieElementRule(int scopeDepth_,
296 const std::vector<ScopeName>* parentScopes_,
297 int fontStyle_,
298 int foreground_,
299 int background_)
300 : scopeDepth(scopeDepth_),
301 parentScopes(parentScopes_ ? *parentScopes_ : std::vector<ScopeName>()),
302 fontStyle(fontStyle_),
303 foreground(foreground_),
304 background(background_) {
305}
306
307ThemeTrieElementRule* ThemeTrieElementRule::clone() const {
308 return new ThemeTrieElementRule(scopeDepth, &parentScopes, fontStyle, foreground, background);
309}
310
311std::vector<ThemeTrieElementRule*> ThemeTrieElementRule::cloneArr(const std::vector<ThemeTrieElementRule*>& arr) {
312 std::vector<ThemeTrieElementRule*> result;
313 for (auto* rule : arr) {
314 result.push_back(rule->clone());
315 }
316 return result;
317}
318
319void ThemeTrieElementRule::acceptOverwrite(int scopeDepth_, int fontStyle_, int foreground_, int background_) {
320 if (scopeDepth_ > this->scopeDepth) {
321 this->scopeDepth = scopeDepth_;
322 }
323
324 if (fontStyle_ != static_cast<int>(FontStyle::NotSet)) {
325 this->fontStyle = fontStyle_;
326 }
327 if (foreground_ != 0) {
328 this->foreground = foreground_;
329 }
330 if (background_ != 0) {
331 this->background = background_;
332 }
333}
334
335// ThemeTrieElement implementation
336
337ThemeTrieElement::ThemeTrieElement(ThemeTrieElementRule* mainRule,
338 const std::vector<ThemeTrieElementRule*>& rulesWithParentScopes)
339 : _mainRule(mainRule), _rulesWithParentScopes(rulesWithParentScopes) {
340}
341
342ThemeTrieElement::~ThemeTrieElement() {
343 delete _mainRule;
344 for (auto* rule : _rulesWithParentScopes) {
345 delete rule;
346 }
347 for (auto& pair : _children) {
348 delete pair.second;
349 }
350}
351
352std::vector<ThemeTrieElementRule*> ThemeTrieElement::match(const ScopeName& scope) {
353 // Collect all matching rules
354 std::vector<ThemeTrieElementRule*> result;
355
356 // Check main rule
357 if (_mainRule) {
358 result.push_back(_mainRule);
359 }
360
361 // Add rules with parent scopes
362 result.insert(result.end(), _rulesWithParentScopes.begin(), _rulesWithParentScopes.end());
363
364 // If scope is empty, return current rules
365 if (scope.empty()) {
366 return result;
367 }
368
369 // Traverse trie segment by segment (matching insert() behavior)
370 size_t dotIndex = scope.find('.');
371 std::string head = (dotIndex == std::string::npos) ? scope : scope.substr(0, dotIndex);
372 std::string tail = (dotIndex == std::string::npos) ? "" : scope.substr(dotIndex + 1);
373
374 // Look for exact match on first segment
375 auto it = _children.find(head);
376 if (it != _children.end()) {
377 auto childMatches = it->second->match(tail);
378 result.insert(result.end(), childMatches.begin(), childMatches.end());
379 }
380
381 return result;
382}
383
384void ThemeTrieElement::insert(int scopeDepth,
385 const std::string& scope,
386 const std::vector<ScopeName>* parentScopes,
387 int fontStyle,
388 int foreground,
389 int background) {
390 if (scope.empty()) {
391 // Reached the end of the scope path
392 if (parentScopes == nullptr || parentScopes->empty()) {
393 _mainRule->acceptOverwrite(scopeDepth, fontStyle, foreground, background);
394 } else {
395 // Has parent scopes
396 bool found = false;
397 for (auto* rule : _rulesWithParentScopes) {
398 if (rule->parentScopes == *parentScopes) {
399 rule->acceptOverwrite(scopeDepth, fontStyle, foreground, background);
400 found = true;
401 break;
402 }
403 }
404 if (!found) {
405 _rulesWithParentScopes.push_back(
406 new ThemeTrieElementRule(scopeDepth, parentScopes, fontStyle, foreground, background)
407 );
408 }
409 }
410 return;
411 }
412
413 // Find dot separator
414 size_t dotIndex = scope.find('.');
415 std::string head;
416 std::string tail;
417
418 if (dotIndex == std::string::npos) {
419 head = scope;
420 tail = "";
421 } else {
422 head = scope.substr(0, dotIndex);
423 tail = scope.substr(dotIndex + 1);
424 }
425
426 // Get or create child
427 ThemeTrieElement* child = nullptr;
428 auto it = _children.find(head);
429 if (it != _children.end()) {
430 child = it->second;
431 } else {
432 // Create intermediate child with placeholder rule (no style yet)
433 // The actual style will be set via acceptOverwrite when we reach the final destination
434 child = new ThemeTrieElement(
435 new ThemeTrieElementRule(0, nullptr, static_cast<int>(FontStyle::NotSet), 0, 0),
436 std::vector<ThemeTrieElementRule*>()
437 );
438 _children[head] = child;
439 }
440
441 child->insert(scopeDepth + 1, tail, parentScopes, fontStyle, foreground, background);
442}
443
444// Helper functions
445
446bool _matchesScope(const ScopeName& scopeName, const ScopeName& scopePattern) {
447 return scopePattern == scopeName ||
448 (scopeName.length() > scopePattern.length() &&
449 scopeName.substr(0, scopePattern.length()) == scopePattern &&
450 scopeName[scopePattern.length()] == '.');
451}
452
453bool _scopePathMatchesParentScopes(ScopeStack* scopePath, const std::vector<ScopeName>& parentScopes) {
454 if (parentScopes.empty()) {
455 return true;
456 }
457
458 for (size_t index = 0; index < parentScopes.size(); index++) {
459 std::string scopePattern = parentScopes[index];
460 bool scopeMustMatch = false;
461
462 // Check for child combinator
463 if (scopePattern == ">") {
464 if (index == parentScopes.size() - 1) {
465 return false;
466 }
467 scopePattern = parentScopes[++index];
468 scopeMustMatch = true;
469 }
470
471 while (scopePath) {
472 if (_matchesScope(scopePath->scopeName, scopePattern)) {
473 break;
474 }
475 if (scopeMustMatch) {
476 return false;
477 }
478 scopePath = scopePath->parent;
479 }
480
481 if (!scopePath) {
482 return false;
483 }
484 scopePath = scopePath->parent;
485 }
486
487 return true;
488}
489
490// Resolve parsed theme rules
491
492Theme* resolveParsedThemeRules(std::vector<ParsedThemeRule*>& parsedThemeRules,
493 const std::vector<std::string>* colorMap) {
494 // Sort rules
495 std::sort(parsedThemeRules.begin(), parsedThemeRules.end(),
496 [](const ParsedThemeRule* a, const ParsedThemeRule* b) {
497 int r = strcmp_custom(a->scope, b->scope);
498 if (r != 0) return r < 0;
499 r = strArrCmp(a->parentScopes, b->parentScopes);
500 if (r != 0) return r < 0;
501 return a->index < b->index;
502 });
503
504 // Determine defaults
505 int defaultFontStyle = static_cast<int>(FontStyle::None);
506 std::string defaultForeground = "#000000";
507 std::string defaultBackground = "#ffffff";
508
509 while (!parsedThemeRules.empty() && parsedThemeRules[0]->scope.empty()) {
510 ParsedThemeRule* incomingDefaults = parsedThemeRules[0];
511 parsedThemeRules.erase(parsedThemeRules.begin());
512
513 if (incomingDefaults->fontStyle != static_cast<int>(FontStyle::NotSet)) {
514 defaultFontStyle = incomingDefaults->fontStyle;
515 }
516 if (incomingDefaults->foreground != nullptr) {
517 defaultForeground = *incomingDefaults->foreground;
518 }
519 if (incomingDefaults->background != nullptr) {
520 defaultBackground = *incomingDefaults->background;
521 }
522
523 delete incomingDefaults;
524 }
525
526 ColorMap* colorMapObj = new ColorMap(colorMap);
527 StyleAttributes* defaults = new StyleAttributes(
528 defaultFontStyle,
529 colorMapObj->getId(&defaultForeground),
530 colorMapObj->getId(&defaultBackground)
531 );
532
533 ThemeTrieElement* root = new ThemeTrieElement(
534 new ThemeTrieElementRule(0, nullptr, static_cast<int>(FontStyle::NotSet), 0, 0),
535 std::vector<ThemeTrieElementRule*>()
536 );
537
538 for (auto* rule : parsedThemeRules) {
539 root->insert(0, rule->scope, rule->parentScopes,
540 rule->fontStyle,
541 colorMapObj->getId(rule->foreground),
542 colorMapObj->getId(rule->background));
543 }
544
545 return new Theme(colorMapObj, defaults, root);
546}
547
548// Theme implementation
549
550Theme::Theme(ColorMap* colorMap, StyleAttributes* defaults, ThemeTrieElement* root)
551 : _colorMap(colorMap), _defaults(defaults), _root(root) {
552
553 _cachedMatchRoot = new CachedFn<ScopeName, std::vector<ThemeTrieElementRule*>>(
554 [this](const ScopeName& scopeName) {
555 return this->_root->match(scopeName);
556 }
557 );
558}
559
560Theme::~Theme() {
561 delete _colorMap;
562 delete _defaults;
563 delete _root;
564 delete _cachedMatchRoot;
565}
566
567Theme* Theme::createFromRawTheme(const IRawTheme* source, const std::vector<std::string>* colorMap) {
568 return createFromParsedTheme(parseTheme(source), colorMap);
569}
570
571Theme* Theme::createFromParsedTheme(const std::vector<ParsedThemeRule*>& source,
572 const std::vector<std::string>* colorMap) {
573 std::vector<ParsedThemeRule*> mutableSource = source;
574 return resolveParsedThemeRules(mutableSource, colorMap);
575}
576
577std::vector<std::string> Theme::getColorMap() const {
578 return _colorMap->getColorMap();
579}
580
581StyleAttributes* Theme::getDefaults() const {
582 return _defaults;
583}
584
585StyleAttributes* Theme::match(ScopeStack* scopePath) {
586 if (scopePath == nullptr) {
587 return _defaults;
588 }
589
590 const ScopeName& scopeName = scopePath->scopeName;
591 std::vector<ThemeTrieElementRule*> matchingTrieElements = _cachedMatchRoot->get(scopeName);
592
593 // Find the effective rule
594 ThemeTrieElementRule* effectiveRule = nullptr;
595 // Find the most specific matching rule
596 // Rules are returned in order from root (least specific) to deepest (most specific)
597 // We want the last/deepest match, so don't break - keep iterating
598 for (auto* rule : matchingTrieElements) {
599 if (_scopePathMatchesParentScopes(scopePath->parent, rule->parentScopes)) {
600 effectiveRule = rule;
601 // Continue to find more specific matches (deeper in the trie)
602 }
603 }
604
605 if (!effectiveRule) {
606 return nullptr;
607 }
608
609 return new StyleAttributes(
610 effectiveRule->fontStyle,
611 effectiveRule->foreground,
612 effectiveRule->background
613 );
614}
615
616} // namespace tml
std::string ScopeName
Semantic name identifying a scope (e.g., "source.javascript", "comment.line")
Definition types.h:20