QmlDesigner: Refactor escape and deescape functions

- Replaced QString::replace with manual text handling to avoid multiple
loops.
- Changed parameter type from QString to QStringView for better
performance.
- Improved handling of special characters and reserved memory for
efficiency.

Change-Id: I984df7b555f6e0fc115575e4e7e0606b55703520
Reviewed-by: Burak Hancerli <burak.hancerli@qt.io>
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Marco Bubke
2025-04-28 23:44:32 +02:00
parent 3025fedd03
commit 5a2261d0bd
2 changed files with 222 additions and 20 deletions

View File

@@ -11,40 +11,106 @@
namespace QmlDesigner::StringUtils { namespace QmlDesigner::StringUtils {
inline QString escape(const QString &value) inline QString escape(QStringView text)
{ {
using namespace Qt::StringLiterals; using namespace Qt::StringLiterals;
if (value.length() == 6 && value.startsWith("\\u")) //Do not double escape unicode chars if (text.size() == 6 && text.startsWith(u"\\u")) //Do not double escape unicode chars
return value; return text.toString();
QString result = value; QString escapedText;
escapedText.reserve(text.size() * 2);
result.replace("\\"_L1, "\\\\"_L1); const auto end = text.end();
result.replace("\""_L1, "\\\""_L1); auto current = text.begin();
result.replace("\t"_L1, "\\t"_L1); QStringView pattern = u"\\\"\t\r\n";
result.replace("\r"_L1, "\\r"_L1); while (current != end) {
result.replace("\n"_L1, "\\n"_L1); auto found = std::ranges::find_first_of(current, end, pattern.begin(), pattern.end());
escapedText.append(QStringView{current, found});
return result; 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;
} }
inline QString deescape(const QString &value) current = std::next(found);
}
return escapedText;
}
inline QString deescape(QStringView text)
{ {
using namespace Qt::StringLiterals; using namespace Qt::StringLiterals;
if (value.length() == 6 && value.startsWith("\\u")) //Ignore unicode chars if (text.isEmpty() || (text.size() == 6 && text.startsWith(u"\\u"))) //Ignore unicode chars
return value; return text.toString();
QString result = value; QString deescapedText;
deescapedText.reserve(text.size());
result.replace("\\\\"_L1, "\\"_L1); const auto end = text.end();
result.replace("\\\""_L1, "\""_L1); auto current = text.begin();
result.replace("\\t"_L1, "\t"_L1); while (current != end) {
result.replace("\\r"_L1, "\r"_L1); auto found = std::ranges::find(current, end, u'\\');
result.replace("\\n"_L1, "\n"_L1); deescapedText.append(QStringView{current, found});
return result; 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;
} }
template<typename T> template<typename T>

View File

@@ -6,6 +6,8 @@
namespace { namespace {
using namespace Qt::StringLiterals;
using QmlDesigner::StringUtils::split_last; using QmlDesigner::StringUtils::split_last;
TEST(StringUtils_split_last, leaf_is_empty_for_empty_input) TEST(StringUtils_split_last, leaf_is_empty_for_empty_input)
@@ -79,4 +81,138 @@ TEST(StringUtils_split_last, no_steam_for_not_dot)
ASSERT_THAT(steam, IsEmpty()); ASSERT_THAT(steam, IsEmpty());
} }
using ConvertFunction = QString (*)(QStringView);
struct EscapeParameters
{
QString input;
QString output;
std::string name;
ConvertFunction convert;
};
class escaping : public testing::TestWithParam<EscapeParameters>
{
public:
escaping()
: inputTerm{GetParam().input}
, outputTerm{GetParam().output}
, name{GetParam().name}
, convert{GetParam().convert}
{}
public:
const QString &inputTerm;
const QString &outputTerm;
const std::string &name;
ConvertFunction convert;
};
auto excape_printer = [](const testing::TestParamInfo<EscapeParameters> &info) {
return info.param.name;
};
INSTANTIATE_TEST_SUITE_P(
StringUtils,
escaping,
testing::Values(
EscapeParameters(u"\\"_s, u"\\\\"_s, "escape_backslash", QmlDesigner::StringUtils::escape),
EscapeParameters(u"\""_s, u"\\\""_s, "escape_quote", QmlDesigner::StringUtils::escape),
EscapeParameters(u"\t"_s, u"\\t"_s, "escape_tab", QmlDesigner::StringUtils::escape),
EscapeParameters(u"\n"_s, u"\\n"_s, "escape_new_line", QmlDesigner::StringUtils::escape),
EscapeParameters(u"\r"_s, u"\\r"_s, "escape_carriage_return", QmlDesigner::StringUtils::escape),
EscapeParameters(u"\\"_s, u"\\"_s, "deescape_backslash", QmlDesigner::StringUtils::deescape),
EscapeParameters(u"\\\\"_s, u"\\"_s, "deescape_double_backslash", QmlDesigner::StringUtils::deescape),
EscapeParameters(u"\\\""_s, u"\""_s, "deescape_quote", QmlDesigner::StringUtils::deescape),
EscapeParameters(u"\\t"_s, u"\t"_s, "deescape_tab", QmlDesigner::StringUtils::deescape),
EscapeParameters(u"\\n"_s, u"\n"_s, "deescape_new_line", QmlDesigner::StringUtils::deescape),
EscapeParameters(
u"\\r"_s, u"\r"_s, "deescape_carriage_return", QmlDesigner::StringUtils::deescape)),
excape_printer);
TEST_P(escaping, begin)
{
QString input = inputTerm + "foo";
auto converted = convert(input);
ASSERT_THAT(converted, outputTerm + "foo");
}
TEST_P(escaping, empty)
{
QString input;
auto converted = convert(input);
ASSERT_THAT(converted, IsEmpty());
}
TEST_P(escaping, only)
{
QString input = inputTerm;
auto converted = convert(input);
ASSERT_THAT(converted, outputTerm);
}
TEST_P(escaping, nothing)
{
QString input = "foobar";
auto converted = convert(input);
ASSERT_THAT(converted, "foobar");
}
TEST_P(escaping, end)
{
QString input = "foo" + inputTerm;
auto converted = convert(input);
ASSERT_THAT(converted, "foo" + outputTerm);
}
TEST_P(escaping, middle)
{
QString input = "foo" + inputTerm + "bar";
auto converted = convert(input);
ASSERT_THAT(converted, "foo" + outputTerm + "bar");
}
TEST_P(escaping, multiple)
{
QString input = "foo" + inputTerm + "bar" + inputTerm + "foo";
auto converted = convert(input);
ASSERT_THAT(converted, "foo" + outputTerm + "bar" + outputTerm + "foo");
}
TEST_P(escaping, multiple_in_row)
{
if (name == "deescape_backslash") // would be double backslash
return;
QString input = "foo" + inputTerm + inputTerm + "foo";
auto converted = convert(input);
ASSERT_THAT(converted, "foo" + outputTerm + outputTerm + "foo");
}
TEST_P(escaping, skip_unicode)
{
QString input = "\\u" + inputTerm + "foo";
input.resize(6);
auto converted = convert(input);
ASSERT_THAT(converted, input);
}
} // namespace } // namespace