diff --git a/src/plugins/qmldesigner/libs/qmldesignerutils/CMakeLists.txt b/src/plugins/qmldesigner/libs/qmldesignerutils/CMakeLists.txt index 5820b8825a1..042061c8697 100644 --- a/src/plugins/qmldesigner/libs/qmldesignerutils/CMakeLists.txt +++ b/src/plugins/qmldesigner/libs/qmldesignerutils/CMakeLists.txt @@ -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 diff --git a/src/plugins/qmldesigner/libs/qmldesignerutils/stringutils.cpp b/src/plugins/qmldesigner/libs/qmldesignerutils/stringutils.cpp new file mode 100644 index 00000000000..616ffee636e --- /dev/null +++ b/src/plugins/qmldesigner/libs/qmldesignerutils/stringutils.cpp @@ -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 diff --git a/src/plugins/qmldesigner/libs/qmldesignerutils/stringutils.h b/src/plugins/qmldesigner/libs/qmldesignerutils/stringutils.h index 4b6c92c9b40..2014cfb8207 100644 --- a/src/plugins/qmldesigner/libs/qmldesignerutils/stringutils.h +++ b/src/plugins/qmldesigner/libs/qmldesignerutils/stringutils.h @@ -3,6 +3,8 @@ #pragma once +#include "qmldesignerutils_global.h" + #include #include @@ -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 concept is_object = std::is_object_v; @@ -128,4 +32,6 @@ inline std::pair 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 diff --git a/tests/unit/tests/unittests/qmldesignerutils/stringutils-test.cpp b/tests/unit/tests/unittests/qmldesignerutils/stringutils-test.cpp index 3ad74764f86..e6df3b2a63e 100644 --- a/tests/unit/tests/unittests/qmldesignerutils/stringutils-test.cpp +++ b/tests/unit/tests/unittests/qmldesignerutils/stringutils-test.cpp @@ -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