1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
5#include "syntaxhighlighter.h"
6#include "syntaxhighlighterdata.h"
7
8#include <QtCore/qregularexpression.h>
9
10#ifdef QQEM_SYNTAX_HIGHLIGHTING_ENABLED
11#include <QtQuick3DGlslParser/private/glsllexer_p.h>
12#include <QtQuick3DGlslParser/private/glslparser_p.h>
13#endif
14
15// Text color and style categories
16enum TextStyle : quint8 {
17 C_VISUAL_WHITESPACE,
18 C_PREPROCESSOR,
19 C_NUMBER,
20 C_COMMENT,
21 C_KEYWORD,
22 C_PARAMETER,
23 C_REMOVED_LINE,
24 C_TAG
25};
26
27static constexpr TextStyle GLSLReservedKeyword = C_REMOVED_LINE;
28
29static QTextCharFormat formatForCategory(TextStyle category)
30{
31 switch (category) {
32 case C_PARAMETER:
33 {
34 QTextCharFormat fmt;
35 fmt.setForeground(QColor::fromRgb(r: 0xaa, g: 0xaa, b: 0xff));
36 return fmt;
37 }
38 case C_PREPROCESSOR:
39 {
40 QTextCharFormat fmt;
41 fmt.setForeground(QColor::fromRgb(r: 0x55, g: 0x55, b: 0xff));
42 return fmt;
43 }
44 case C_NUMBER:
45 {
46 QTextCharFormat fmt;
47 fmt.setForeground(QColor::fromRgb(r: 0xff, g: 0x55, b: 0xff));
48 return fmt;
49 }
50 case C_COMMENT:
51 {
52 QTextCharFormat fmt;
53 fmt.setForeground(QColor::fromRgb(r: 0x80, g: 0x80, b: 0x80));
54 return fmt;
55 }
56 case C_KEYWORD:
57 {
58 QTextCharFormat fmt;
59 fmt.setForeground(QColor::fromRgb(r: 0xff, g: 0xff, b: 0x55));
60 return fmt;
61 }
62 case C_TAG:
63 {
64 QTextCharFormat fmt;
65 fmt.setForeground(QColor::fromRgb(r: 0xaa, g: 0xff, b: 0xaa));
66 return fmt;
67 }
68 case C_REMOVED_LINE:
69 Q_FALLTHROUGH();
70 case C_VISUAL_WHITESPACE:
71 break;
72 }
73
74 return QTextCharFormat();
75}
76
77SyntaxHighlighter::SyntaxHighlighter(QObject *p)
78 : QSyntaxHighlighter(p)
79{
80
81}
82
83#ifdef QQEM_SYNTAX_HIGHLIGHTING_ENABLED
84void SyntaxHighlighter::highlightBlock(const QString &text)
85{
86 if (m_tagKeywords.isEmpty()) {
87 const auto tagKeywords = SyntaxHighlighterData::reservedTagNames();
88 for (const auto &kw : tagKeywords)
89 m_tagKeywords.insert(value: kw);
90 }
91 if (m_argumentKeywords.isEmpty()) {
92 const auto args = SyntaxHighlighterData::reservedArgumentNames();
93 for (const auto &arg : args)
94 m_argumentKeywords.insert(value: arg);
95 }
96 const int previousState = previousBlockState();
97 int state = 0;
98 if (previousState != -1)
99 state = previousState & 0xff;
100
101 const QByteArray data = text.toLatin1();
102 GLSL::Lexer lex(/*engine=*/ nullptr, data.constData(), data.size());
103 lex.setState(state);
104 lex.setScanKeywords(false);
105 lex.setScanComments(true);
106
107 lex.setVariant(GLSL::Lexer::Variant_GLSL_400);
108
109 QList<GLSL::Token> tokens;
110 GLSL::Token tk;
111 do {
112 lex.yylex(tk: &tk);
113 tokens.append(t: tk);
114 } while (tk.isNot(k: GLSL::Parser::EOF_SYMBOL));
115
116 state = lex.state(); // refresh the state
117
118 if (tokens.isEmpty()) {
119 setCurrentBlockState(previousState);
120 if (!text.isEmpty()) // the empty line can still contain whitespace
121 setFormat(start: 0, count: text.length(), format: formatForCategory(category: C_VISUAL_WHITESPACE));
122 return;
123 }
124
125 for (int i = 0, end = tokens.size(); i != end; ++i) {
126 const GLSL::Token &tk = tokens.at(i);
127
128 int previousTokenEnd = 0;
129 if (i != 0) {
130 // mark the whitespaces
131 previousTokenEnd = tokens.at(i: i - 1).begin() + tokens.at(i: i - 1).length;
132 }
133
134 if (previousTokenEnd != tk.begin())
135 setFormat(start: previousTokenEnd, count: tk.begin() - previousTokenEnd, format: formatForCategory(category: C_VISUAL_WHITESPACE));
136
137 if (tk.is(k: GLSL::Parser::T_NUMBER)) {
138 setFormat(start: tk.begin(), count: tk.length, format: formatForCategory(category: C_NUMBER));
139
140 } else if (tk.is(k: GLSL::Parser::T_IDENTIFIER)) {
141 int kind = lex.findKeyword(word: data.constData() + tk.position, length: tk.length);
142 if (kind == GLSL::Parser::T_IDENTIFIER) {
143 if (m_argumentKeywords.contains(value: QByteArrayView(data.constData() + tk.position, tk.length)))
144 setFormat(start: tk.position, count: tk.length, format: formatForCategory(category: C_PARAMETER));
145 }
146 if (kind == GLSL::Parser::T_RESERVED) {
147 setFormat(start: tk.position, count: tk.length, format: formatForCategory(category: GLSLReservedKeyword));
148 } else if (kind != GLSL::Parser::T_IDENTIFIER) {
149 setFormat(start: tk.position, count: tk.length, format: formatForCategory(category: C_KEYWORD));
150 } else {
151 const auto tagStartPos = tk.position - 1;
152 bool isTag = data.at(i: tagStartPos) == '@';
153 if (isTag) {
154 const auto tagLength = tk.length + 1;
155 auto line = QByteArrayView(data.constData() + tagStartPos, tagLength).trimmed();
156 auto firstSpace = line.indexOf(ch: ' ');
157 QByteArrayView firstWord;
158 if (firstSpace > 0)
159 firstWord = line.sliced(pos: 0, n: firstSpace);
160 if (m_tagKeywords.contains(value: line) || (!firstWord.isEmpty() && m_tagKeywords.contains(value: firstWord)))
161 setFormat(start: tagStartPos, count: tagLength, format: formatForCategory(category: C_TAG));
162 }
163 }
164 } else if (tk.is(k: GLSL::Parser::T_COMMENT)) {
165 highlightLine(text, position: tk.begin(), length: tk.length, format: formatForCategory(category: C_COMMENT));
166 }
167 }
168
169 // mark the trailing white spaces
170 {
171 const GLSL::Token tk = tokens.last();
172 const int lastTokenEnd = tk.begin() + tk.length;
173 if (text.length() > lastTokenEnd)
174 highlightLine(text, position: lastTokenEnd, length: text.length() - lastTokenEnd, format: QTextCharFormat());
175 }
176
177 // Multi-line comments
178 static QRegularExpression startExpression("/\\*");
179 static QRegularExpression endExpression("\\*/");
180
181 setCurrentBlockState(None);
182
183 int startIndex = 0;
184 if (previousBlockState() != Comment)
185 startIndex = text.indexOf(re: startExpression);
186
187 // Check multi-line comment blocks
188 while (startIndex >= 0) {
189 QRegularExpressionMatch endMatch;
190 int endIndex = text.indexOf(re: endExpression, from: startIndex, rmatch: &endMatch);
191 int commentLength;
192 if (endIndex == -1) {
193 setCurrentBlockState(Comment);
194 commentLength = text.length() - startIndex;
195 } else {
196 commentLength = endIndex - startIndex
197 + endMatch.capturedLength();
198 }
199 setFormat(start: startIndex, count: commentLength, format: formatForCategory(category: C_COMMENT));
200 startIndex = text.indexOf(re: startExpression,
201 from: startIndex + commentLength);
202 }
203}
204
205#else
206
207// Dummy version without glslparser dependency
208void SyntaxHighlighter::highlightBlock(const QString &text)
209{
210 Q_UNUSED(text)
211}
212
213#endif
214
215void SyntaxHighlighter::highlightLine(const QString &text, int position, int length,
216 const QTextCharFormat &format)
217{
218 const QTextCharFormat visualSpaceFormat = formatForCategory(category: C_VISUAL_WHITESPACE);
219
220 const int end = position + length;
221 int index = position;
222
223 while (index != end) {
224 const bool isSpace = text.at(i: index).isSpace();
225 const int start = index;
226
227 do {
228 ++index;
229 } while (index != end && text.at(i: index).isSpace() == isSpace);
230
231 const int tokenLength = index - start;
232 if (isSpace)
233 setFormat(start, count: tokenLength, format: visualSpaceFormat);
234 else if (format.isValid())
235 setFormat(start, count: tokenLength, format);
236 }
237}
238
239QQuickTextDocument *SyntaxHighlighter::document() const
240{
241 return m_quickTextDocument;
242}
243
244void SyntaxHighlighter::setDocument(QQuickTextDocument *newDocument)
245{
246 if (m_quickTextDocument == newDocument)
247 return;
248
249 m_quickTextDocument = newDocument;
250 QSyntaxHighlighter::setDocument(m_quickTextDocument != nullptr ? m_quickTextDocument->textDocument() : nullptr);
251
252 emit documentChanged();
253}
254

source code of qtquickeffectmaker/tools/qqem/syntaxhighlighter.cpp