// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only // Qt-Security score:critical reason:data-parser #include "syntaxhighlighter.h" #include "syntaxhighlighterdata.h" #include #ifdef QQEM_SYNTAX_HIGHLIGHTING_ENABLED #include #include #endif // Text color and style categories enum TextStyle : quint8 { C_VISUAL_WHITESPACE, C_PREPROCESSOR, C_NUMBER, C_COMMENT, C_KEYWORD, C_PARAMETER, C_REMOVED_LINE, C_TAG }; static constexpr TextStyle GLSLReservedKeyword = C_REMOVED_LINE; static QTextCharFormat formatForCategory(TextStyle category) { switch (category) { case C_PARAMETER: { QTextCharFormat fmt; fmt.setForeground(QColor::fromRgb(0xaa, 0xaa, 0xff)); return fmt; } case C_PREPROCESSOR: { QTextCharFormat fmt; fmt.setForeground(QColor::fromRgb(0x55, 0x55, 0xff)); return fmt; } case C_NUMBER: { QTextCharFormat fmt; fmt.setForeground(QColor::fromRgb(0xff, 0x55, 0xff)); return fmt; } case C_COMMENT: { QTextCharFormat fmt; fmt.setForeground(QColor::fromRgb(0x80, 0x80, 0x80)); return fmt; } case C_KEYWORD: { QTextCharFormat fmt; fmt.setForeground(QColor::fromRgb(0xff, 0xff, 0x55)); return fmt; } case C_TAG: { QTextCharFormat fmt; fmt.setForeground(QColor::fromRgb(0xaa, 0xff, 0xaa)); return fmt; } case C_REMOVED_LINE: Q_FALLTHROUGH(); case C_VISUAL_WHITESPACE: break; } return QTextCharFormat(); } SyntaxHighlighter::SyntaxHighlighter(QObject *p) : QSyntaxHighlighter(p) { } #ifdef QQEM_SYNTAX_HIGHLIGHTING_ENABLED void SyntaxHighlighter::highlightBlock(const QString &text) { if (m_tagKeywords.isEmpty()) { const auto tagKeywords = SyntaxHighlighterData::reservedTagNames(); for (const auto &kw : tagKeywords) m_tagKeywords.insert(kw); } if (m_argumentKeywords.isEmpty()) { const auto args = SyntaxHighlighterData::reservedArgumentNames(); for (const auto &arg : args) m_argumentKeywords.insert(arg); } const int previousState = previousBlockState(); int state = 0; if (previousState != -1) state = previousState & 0xff; const QByteArray data = text.toLatin1(); GLSL::Lexer lex(/*engine=*/ nullptr, data.constData(), data.size()); lex.setState(state); lex.setScanKeywords(false); lex.setScanComments(true); lex.setVariant(GLSL::Lexer::Variant_GLSL_400); QList tokens; GLSL::Token tk; do { lex.yylex(&tk); tokens.append(tk); } while (tk.isNot(GLSL::Parser::EOF_SYMBOL)); state = lex.state(); // refresh the state if (tokens.isEmpty()) { setCurrentBlockState(previousState); if (!text.isEmpty()) // the empty line can still contain whitespace setFormat(0, text.length(), formatForCategory(C_VISUAL_WHITESPACE)); return; } for (int i = 0, end = tokens.size(); i != end; ++i) { const GLSL::Token &tk = tokens.at(i); int previousTokenEnd = 0; if (i != 0) { // mark the whitespaces previousTokenEnd = tokens.at(i - 1).begin() + tokens.at(i - 1).length; } if (previousTokenEnd != tk.begin()) setFormat(previousTokenEnd, tk.begin() - previousTokenEnd, formatForCategory(C_VISUAL_WHITESPACE)); if (tk.is(GLSL::Parser::T_NUMBER)) { setFormat(tk.begin(), tk.length, formatForCategory(C_NUMBER)); } else if (tk.is(GLSL::Parser::T_IDENTIFIER)) { int kind = lex.findKeyword(data.constData() + tk.position, tk.length); if (kind == GLSL::Parser::T_IDENTIFIER) { if (m_argumentKeywords.contains(QByteArrayView(data.constData() + tk.position, tk.length))) setFormat(tk.position, tk.length, formatForCategory(C_PARAMETER)); } if (kind == GLSL::Parser::T_RESERVED) { setFormat(tk.position, tk.length, formatForCategory(GLSLReservedKeyword)); } else if (kind != GLSL::Parser::T_IDENTIFIER) { setFormat(tk.position, tk.length, formatForCategory(C_KEYWORD)); } else { const auto tagStartPos = tk.position - 1; bool isTag = data.at(tagStartPos) == '@'; if (isTag) { const auto tagLength = tk.length + 1; auto line = QByteArrayView(data.constData() + tagStartPos, tagLength).trimmed(); auto firstSpace = line.indexOf(' '); QByteArrayView firstWord; if (firstSpace > 0) firstWord = line.sliced(0, firstSpace); if (m_tagKeywords.contains(line) || (!firstWord.isEmpty() && m_tagKeywords.contains(firstWord))) setFormat(tagStartPos, tagLength, formatForCategory(C_TAG)); } } } else if (tk.is(GLSL::Parser::T_COMMENT)) { highlightLine(text, tk.begin(), tk.length, formatForCategory(C_COMMENT)); } } // mark the trailing white spaces { const GLSL::Token tk = tokens.last(); const int lastTokenEnd = tk.begin() + tk.length; if (text.length() > lastTokenEnd) highlightLine(text, lastTokenEnd, text.length() - lastTokenEnd, QTextCharFormat()); } // Multi-line comments static QRegularExpression startExpression("/\\*"); static QRegularExpression endExpression("\\*/"); setCurrentBlockState(None); int startIndex = 0; if (previousBlockState() != Comment) startIndex = text.indexOf(startExpression); // Check multi-line comment blocks while (startIndex >= 0) { QRegularExpressionMatch endMatch; int endIndex = text.indexOf(endExpression, startIndex, &endMatch); int commentLength; if (endIndex == -1) { setCurrentBlockState(Comment); commentLength = text.length() - startIndex; } else { commentLength = endIndex - startIndex + endMatch.capturedLength(); } setFormat(startIndex, commentLength, formatForCategory(C_COMMENT)); startIndex = text.indexOf(startExpression, startIndex + commentLength); } } #else // Dummy version without glslparser dependency void SyntaxHighlighter::highlightBlock(const QString &text) { Q_UNUSED(text) } #endif void SyntaxHighlighter::highlightLine(const QString &text, int position, int length, const QTextCharFormat &format) { const QTextCharFormat visualSpaceFormat = formatForCategory(C_VISUAL_WHITESPACE); const int end = position + length; int index = position; while (index != end) { const bool isSpace = text.at(index).isSpace(); const int start = index; do { ++index; } while (index != end && text.at(index).isSpace() == isSpace); const int tokenLength = index - start; if (isSpace) setFormat(start, tokenLength, visualSpaceFormat); else if (format.isValid()) setFormat(start, tokenLength, format); } } QQuickTextDocument *SyntaxHighlighter::document() const { return m_quickTextDocument; } void SyntaxHighlighter::setDocument(QQuickTextDocument *newDocument) { if (m_quickTextDocument == newDocument) return; m_quickTextDocument = newDocument; QSyntaxHighlighter::setDocument(m_quickTextDocument != nullptr ? m_quickTextDocument->textDocument() : nullptr); emit documentChanged(); }