/* This file is part of Konsole, a terminal emulator for KDE. SPDX-FileCopyrightText: 2018 Mariusz Glebocki SPDX-License-Identifier: GPL-2.0-or-later */ #include "template.h" #include #include static const QString unescape(const QStringRef &str) { QString result; result.reserve(str.length()); for (int i = 0; i < str.length(); ++i) { if (i < str.length() - 1 && str[i] == QLatin1Char('\\')) result += str[++i]; else result += str[i]; } return result; } // // Template::Element // const QString Template::Element::findFmt(Var::DataType type) const { const Template::Element *element; for (element = this; element != nullptr; element = element->parent) { if (!element->fmt.isEmpty() && isValidFmt(element->fmt, type)) { return element->fmt; } } return defaultFmt(type); } QString Template::Element::path() const { QStringList namesList; const Template::Element *element; for (element = this; element != nullptr; element = element->parent) { if (!element->hasName() && element->parent != nullptr) { QString anonName = QStringLiteral("[anon]"); for (int i = 0; i < element->parent->children.size(); ++i) { if (&element->parent->children[i] == element) { anonName = QStringLiteral("[%1]").arg(i); break; } } namesList.prepend(anonName); } else { namesList.prepend(element->name); } } return namesList.join(QLatin1Char('.')); } const QString Template::Element::defaultFmt(Var::DataType type) { switch (type) { case Var::DataType::Number: return QStringLiteral("%d"); case Var::DataType::String: return QStringLiteral("%s"); default: Q_UNREACHABLE(); } } bool Template::Element::isValidFmt(const QString &fmt, Var::DataType type) { switch (type) { case Var::DataType::String: return fmt.endsWith(QLatin1Char('s')); case Var::DataType::Number: return true; // regexp in parser takes care of it default: return false; } } // // Template // Template::Template(const QString &text) : _text(text) { _root.name = QStringLiteral("[root]"); _root.outer = QStringRef(&_text); _root.inner = QStringRef(&_text); _root.parent = nullptr; _root.line = 1; _root.column = 1; } void Template::parse() { _root.children.clear(); _root.outer = QStringRef(&_text); _root.inner = QStringRef(&_text); parseRecursively(_root); // dbgDumpTree(_root); } QString Template::generate(const Var &data) { QString result; result.reserve(_text.size()); generateRecursively(result, _root, data); return result; } static inline void warn(const Template::Element &element, const QString &id, const QString &msg) { const QString path = id.isEmpty() ? element.path() : Template::Element(&element, id).path(); qWarning() << QStringLiteral("Warning: %1:%2: %3: %4").arg(element.line).arg(element.column).arg(path, msg); } static inline void warn(const Template::Element &element, const QString &msg) { warn(element, QString(), msg); } void Template::executeCommand(Element &element, const Template::Element &childStub, const QStringList &argv) { // Insert content N times if (argv[0] == QStringLiteral("repeat")) { bool ok; unsigned count = argv.value(1).toInt(&ok); if (!ok || count < 1) { warn(element, QStringLiteral("!") + argv[0], QStringLiteral("invalid repeat count (%1), assuming 0.").arg(argv[1])); return; } element.children.append(childStub); Template::Element &cmdElement = element.children.last(); if (!cmdElement.inner.isEmpty()) { // Parse children parseRecursively(cmdElement); // Remember how many children was there before replication int originalChildrenCount = cmdElement.children.size(); // Replicate children for (unsigned i = 1; i < count; ++i) { for (int chId = 0; chId < originalChildrenCount; ++chId) { cmdElement.children.append(cmdElement.children[chId]); } } } // Set printf-like format (with leading %) applied for strings and numbers // inside the group } else if (argv[0] == QStringLiteral("fmt")) { static const QRegularExpression FMT_RE(QStringLiteral(R":(^%[-0 +#]?(?:[1-9][0-9]*)?\.?[0-9]*[diouxXs]$):")); const auto match = FMT_RE.match(argv.value(1)); QString fmt = QStringLiteral(""); if (!match.hasMatch()) warn(element, QStringLiteral("!") + argv[0], QStringLiteral("invalid format (%1), assuming default").arg(argv[1])); else fmt = match.captured(); element.children.append(childStub); Template::Element &cmdElement = element.children.last(); cmdElement.fmt = fmt; parseRecursively(cmdElement); } } void Template::parseRecursively(Element &element) { static const QRegularExpression RE(QStringLiteral(R":((?'comment'«\*(([^:]*):)?.*?(?(-2):\g{-1})\*»)|):" R":(«(?:(?'name'[-_a-zA-Z0-9]*)|(?:!(?'cmd'[-_a-zA-Z0-9]+(?: +(?:[^\\:]+|(?:\\.)+)+)?)))):" R":((?::(?:~[ \t]*\n)?(?'inner'(?:[^«]*?|(?R))*))?(?:\n[ \t]*~)?»):"), QRegularExpression::DotMatchesEverythingOption | QRegularExpression::MultilineOption); static const QRegularExpression CMD_SPLIT_RE(QStringLiteral(R":((?:"((?:(?:\\.)*|[^"]*)*)"|(?:[^\\ "]+|(?:\\.)+)+)):"), QRegularExpression::DotMatchesEverythingOption | QRegularExpression::MultilineOption); static const QRegularExpression UNESCAPE_RE(QStringLiteral(R":(\\(.)):"), QRegularExpression::DotMatchesEverythingOption | QRegularExpression::MultilineOption); static const QString nameGroupName = QStringLiteral("name"); static const QString innerGroupName = QStringLiteral("inner"); static const QString cmdGroupName = QStringLiteral("cmd"); static const QString commentGroupName = QStringLiteral("comment"); int posOffset = element.outer.position(); uint posLine = element.line; uint posColumn = element.column; auto matchIter = RE.globalMatch(element.inner); while (matchIter.hasNext()) { auto match = matchIter.next(); auto cmd = match.captured(cmdGroupName); auto comment = match.captured(commentGroupName); const auto localOuterRef = match.capturedRef(0); const auto localInnerRef = match.capturedRef(innerGroupName); auto outerRef = QStringRef(&_text, localOuterRef.position(), localOuterRef.length()); auto innerRef = QStringRef(&_text, localInnerRef.position(), localInnerRef.length()); while (posOffset < outerRef.position() && posOffset < _text.size()) { if (_text[posOffset++] == QLatin1Char('\n')) { ++posLine; posColumn = 1; } else { ++posColumn; } } if (!cmd.isEmpty()) { QStringList cmdArgv; auto cmdArgIter = CMD_SPLIT_RE.globalMatch(cmd); while (cmdArgIter.hasNext()) { auto cmdArg = cmdArgIter.next(); cmdArgv += cmdArg.captured(cmdArg.captured(1).isEmpty() ? 0 : 1); cmdArgv.last().replace(UNESCAPE_RE, QStringLiteral("\1")); } Template::Element childStub = Template::Element(&element); childStub.outer = outerRef; childStub.name = QLatin1Char('!') + cmd; childStub.inner = innerRef; childStub.line = posLine; childStub.column = posColumn; executeCommand(element, childStub, cmdArgv); } else if (!comment.isEmpty()) { element.children.append(Element(&element)); Template::Element &child = element.children.last(); child.outer = outerRef; child.name = QString(); child.inner = QStringRef(); child.line = posLine; child.column = posColumn; child.isComment = true; } else { element.children.append(Element(&element)); Template::Element &child = element.children.last(); child.outer = outerRef; child.name = match.captured(nameGroupName); child.inner = innerRef; child.line = posLine; child.column = posColumn; if (!child.inner.isEmpty()) parseRecursively(child); } } } int Template::generateRecursively(QString &result, const Template::Element &element, const Var &data, int consumed) { int consumedDataItems = consumed; if (!element.children.isEmpty()) { int totalDataItems; switch (data.dataType()) { case Var::DataType::Number: case Var::DataType::String: case Var::DataType::Map: totalDataItems = 1; break; case Var::DataType::Vector: totalDataItems = data.vec.size(); break; case Var::DataType::Invalid: default: Q_UNREACHABLE(); } while (consumedDataItems < totalDataItems) { int prevChildEndPosition = element.inner.position(); for (const auto &child : element.children) { const int characterCountBetweenChildren = child.outer.position() - prevChildEndPosition; if (characterCountBetweenChildren > 0) { // Add text between previous child (or inner beginning) and this child. result += unescape(_text.midRef(prevChildEndPosition, characterCountBetweenChildren)); } else if (characterCountBetweenChildren < 0) { // Repeated item; they overlap and end1 > start2 result += unescape(element.inner.mid(prevChildEndPosition - element.inner.position())); result += unescape(element.inner.left(child.outer.position() - element.inner.position())); } switch (data.dataType()) { case Var::DataType::Number: case Var::DataType::String: generateRecursively(result, child, data); consumedDataItems = 1; // Deepest child always consumes number/string break; case Var::DataType::Vector: if (!data.vec.isEmpty()) { if (!child.hasName() && !child.isCommand() && consumedDataItems < data.vec.size()) { consumedDataItems += generateRecursively(result, child, data[consumedDataItems]); } else { consumedDataItems += generateRecursively(result, child, data.vec.mid(consumedDataItems)); } } else { warn(child, QStringLiteral("no more items available in parent's list.")); } break; case Var::DataType::Map: if (!child.hasName()) { consumedDataItems = generateRecursively(result, child, data); } else if (data.map.contains(child.name)) { generateRecursively(result, child, data.map[child.name]); // Always consume, repeating doesn't change anything consumedDataItems = 1; } else { warn(child, QStringLiteral("missing value for the element in parent's map.")); } break; default: break; } prevChildEndPosition = child.outer.position() + child.outer.length(); } result += unescape(element.inner.mid(prevChildEndPosition - element.inner.position(), -1)); if (element.isCommand()) { break; } const bool isLast = consumedDataItems >= totalDataItems; if (!isLast) { // Collapse empty lines between elements int nlNum = 0; for (int i = 0; i < element.inner.size() / 2; ++i) { if (element.inner.at(i) == QLatin1Char('\n') && element.inner.at(i) == element.inner.at(element.inner.size() - i - 1)) nlNum++; else break; } if (nlNum > 0) result.chop(nlNum); } } } else if (!element.isComment) { // Handle leaf element switch (data.dataType()) { case Var::DataType::Number: { const QString fmt = element.findFmt(Var::DataType::Number); result += QString::asprintf(qUtf8Printable(fmt), data.num); break; } case Var::DataType::String: { const QString fmt = element.findFmt(Var::DataType::String); result += QString::asprintf(qUtf8Printable(fmt), qUtf8Printable(data.str)); break; } case Var::DataType::Vector: if (data.vec.isEmpty()) { warn(element, QStringLiteral("got empty list.")); } else if (data.vec.at(0).dataType() == Var::DataType::Number) { const QString fmt = element.findFmt(Var::DataType::Number); result += QString::asprintf(qUtf8Printable(fmt), data.num); } else if (data.vec.at(0).dataType() == Var::DataType::String) { const QString fmt = element.findFmt(Var::DataType::String); result += QString::asprintf(qUtf8Printable(fmt), qUtf8Printable(data.str)); } else { warn(element, QStringLiteral("the list entry data type (%1) is not supported in childrenless elements.").arg(data.vec.at(0).dataTypeAsString())); } break; case Var::DataType::Map: warn(element, QStringLiteral("map type is not supported in childrenless elements.")); break; case Var::DataType::Invalid: break; } consumedDataItems = 1; } return consumedDataItems; } /* void dbgDumpTree(const Template::Element &element) { static int indent = 0; QString type; if(element.isCommand()) type = QStringLiteral("command"); else if(element.isComment) type = QStringLiteral("comment"); else if(element.hasName() && element.inner.isEmpty()) type = QStringLiteral("empty named"); else if(element.hasName()) type = QStringLiteral("named"); else if(element.inner.isEmpty()) type = QStringLiteral("empty anonymous"); else type = QStringLiteral("anonymous"); qDebug().noquote() << QStringLiteral("%1[%2] \"%3\" %4:%5") .arg(QStringLiteral("· ").repeated(indent), type, element.name) .arg(element.line) .arg(element.column); indent++; for(const auto &child: element.children) { dbgDumpTree(child); } indent--; } */