1/*
2 SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
3 SPDX-FileCopyrightText: 2018 Christoph Cullmann <cullmann@kde.org>
4 SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen+kde@gmail.com>
5
6 SPDX-License-Identifier: MIT
7*/
8
9#include "context_p.h"
10#include "definition_p.h"
11#include "dynamicregexpcache_p.h"
12#include "ksyntaxhighlighting_logging.h"
13#include "rule_p.h"
14#include "worddelimiters_p.h"
15#include "xml_p.h"
16
17using namespace KSyntaxHighlighting;
18
19// QChar::isDigit() match any digit in unicode (romain numeral, etc)
20static bool isDigit(QChar c)
21{
22 return (c <= QLatin1Char('9') && QLatin1Char('0') <= c);
23}
24
25static bool isOctalChar(QChar c)
26{
27 return (c <= QLatin1Char('7') && QLatin1Char('0') <= c);
28}
29
30static bool isHexChar(QChar c)
31{
32 return isDigit(c) || (c <= QLatin1Char('f') && QLatin1Char('a') <= c) || (c <= QLatin1Char('F') && QLatin1Char('A') <= c);
33}
34
35static int matchEscapedChar(QStringView text, int offset)
36{
37 if (text.at(n: offset) != QLatin1Char('\\') || text.size() < offset + 2) {
38 return offset;
39 }
40
41 const auto c = text.at(n: offset + 1);
42 switch (c.unicode()) {
43 // control chars
44 case 'a':
45 case 'b':
46 case 'e':
47 case 'f':
48 case 'n':
49 case 'r':
50 case 't':
51 case 'v':
52 case '"':
53 case '\'':
54 case '?':
55 case '\\':
56 return offset + 2;
57
58 // hex encoded character
59 case 'x':
60 if (offset + 2 < text.size() && isHexChar(c: text.at(n: offset + 2))) {
61 if (offset + 3 < text.size() && isHexChar(c: text.at(n: offset + 3))) {
62 return offset + 4;
63 }
64 return offset + 3;
65 }
66 return offset;
67
68 // octal encoding, simple \0 is OK, too, unlike simple \x above
69 case '0':
70 case '1':
71 case '2':
72 case '3':
73 case '4':
74 case '5':
75 case '6':
76 case '7':
77 if (offset + 2 < text.size() && isOctalChar(c: text.at(n: offset + 2))) {
78 if (offset + 3 < text.size() && isOctalChar(c: text.at(n: offset + 3))) {
79 return offset + 4;
80 }
81 return offset + 3;
82 }
83 return offset + 2;
84 }
85
86 return offset;
87}
88
89static QString replaceCaptures(const QString &pattern, const QStringList &captures, bool quote)
90{
91 auto result = pattern;
92 for (int i = captures.size(); i >= 1; --i) {
93 result.replace(before: QLatin1Char('%') + QString::number(i), after: quote ? QRegularExpression::escape(str: captures.at(i: i - 1)) : captures.at(i: i - 1));
94 }
95 return result;
96}
97
98static MatchResult matchString(QStringView pattern, QStringView text, int offset, Qt::CaseSensitivity caseSensitivity)
99{
100 if (offset + pattern.size() <= text.size() && text.mid(pos: offset, n: pattern.size()).compare(other: pattern, cs: caseSensitivity) == 0) {
101 return offset + pattern.size();
102 }
103 return offset;
104}
105
106static void resolveAdditionalWordDelimiters(WordDelimiters &wordDelimiters, const HighlightingContextData::Rule::WordDelimiters &delimiters)
107{
108 // cache for DefinitionData::wordDelimiters, is accessed VERY often
109 if (!delimiters.additionalDeliminator.isEmpty() || !delimiters.weakDeliminator.isEmpty()) {
110 wordDelimiters.append(s: QStringView(delimiters.additionalDeliminator));
111 wordDelimiters.remove(c: QStringView(delimiters.weakDeliminator));
112 }
113}
114
115Rule::~Rule() = default;
116
117const IncludeRules *Rule::castToIncludeRules() const
118{
119 if (m_type != Type::IncludeRules) {
120 return nullptr;
121 }
122 return static_cast<const IncludeRules *>(this);
123}
124
125bool Rule::resolveCommon(DefinitionData &def, const HighlightingContextData::Rule &ruleData, QStringView lookupContextName)
126{
127 switch (ruleData.type) {
128 // IncludeRules uses this with a different semantic
129 case HighlightingContextData::Rule::Type::IncludeRules:
130 m_type = Type::IncludeRules;
131 return true;
132 case HighlightingContextData::Rule::Type::LineContinue:
133 m_type = Type::LineContinue;
134 break;
135 default:
136 m_type = Type::OtherRule;
137 break;
138 }
139
140 /**
141 * try to get our format from the definition we stem from
142 */
143 if (!ruleData.common.attributeName.isEmpty()) {
144 m_attributeFormat = def.formatByName(name: ruleData.common.attributeName);
145 if (!m_attributeFormat.isValid()) {
146 qCWarning(Log) << "Rule: Unknown format" << ruleData.common.attributeName << "in context" << lookupContextName << "of definition" << def.name;
147 }
148 }
149
150 m_firstNonSpace = ruleData.common.firstNonSpace;
151 m_lookAhead = ruleData.common.lookAhead;
152 m_column = ruleData.common.column;
153
154 if (!ruleData.common.beginRegionName.isEmpty()) {
155 m_beginRegion = FoldingRegion(FoldingRegion::Begin, def.foldingRegionId(foldName: ruleData.common.beginRegionName));
156 }
157 if (!ruleData.common.endRegionName.isEmpty()) {
158 m_endRegion = FoldingRegion(FoldingRegion::End, def.foldingRegionId(foldName: ruleData.common.endRegionName));
159 }
160
161 m_context.resolve(def, contextInstr: ruleData.common.contextName);
162
163 return !(m_lookAhead && m_context.isStay());
164}
165
166static Rule::Ptr createRule(DefinitionData &def, const HighlightingContextData::Rule &ruleData, QStringView lookupContextName)
167{
168 using Type = HighlightingContextData::Rule::Type;
169
170 switch (ruleData.type) {
171 case Type::AnyChar:
172 return std::make_shared<AnyChar>(args: ruleData.data.anyChar);
173 case Type::DetectChar:
174 return std::make_shared<DetectChar>(args: ruleData.data.detectChar);
175 case Type::Detect2Chars:
176 return std::make_shared<Detect2Chars>(args: ruleData.data.detect2Chars);
177 case Type::IncludeRules:
178 return std::make_shared<IncludeRules>(args: ruleData.data.includeRules);
179 case Type::Int:
180 return std::make_shared<Int>(args&: def, args: ruleData.data.detectInt);
181 case Type::Keyword:
182 return KeywordListRule::create(def, data: ruleData.data.keyword, lookupContextName);
183 case Type::LineContinue:
184 return std::make_shared<LineContinue>(args: ruleData.data.lineContinue);
185 case Type::RangeDetect:
186 return std::make_shared<RangeDetect>(args: ruleData.data.rangeDetect);
187 case Type::RegExpr:
188 if (!ruleData.data.regExpr.dynamic) {
189 return std::make_shared<RegExpr>(args: ruleData.data.regExpr);
190 } else {
191 return std::make_shared<DynamicRegExpr>(args: ruleData.data.regExpr);
192 }
193 case Type::StringDetect:
194 if (ruleData.data.stringDetect.dynamic) {
195 return std::make_shared<DynamicStringDetect>(args: ruleData.data.stringDetect);
196 }
197 return std::make_shared<StringDetect>(args: ruleData.data.stringDetect);
198 case Type::WordDetect:
199 return std::make_shared<WordDetect>(args&: def, args: ruleData.data.wordDetect);
200 case Type::Float:
201 return std::make_shared<Float>(args&: def, args: ruleData.data.detectFloat);
202 case Type::HlCOct:
203 return std::make_shared<HlCOct>(args&: def, args: ruleData.data.hlCOct);
204 case Type::HlCStringChar:
205 return std::make_shared<HlCStringChar>();
206 case Type::DetectIdentifier:
207 return std::make_shared<DetectIdentifier>();
208 case Type::DetectSpaces:
209 return std::make_shared<DetectSpaces>();
210 case Type::HlCChar:
211 return std::make_shared<HlCChar>();
212 case Type::HlCHex:
213 return std::make_shared<HlCHex>(args&: def, args: ruleData.data.hlCHex);
214
215 case Type::Unknown:;
216 }
217
218 return Rule::Ptr(nullptr);
219}
220
221Rule::Ptr Rule::create(DefinitionData &def, const HighlightingContextData::Rule &ruleData, QStringView lookupContextName)
222{
223 auto rule = createRule(def, ruleData, lookupContextName);
224 if (rule && !rule->resolveCommon(def, ruleData, lookupContextName)) {
225 rule.reset();
226 }
227 return rule;
228}
229
230AnyChar::AnyChar(const HighlightingContextData::Rule::AnyChar &data)
231 : m_chars(data.chars)
232{
233}
234
235MatchResult AnyChar::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
236{
237 if (m_chars.contains(c: text.at(n: offset))) {
238 return offset + 1;
239 }
240 return offset;
241}
242
243DetectChar::DetectChar(const HighlightingContextData::Rule::DetectChar &data)
244 : m_char(data.char1)
245 , m_captureIndex((data.dynamic ? data.char1.digitValue() : 0) - 1)
246{
247 m_dynamic = data.dynamic;
248}
249
250MatchResult DetectChar::doMatch(QStringView text, int offset, const QStringList &captures, DynamicRegexpCache &) const
251{
252 if (m_dynamic) {
253 if (m_captureIndex == -1 || captures.size() <= m_captureIndex || captures.at(i: m_captureIndex).isEmpty()) {
254 return offset;
255 }
256 if (text.at(n: offset) == captures.at(i: m_captureIndex).at(i: 0)) {
257 return offset + 1;
258 }
259 return offset;
260 }
261
262 if (text.at(n: offset) == m_char) {
263 return offset + 1;
264 }
265 return offset;
266}
267
268Detect2Chars::Detect2Chars(const HighlightingContextData::Rule::Detect2Chars &data)
269 : m_char1(data.char1)
270 , m_char2(data.char2)
271{
272}
273
274MatchResult Detect2Chars::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
275{
276 if (text.size() - offset < 2) {
277 return offset;
278 }
279 if (text.at(n: offset) == m_char1 && text.at(n: offset + 1) == m_char2) {
280 return offset + 2;
281 }
282 return offset;
283}
284
285MatchResult DetectIdentifier::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
286{
287 if (!text.at(n: offset).isLetter() && text.at(n: offset) != QLatin1Char('_')) {
288 return offset;
289 }
290
291 for (int i = offset + 1; i < text.size(); ++i) {
292 const auto c = text.at(n: i);
293 if (!c.isLetterOrNumber() && c != QLatin1Char('_')) {
294 return i;
295 }
296 }
297
298 return text.size();
299}
300
301MatchResult DetectSpaces::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
302{
303 while (offset < text.size() && text.at(n: offset).isSpace()) {
304 ++offset;
305 }
306 return offset;
307}
308
309Float::Float(DefinitionData &def, const HighlightingContextData::Rule::Float &data)
310 : m_wordDelimiters(def.wordDelimiters)
311{
312 resolveAdditionalWordDelimiters(wordDelimiters&: m_wordDelimiters, delimiters: data.wordDelimiters);
313}
314
315MatchResult Float::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
316{
317 if (offset > 0 && !m_wordDelimiters.contains(c: text.at(n: offset - 1))) {
318 return offset;
319 }
320
321 auto newOffset = offset;
322 while (newOffset < text.size() && isDigit(c: text.at(n: newOffset))) {
323 ++newOffset;
324 }
325
326 if (newOffset >= text.size() || text.at(n: newOffset) != QLatin1Char('.')) {
327 return offset;
328 }
329 ++newOffset;
330
331 while (newOffset < text.size() && isDigit(c: text.at(n: newOffset))) {
332 ++newOffset;
333 }
334
335 if (newOffset == offset + 1) { // we only found a decimal point
336 return offset;
337 }
338
339 auto expOffset = newOffset;
340 if (expOffset >= text.size() || (text.at(n: expOffset) != QLatin1Char('e') && text.at(n: expOffset) != QLatin1Char('E'))) {
341 return newOffset;
342 }
343 ++expOffset;
344
345 if (expOffset < text.size() && (text.at(n: expOffset) == QLatin1Char('+') || text.at(n: expOffset) == QLatin1Char('-'))) {
346 ++expOffset;
347 }
348 bool foundExpDigit = false;
349 while (expOffset < text.size() && isDigit(c: text.at(n: expOffset))) {
350 ++expOffset;
351 foundExpDigit = true;
352 }
353
354 if (!foundExpDigit) {
355 return newOffset;
356 }
357 return expOffset;
358}
359
360MatchResult HlCChar::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
361{
362 if (text.size() < offset + 3) {
363 return offset;
364 }
365
366 if (text.at(n: offset) != QLatin1Char('\'') || text.at(n: offset + 1) == QLatin1Char('\'')) {
367 return offset;
368 }
369
370 auto newOffset = matchEscapedChar(text, offset: offset + 1);
371 if (newOffset == offset + 1) {
372 if (text.at(n: newOffset) == QLatin1Char('\\')) {
373 return offset;
374 } else {
375 ++newOffset;
376 }
377 }
378 if (newOffset >= text.size()) {
379 return offset;
380 }
381
382 if (text.at(n: newOffset) == QLatin1Char('\'')) {
383 return newOffset + 1;
384 }
385
386 return offset;
387}
388
389HlCHex::HlCHex(DefinitionData &def, const HighlightingContextData::Rule::HlCHex &data)
390 : m_wordDelimiters(def.wordDelimiters)
391{
392 resolveAdditionalWordDelimiters(wordDelimiters&: m_wordDelimiters, delimiters: data.wordDelimiters);
393}
394
395MatchResult HlCHex::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
396{
397 if (offset > 0 && !m_wordDelimiters.contains(c: text.at(n: offset - 1))) {
398 return offset;
399 }
400
401 if (text.size() < offset + 3) {
402 return offset;
403 }
404
405 if (text.at(n: offset) != QLatin1Char('0') || (text.at(n: offset + 1) != QLatin1Char('x') && text.at(n: offset + 1) != QLatin1Char('X'))) {
406 return offset;
407 }
408
409 if (!isHexChar(c: text.at(n: offset + 2))) {
410 return offset;
411 }
412
413 offset += 3;
414 while (offset < text.size() && isHexChar(c: text.at(n: offset))) {
415 ++offset;
416 }
417
418 // TODO Kate matches U/L suffix, QtC does not?
419
420 return offset;
421}
422
423HlCOct::HlCOct(DefinitionData &def, const HighlightingContextData::Rule::HlCOct &data)
424 : m_wordDelimiters(def.wordDelimiters)
425{
426 resolveAdditionalWordDelimiters(wordDelimiters&: m_wordDelimiters, delimiters: data.wordDelimiters);
427}
428
429MatchResult HlCOct::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
430{
431 if (offset > 0 && !m_wordDelimiters.contains(c: text.at(n: offset - 1))) {
432 return offset;
433 }
434
435 if (text.size() < offset + 2) {
436 return offset;
437 }
438
439 if (text.at(n: offset) != QLatin1Char('0')) {
440 return offset;
441 }
442
443 if (!isOctalChar(c: text.at(n: offset + 1))) {
444 return offset;
445 }
446
447 offset += 2;
448 while (offset < text.size() && isOctalChar(c: text.at(n: offset))) {
449 ++offset;
450 }
451
452 return offset;
453}
454
455MatchResult HlCStringChar::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
456{
457 return matchEscapedChar(text, offset);
458}
459
460IncludeRules::IncludeRules(const HighlightingContextData::Rule::IncludeRules &data)
461 : m_contextName(data.contextName)
462 , m_includeAttribute(data.includeAttribute)
463{
464}
465
466MatchResult IncludeRules::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
467{
468 Q_UNUSED(text);
469 qCWarning(Log) << "Unresolved include rule";
470 return offset;
471}
472
473Int::Int(DefinitionData &def, const HighlightingContextData::Rule::Int &data)
474 : m_wordDelimiters(def.wordDelimiters)
475{
476 resolveAdditionalWordDelimiters(wordDelimiters&: m_wordDelimiters, delimiters: data.wordDelimiters);
477}
478
479MatchResult Int::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
480{
481 if (offset > 0 && !m_wordDelimiters.contains(c: text.at(n: offset - 1))) {
482 return offset;
483 }
484
485 while (offset < text.size() && isDigit(c: text.at(n: offset))) {
486 ++offset;
487 }
488 return offset;
489}
490
491Rule::Ptr KeywordListRule::create(DefinitionData &def, const HighlightingContextData::Rule::Keyword &data, QStringView lookupContextName)
492{
493 /**
494 * get our keyword list, if not found => bail out
495 */
496 auto *keywordList = def.keywordList(name: data.name);
497 if (!keywordList) {
498 qCWarning(Log) << "Rule: Unknown keyword list" << data.name << "in context" << lookupContextName << "of definition" << def.name;
499 return Rule::Ptr();
500 }
501
502 if (keywordList->isEmpty()) {
503 return Rule::Ptr();
504 }
505
506 /**
507 * we might overwrite the case sensitivity
508 * then we need to init the list for lookup of that sensitivity setting
509 */
510 if (data.hasCaseSensitivityOverride) {
511 keywordList->initLookupForCaseSensitivity(caseSensitive: data.caseSensitivityOverride);
512 }
513
514 return std::make_shared<KeywordListRule>(args&: *keywordList, args&: def, args: data);
515}
516
517KeywordListRule::KeywordListRule(const KeywordList &keywordList, DefinitionData &def, const HighlightingContextData::Rule::Keyword &data)
518 : m_wordDelimiters(def.wordDelimiters)
519 , m_keywordList(keywordList)
520 , m_caseSensitivity(data.hasCaseSensitivityOverride ? data.caseSensitivityOverride : keywordList.caseSensitivity())
521{
522 resolveAdditionalWordDelimiters(wordDelimiters&: m_wordDelimiters, delimiters: data.wordDelimiters);
523 m_hasSkipOffset = true;
524}
525
526MatchResult KeywordListRule::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
527{
528 auto newOffset = offset;
529 while (text.size() > newOffset && !m_wordDelimiters.contains(c: text.at(n: newOffset))) {
530 ++newOffset;
531 }
532 if (newOffset == offset) {
533 return offset;
534 }
535
536 if (m_keywordList.contains(str: text.mid(pos: offset, n: newOffset - offset), caseSensitive: m_caseSensitivity)) {
537 return newOffset;
538 }
539
540 // we don't match, but we can skip until newOffset as we can't start a keyword in-between
541 return MatchResult(offset, newOffset);
542}
543
544LineContinue::LineContinue(const HighlightingContextData::Rule::LineContinue &data)
545 : m_char(data.char1)
546{
547}
548
549MatchResult LineContinue::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
550{
551 if (offset == text.size() - 1 && text.at(n: offset) == m_char) {
552 return offset + 1;
553 }
554 return offset;
555}
556
557RangeDetect::RangeDetect(const HighlightingContextData::Rule::RangeDetect &data)
558 : m_begin(data.begin)
559 , m_end(data.end)
560{
561}
562
563MatchResult RangeDetect::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
564{
565 if (text.size() - offset < 2) {
566 return offset;
567 }
568 if (text.at(n: offset) != m_begin) {
569 return offset;
570 }
571
572 auto newOffset = offset + 1;
573 while (newOffset < text.size()) {
574 if (text.at(n: newOffset) == m_end) {
575 return newOffset + 1;
576 }
577 ++newOffset;
578 }
579 return offset;
580}
581
582static QRegularExpression::PatternOptions makePattenOptions(const HighlightingContextData::Rule::RegExpr &data)
583{
584 return (data.isMinimal ? QRegularExpression::InvertedGreedinessOption : QRegularExpression::NoPatternOption)
585 | (data.caseSensitivity == Qt::CaseInsensitive ? QRegularExpression::CaseInsensitiveOption : QRegularExpression::NoPatternOption)
586 // DontCaptureOption is removed by resolve() when necessary
587 | QRegularExpression::DontCaptureOption
588 // ensure Unicode support is enabled
589 | QRegularExpression::UseUnicodePropertiesOption;
590}
591
592static void resolveRegex(QRegularExpression &regexp, Context *context)
593{
594 bool enableCapture = context && context->hasDynamicRule();
595
596 // disable DontCaptureOption when reference a context with dynamic rule or
597 // with invalid regex because DontCaptureOption with back reference capture is an error
598 if (enableCapture || !regexp.isValid()) {
599 regexp.setPatternOptions(regexp.patternOptions() & ~QRegularExpression::DontCaptureOption);
600 }
601
602 if (!regexp.isValid()) {
603 qCDebug(Log) << "Invalid regexp:" << regexp.pattern();
604 }
605}
606
607static MatchResult regexMatch(const QRegularExpression &regexp, QStringView text, int offset)
608{
609 /**
610 * match the pattern
611 */
612 const auto result = regexp.matchView(subjectView: text, offset, matchType: QRegularExpression::NormalMatch, matchOptions: QRegularExpression::DontCheckSubjectStringMatchOption);
613 if (result.capturedStart() == offset) {
614 /**
615 * we only need to compute the captured texts if we have real capture groups
616 * highlightings should only address %1..%.., see e.g. replaceCaptures
617 * DetectChar ignores %0, too
618 */
619 int lastCapturedIndex = result.lastCapturedIndex();
620 if (lastCapturedIndex > 0) {
621 QStringList captures;
622 captures.reserve(asize: lastCapturedIndex);
623 // ignore the capturing group number 0
624 for (int i = 1; i <= lastCapturedIndex; ++i)
625 captures.push_back(t: result.captured(nth: i));
626 return MatchResult(offset + result.capturedLength(), std::move(captures));
627 }
628
629 /**
630 * else: ignore the implicit 0 group we always capture, no need to allocate stuff for that
631 */
632 return MatchResult(offset + result.capturedLength());
633 }
634
635 /**
636 * no match
637 * we can always compute the skip offset as the highlighter will invalidate the cache for changed captures for dynamic rules!
638 */
639 return MatchResult(offset, result.capturedStart());
640}
641
642RegExpr::RegExpr(const HighlightingContextData::Rule::RegExpr &data)
643 : m_regexp(data.pattern, makePattenOptions(data))
644{
645 m_hasSkipOffset = true;
646}
647
648void RegExpr::resolve()
649{
650 m_isResolved = true;
651
652 resolveRegex(regexp&: m_regexp, context: context().context());
653}
654
655MatchResult RegExpr::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
656{
657 if (Q_UNLIKELY(!m_isResolved)) {
658 const_cast<RegExpr *>(this)->resolve();
659 }
660
661 return regexMatch(regexp: m_regexp, text, offset);
662}
663
664DynamicRegExpr::DynamicRegExpr(const HighlightingContextData::Rule::RegExpr &data)
665 : m_pattern(data.pattern)
666 , m_patternOptions(makePattenOptions(data))
667{
668 m_dynamic = true;
669 m_hasSkipOffset = true;
670}
671
672void DynamicRegExpr::resolve()
673{
674 m_isResolved = true;
675
676 QRegularExpression regexp(m_pattern, m_patternOptions);
677 resolveRegex(regexp, context: context().context());
678 m_patternOptions = regexp.patternOptions();
679}
680
681MatchResult DynamicRegExpr::doMatch(QStringView text, int offset, const QStringList &captures, DynamicRegexpCache &dynamicRegexpCache) const
682{
683 if (Q_UNLIKELY(!m_isResolved)) {
684 const_cast<DynamicRegExpr *>(this)->resolve();
685 }
686
687 /**
688 * create new pattern with right instantiation
689 */
690 auto pattern = replaceCaptures(pattern: m_pattern, captures, quote: true);
691 auto &regexp = dynamicRegexpCache.compileRegexp(pattern: std::move(pattern), patternOptions: m_patternOptions);
692 return regexMatch(regexp, text, offset);
693}
694
695StringDetect::StringDetect(const HighlightingContextData::Rule::StringDetect &data)
696 : m_string(data.string)
697 , m_caseSensitivity(data.caseSensitivity)
698{
699}
700
701MatchResult StringDetect::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
702{
703 return matchString(pattern: m_string, text, offset, caseSensitivity: m_caseSensitivity);
704}
705
706DynamicStringDetect::DynamicStringDetect(const HighlightingContextData::Rule::StringDetect &data)
707 : m_string(data.string)
708 , m_caseSensitivity(data.caseSensitivity)
709{
710 m_dynamic = true;
711}
712
713MatchResult DynamicStringDetect::doMatch(QStringView text, int offset, const QStringList &captures, DynamicRegexpCache &) const
714{
715 /**
716 * for dynamic case: create new pattern with right instantiation
717 */
718 const auto pattern = replaceCaptures(pattern: m_string, captures, quote: false);
719 return matchString(pattern, text, offset, caseSensitivity: m_caseSensitivity);
720}
721
722WordDetect::WordDetect(DefinitionData &def, const HighlightingContextData::Rule::WordDetect &data)
723 : m_wordDelimiters(def.wordDelimiters)
724 , m_word(data.word)
725 , m_caseSensitivity(data.caseSensitivity)
726{
727 resolveAdditionalWordDelimiters(wordDelimiters&: m_wordDelimiters, delimiters: data.wordDelimiters);
728}
729
730MatchResult WordDetect::doMatch(QStringView text, int offset, const QStringList &, DynamicRegexpCache &) const
731{
732 if (text.size() - offset < m_word.size()) {
733 return offset;
734 }
735
736 /**
737 * detect delimiter characters on the inner and outer boundaries of the string
738 * NOTE: m_word isn't empty
739 */
740 if (offset > 0 && !m_wordDelimiters.contains(c: text.at(n: offset - 1)) && !m_wordDelimiters.contains(c: text.at(n: offset))) {
741 return offset;
742 }
743
744 if (text.mid(pos: offset, n: m_word.size()).compare(other: m_word, cs: m_caseSensitivity) != 0) {
745 return offset;
746 }
747
748 if (text.size() == offset + m_word.size() || m_wordDelimiters.contains(c: text.at(n: offset + m_word.size()))
749 || m_wordDelimiters.contains(c: text.at(n: offset + m_word.size() - 1))) {
750 return offset + m_word.size();
751 }
752
753 return offset;
754}
755

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