QmlDesigner: Add string utils function

Add utilities for string matching and comment detection in QStringView

- Implemented `find_comment_end()` to locate the end of a comment block
or line.
- Move complex functions to .cpp file

Change-Id: Id544905d9486b6c4e7cddc91213956830937d8ae
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Marco Bubke
2025-05-21 19:54:01 +02:00
parent 3b85cd1344
commit 44936a8968
4 changed files with 221 additions and 101 deletions

View File

@@ -17,7 +17,7 @@ add_qtc_library(QmlDesignerUtils STATIC
qmldesignerutils_global.h
version.cpp version.h
maputils.h
stringutils.h
stringutils.cpp stringutils.h
)
extend_qtc_library(QmlDesignerUtils

View File

@@ -0,0 +1,154 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "stringutils.h"
namespace QmlDesigner::StringUtils {
QString escape(QStringView text)
{
using namespace Qt::StringLiterals;
if (text.size() == 6 && text.startsWith(u"\\u")) //Do not double escape unicode chars
return text.toString();
QString escapedText;
escapedText.reserve(text.size() * 2);
const auto end = text.end();
auto current = text.begin();
QStringView pattern = u"\\\"\t\r\n";
while (current != end) {
auto found = std::ranges::find_first_of(current, end, pattern.begin(), pattern.end());
escapedText.append(QStringView{current, found});
if (found == end)
break;
QChar c = *found;
switch (c.unicode()) {
case u'\\':
escapedText.append(u"\\\\");
break;
case u'\"':
escapedText.append(u"\\\"");
break;
case u'\t':
escapedText.append(u"\\t");
break;
case u'\r':
escapedText.append(u"\\r");
break;
case u'\n':
escapedText.append(u"\\n");
break;
}
current = std::next(found);
}
return escapedText;
}
QString deescape(QStringView text)
{
using namespace Qt::StringLiterals;
if (text.isEmpty() || (text.size() == 6 && text.startsWith(u"\\u"))) //Ignore unicode chars
return text.toString();
QString deescapedText;
deescapedText.reserve(text.size());
const auto end = text.end();
auto current = text.begin();
while (current != end) {
auto found = std::ranges::find(current, end, u'\\');
deescapedText.append(QStringView{current, found});
if (found == end)
break;
current = std::next(found);
if (current == end) {
deescapedText.append(u'\\');
break;
}
QChar c = *current;
switch (c.unicode()) {
case u'\\':
deescapedText.append(u'\\');
current = std::next(current);
break;
case u'\"':
deescapedText.append(u'\"');
current = std::next(current);
break;
case u't':
deescapedText.append(u'\t');
current = std::next(current);
break;
case u'r':
deescapedText.append(u'\r');
current = std::next(current);
break;
case u'n':
deescapedText.append(u'\n');
current = std::next(current);
break;
default:
deescapedText.append(u'\\');
break;
}
}
return deescapedText;
}
namespace {
enum class Comment { No, Line, Block };
Comment commentKind(QStringView text)
{
if (text.size() < 2)
return Comment::No;
if (text.startsWith(u"//"))
return Comment::Line;
if (text.startsWith(u"/*"))
return Comment::Block;
return Comment::No;
}
} // namespace
QStringView::iterator find_comment_end(QStringView text)
{
auto current = text.begin();
const auto end = text.end();
while (current != end) {
text = {current, text.end()};
switch (commentKind(text)) {
case Comment::No:
return current;
case Comment::Line:
current = std::ranges::find(text, u'\n');
break;
case Comment::Block:
current = std::ranges::search(text, QStringView{u"*/"}).end();
current = std::ranges::find(current, end, u'\n');
break;
}
if (current != end)
current = std::next(current);
}
return current;
}
} // namespace QmlDesigner::StringUtils

View File

@@ -3,6 +3,8 @@
#pragma once
#include "qmldesignerutils_global.h"
#include <QString>
#include <QStringView>
@@ -11,107 +13,9 @@
namespace QmlDesigner::StringUtils {
inline QString escape(QStringView text)
{
using namespace Qt::StringLiterals;
QMLDESIGNERUTILS_EXPORT QString escape(QStringView text);
if (text.size() == 6 && text.startsWith(u"\\u")) //Do not double escape unicode chars
return text.toString();
QString escapedText;
escapedText.reserve(text.size() * 2);
const auto end = text.end();
auto current = text.begin();
QStringView pattern = u"\\\"\t\r\n";
while (current != end) {
auto found = std::ranges::find_first_of(current, end, pattern.begin(), pattern.end());
escapedText.append(QStringView{current, found});
if (found == end)
break;
QChar c = *found;
switch (c.unicode()) {
case u'\\':
escapedText.append(u"\\\\");
break;
case u'\"':
escapedText.append(u"\\\"");
break;
case u'\t':
escapedText.append(u"\\t");
break;
case u'\r':
escapedText.append(u"\\r");
break;
case u'\n':
escapedText.append(u"\\n");
break;
}
current = std::next(found);
}
return escapedText;
}
inline QString deescape(QStringView text)
{
using namespace Qt::StringLiterals;
if (text.isEmpty() || (text.size() == 6 && text.startsWith(u"\\u"))) //Ignore unicode chars
return text.toString();
QString deescapedText;
deescapedText.reserve(text.size());
const auto end = text.end();
auto current = text.begin();
while (current != end) {
auto found = std::ranges::find(current, end, u'\\');
deescapedText.append(QStringView{current, found});
if (found == end)
break;
current = std::next(found);
if (current == end) {
deescapedText.append(u'\\');
break;
}
QChar c = *current;
switch (c.unicode()) {
case u'\\':
deescapedText.append(u'\\');
current = std::next(current);
break;
case u'\"':
deescapedText.append(u'\"');
current = std::next(current);
break;
case u't':
deescapedText.append(u'\t');
current = std::next(current);
break;
case u'r':
deescapedText.append(u'\r');
current = std::next(current);
break;
case u'n':
deescapedText.append(u'\n');
current = std::next(current);
break;
default:
deescapedText.append(u'\\');
break;
}
}
return deescapedText;
}
QMLDESIGNERUTILS_EXPORT QString deescape(QStringView text);
template<typename T>
concept is_object = std::is_object_v<T>;
@@ -128,4 +32,6 @@ inline std::pair<QStringView, QStringView> split_last(QStringView text, QChar c)
return {{text.begin(), std::prev(splitPoint)}, {splitPoint, text.end()}};
}
QMLDESIGNERUTILS_EXPORT QStringView::iterator find_comment_end(QStringView text);
} // namespace QmlDesigner::StringUtils

View File

@@ -8,6 +8,7 @@ namespace {
using namespace Qt::StringLiterals;
using QmlDesigner::StringUtils::find_comment_end;
using QmlDesigner::StringUtils::split_last;
TEST(StringUtils_split_last, leaf_is_empty_for_empty_input)
@@ -222,4 +223,63 @@ TEST_P(escaping, skip_unicode)
ASSERT_THAT(converted, input);
}
TEST(StringUtils, find_comment_end_empty)
{
QStringView input{};
auto found = find_comment_end(input);
ASSERT_THAT(found, input.end());
}
TEST(StringUtils, find_comment_end_line_comment)
{
QStringView input{u"//foo\n//bar\nbar"};
auto found = find_comment_end(input);
QStringView text{found, input.end()};
ASSERT_THAT(text, u"bar");
}
TEST(StringUtils, find_comment_end_block_comment)
{
QStringView input{u"/*foo\n//bar*/foo\nbar"};
auto found = find_comment_end(input);
QStringView text{found, input.end()};
ASSERT_THAT(text, u"bar");
}
TEST(StringUtils, find_comment_end_block_and_line_comment)
{
QStringView input{u"//foo\n/*bar*/foo\nbar"};
auto found = find_comment_end(input);
QStringView text{found, input.end()};
ASSERT_THAT(text, u"bar");
}
TEST(StringUtils, find_comment_end_line_comment_has_no_newline)
{
QStringView input{u"//foobar"};
auto found = find_comment_end(input);
QStringView text{found, input.end()};
ASSERT_THAT(text, input.end());
}
TEST(StringUtils, find_comment_end_block_comment_has_no_newline)
{
QStringView input{u"//foo\n/*bar*/foo"};
auto found = find_comment_end(input);
QStringView text{found, input.end()};
ASSERT_THAT(text, input.end());
}
} // namespace