File indexing completed on 2024-05-05 05:54:13

0001 /*
0002     This file is part of Konsole, a terminal emulator for KDE.
0003 
0004     SPDX-FileCopyrightText: 2018 Mariusz Glebocki <mglb@arccos-1.net>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "template.h"
0010 #include <QDebug>
0011 #include <QRegularExpression>
0012 
0013 static const QString unescape(const QStringRef &str)
0014 {
0015     QString result;
0016     result.reserve(str.length());
0017     for (int i = 0; i < str.length(); ++i) {
0018         if (i < str.length() - 1 && str[i] == QLatin1Char('\\'))
0019             result += str[++i];
0020         else
0021             result += str[i];
0022     }
0023     return result;
0024 }
0025 
0026 //
0027 // Template::Element
0028 //
0029 const QString Template::Element::findFmt(Var::DataType type) const
0030 {
0031     const Template::Element *element;
0032     for (element = this; element != nullptr; element = element->parent) {
0033         if (!element->fmt.isEmpty() && isValidFmt(element->fmt, type)) {
0034             return element->fmt;
0035         }
0036     }
0037     return defaultFmt(type);
0038 }
0039 
0040 QString Template::Element::path() const
0041 {
0042     QStringList namesList;
0043     const Template::Element *element;
0044     for (element = this; element != nullptr; element = element->parent) {
0045         if (!element->hasName() && element->parent != nullptr) {
0046             QString anonName = QStringLiteral("[anon]");
0047             for (int i = 0; i < element->parent->children.size(); ++i) {
0048                 if (&element->parent->children[i] == element) {
0049                     anonName = QStringLiteral("[%1]").arg(i);
0050                     break;
0051                 }
0052             }
0053             namesList.prepend(anonName);
0054         } else {
0055             namesList.prepend(element->name);
0056         }
0057     }
0058     return namesList.join(QLatin1Char('.'));
0059 }
0060 
0061 const QString Template::Element::defaultFmt(Var::DataType type)
0062 {
0063     switch (type) {
0064     case Var::DataType::Number:
0065         return QStringLiteral("%d");
0066     case Var::DataType::String:
0067         return QStringLiteral("%s");
0068     default:
0069         Q_UNREACHABLE();
0070     }
0071 }
0072 
0073 bool Template::Element::isValidFmt(const QString &fmt, Var::DataType type)
0074 {
0075     switch (type) {
0076     case Var::DataType::String:
0077         return fmt.endsWith(QLatin1Char('s'));
0078     case Var::DataType::Number:
0079         return true; // regexp in parser takes care of it
0080     default:
0081         return false;
0082     }
0083 }
0084 
0085 //
0086 // Template
0087 //
0088 
0089 Template::Template(const QString &text)
0090     : _text(text)
0091 {
0092     _root.name = QStringLiteral("[root]");
0093     _root.outer = QStringRef(&_text);
0094     _root.inner = QStringRef(&_text);
0095     _root.parent = nullptr;
0096     _root.line = 1;
0097     _root.column = 1;
0098 }
0099 
0100 void Template::parse()
0101 {
0102     _root.children.clear();
0103     _root.outer = QStringRef(&_text);
0104     _root.inner = QStringRef(&_text);
0105     parseRecursively(_root);
0106     //    dbgDumpTree(_root);
0107 }
0108 
0109 QString Template::generate(const Var &data)
0110 {
0111     QString result;
0112     result.reserve(_text.size());
0113     generateRecursively(result, _root, data);
0114     return result;
0115 }
0116 
0117 static inline void warn(const Template::Element &element, const QString &id, const QString &msg)
0118 {
0119     const QString path = id.isEmpty() ? element.path() : Template::Element(&element, id).path();
0120     qWarning() << QStringLiteral("Warning: %1:%2: %3: %4").arg(element.line).arg(element.column).arg(path, msg);
0121 }
0122 static inline void warn(const Template::Element &element, const QString &msg)
0123 {
0124     warn(element, QString(), msg);
0125 }
0126 
0127 void Template::executeCommand(Element &element, const Template::Element &childStub, const QStringList &argv)
0128 {
0129     // Insert content N times
0130     if (argv[0] == QStringLiteral("repeat")) {
0131         bool ok;
0132         unsigned count = argv.value(1).toInt(&ok);
0133         if (!ok || count < 1) {
0134             warn(element, QStringLiteral("!") + argv[0], QStringLiteral("invalid repeat count (%1), assuming 0.").arg(argv[1]));
0135             return;
0136         }
0137 
0138         element.children.append(childStub);
0139         Template::Element &cmdElement = element.children.last();
0140         if (!cmdElement.inner.isEmpty()) {
0141             // Parse children
0142             parseRecursively(cmdElement);
0143             // Remember how many children was there before replication
0144             int originalChildrenCount = cmdElement.children.size();
0145             // Replicate children
0146             for (unsigned i = 1; i < count; ++i) {
0147                 for (int chId = 0; chId < originalChildrenCount; ++chId) {
0148                     cmdElement.children.append(cmdElement.children[chId]);
0149                 }
0150             }
0151         }
0152         // Set printf-like format (with leading %) applied for strings and numbers
0153         // inside the group
0154     } else if (argv[0] == QStringLiteral("fmt")) {
0155         static const QRegularExpression FMT_RE(QStringLiteral(R":(^%[-0 +#]?(?:[1-9][0-9]*)?\.?[0-9]*[diouxXs]$):"));
0156         const auto match = FMT_RE.match(argv.value(1));
0157         QString fmt = QStringLiteral("");
0158         if (!match.hasMatch())
0159             warn(element, QStringLiteral("!") + argv[0], QStringLiteral("invalid format (%1), assuming default").arg(argv[1]));
0160         else
0161             fmt = match.captured();
0162 
0163         element.children.append(childStub);
0164         Template::Element &cmdElement = element.children.last();
0165         cmdElement.fmt = fmt;
0166         parseRecursively(cmdElement);
0167     }
0168 }
0169 
0170 void Template::parseRecursively(Element &element)
0171 {
0172     static const QRegularExpression RE(QStringLiteral(R":((?'comment'«\*(([^:]*):)?.*?(?(-2):\g{-1})\*»)|):"
0173                                                       R":(«(?:(?'name'[-_a-zA-Z0-9]*)|(?:!(?'cmd'[-_a-zA-Z0-9]+(?: +(?:[^\\:]+|(?:\\.)+)+)?)))):"
0174                                                       R":((?::(?:~[ \t]*\n)?(?'inner'(?:[^«]*?|(?R))*))?(?:\n[ \t]*~)?»):"),
0175                                        QRegularExpression::DotMatchesEverythingOption | QRegularExpression::MultilineOption);
0176     static const QRegularExpression CMD_SPLIT_RE(QStringLiteral(R":((?:"((?:(?:\\.)*|[^"]*)*)"|(?:[^\\ "]+|(?:\\.)+)+)):"),
0177                                                  QRegularExpression::DotMatchesEverythingOption | QRegularExpression::MultilineOption);
0178     static const QRegularExpression UNESCAPE_RE(QStringLiteral(R":(\\(.)):"),
0179                                                 QRegularExpression::DotMatchesEverythingOption | QRegularExpression::MultilineOption);
0180     static const QString nameGroupName = QStringLiteral("name");
0181     static const QString innerGroupName = QStringLiteral("inner");
0182     static const QString cmdGroupName = QStringLiteral("cmd");
0183     static const QString commentGroupName = QStringLiteral("comment");
0184 
0185     int posOffset = element.outer.position();
0186     uint posLine = element.line;
0187     uint posColumn = element.column;
0188 
0189     auto matchIter = RE.globalMatch(element.inner);
0190     while (matchIter.hasNext()) {
0191         auto match = matchIter.next();
0192         auto cmd = match.captured(cmdGroupName);
0193         auto comment = match.captured(commentGroupName);
0194 
0195         const auto localOuterRef = match.capturedRef(0);
0196         const auto localInnerRef = match.capturedRef(innerGroupName);
0197 
0198         auto outerRef = QStringRef(&_text, localOuterRef.position(), localOuterRef.length());
0199         auto innerRef = QStringRef(&_text, localInnerRef.position(), localInnerRef.length());
0200 
0201         while (posOffset < outerRef.position() && posOffset < _text.size()) {
0202             if (_text[posOffset++] == QLatin1Char('\n')) {
0203                 ++posLine;
0204                 posColumn = 1;
0205             } else {
0206                 ++posColumn;
0207             }
0208         }
0209 
0210         if (!cmd.isEmpty()) {
0211             QStringList cmdArgv;
0212             auto cmdArgIter = CMD_SPLIT_RE.globalMatch(cmd);
0213             while (cmdArgIter.hasNext()) {
0214                 auto cmdArg = cmdArgIter.next();
0215                 cmdArgv += cmdArg.captured(cmdArg.captured(1).isEmpty() ? 0 : 1);
0216                 cmdArgv.last().replace(UNESCAPE_RE, QStringLiteral("\1"));
0217             }
0218 
0219             Template::Element childStub = Template::Element(&element);
0220             childStub.outer = outerRef;
0221             childStub.name = QLatin1Char('!') + cmd;
0222             childStub.inner = innerRef;
0223             childStub.line = posLine;
0224             childStub.column = posColumn;
0225             executeCommand(element, childStub, cmdArgv);
0226         } else if (!comment.isEmpty()) {
0227             element.children.append(Element(&element));
0228             Template::Element &child = element.children.last();
0229             child.outer = outerRef;
0230             child.name = QString();
0231             child.inner = QStringRef();
0232             child.line = posLine;
0233             child.column = posColumn;
0234             child.isComment = true;
0235         } else {
0236             element.children.append(Element(&element));
0237             Template::Element &child = element.children.last();
0238             child.outer = outerRef;
0239             child.name = match.captured(nameGroupName);
0240             child.inner = innerRef;
0241             child.line = posLine;
0242             child.column = posColumn;
0243             if (!child.inner.isEmpty())
0244                 parseRecursively(child);
0245         }
0246     }
0247 }
0248 
0249 int Template::generateRecursively(QString &result, const Template::Element &element, const Var &data, int consumed)
0250 {
0251     int consumedDataItems = consumed;
0252 
0253     if (!element.children.isEmpty()) {
0254         int totalDataItems;
0255         switch (data.dataType()) {
0256         case Var::DataType::Number:
0257         case Var::DataType::String:
0258         case Var::DataType::Map:
0259             totalDataItems = 1;
0260             break;
0261         case Var::DataType::Vector:
0262             totalDataItems = data.vec.size();
0263             break;
0264         case Var::DataType::Invalid:
0265         default:
0266             Q_UNREACHABLE();
0267         }
0268 
0269         while (consumedDataItems < totalDataItems) {
0270             int prevChildEndPosition = element.inner.position();
0271             for (const auto &child : element.children) {
0272                 const int characterCountBetweenChildren = child.outer.position() - prevChildEndPosition;
0273                 if (characterCountBetweenChildren > 0) {
0274                     // Add text between previous child (or inner beginning) and this child.
0275                     result += unescape(_text.midRef(prevChildEndPosition, characterCountBetweenChildren));
0276                 } else if (characterCountBetweenChildren < 0) {
0277                     // Repeated item; they overlap and end1 > start2
0278                     result += unescape(element.inner.mid(prevChildEndPosition - element.inner.position()));
0279                     result += unescape(element.inner.left(child.outer.position() - element.inner.position()));
0280                 }
0281 
0282                 switch (data.dataType()) {
0283                 case Var::DataType::Number:
0284                 case Var::DataType::String:
0285                     generateRecursively(result, child, data);
0286                     consumedDataItems = 1; // Deepest child always consumes number/string
0287                     break;
0288                 case Var::DataType::Vector:
0289                     if (!data.vec.isEmpty()) {
0290                         if (!child.hasName() && !child.isCommand() && consumedDataItems < data.vec.size()) {
0291                             consumedDataItems += generateRecursively(result, child, data[consumedDataItems]);
0292                         } else {
0293                             consumedDataItems += generateRecursively(result, child, data.vec.mid(consumedDataItems));
0294                         }
0295                     } else {
0296                         warn(child, QStringLiteral("no more items available in parent's list."));
0297                     }
0298                     break;
0299                 case Var::DataType::Map:
0300                     if (!child.hasName()) {
0301                         consumedDataItems = generateRecursively(result, child, data);
0302                     } else if (data.map.contains(child.name)) {
0303                         generateRecursively(result, child, data.map[child.name]);
0304                         // Always consume, repeating doesn't change anything
0305                         consumedDataItems = 1;
0306                     } else {
0307                         warn(child, QStringLiteral("missing value for the element in parent's map."));
0308                     }
0309                     break;
0310                 default:
0311                     break;
0312                 }
0313                 prevChildEndPosition = child.outer.position() + child.outer.length();
0314             }
0315 
0316             result += unescape(element.inner.mid(prevChildEndPosition - element.inner.position(), -1));
0317 
0318             if (element.isCommand()) {
0319                 break;
0320             }
0321 
0322             const bool isLast = consumedDataItems >= totalDataItems;
0323             if (!isLast) {
0324                 // Collapse empty lines between elements
0325                 int nlNum = 0;
0326                 for (int i = 0; i < element.inner.size() / 2; ++i) {
0327                     if (element.inner.at(i) == QLatin1Char('\n') && element.inner.at(i) == element.inner.at(element.inner.size() - i - 1))
0328                         nlNum++;
0329                     else
0330                         break;
0331                 }
0332                 if (nlNum > 0)
0333                     result.chop(nlNum);
0334             }
0335         }
0336     } else if (!element.isComment) {
0337         // Handle leaf element
0338         switch (data.dataType()) {
0339         case Var::DataType::Number: {
0340             const QString fmt = element.findFmt(Var::DataType::Number);
0341             result += QString::asprintf(qUtf8Printable(fmt), data.num);
0342             break;
0343         }
0344         case Var::DataType::String: {
0345             const QString fmt = element.findFmt(Var::DataType::String);
0346             result += QString::asprintf(qUtf8Printable(fmt), qUtf8Printable(data.str));
0347             break;
0348         }
0349         case Var::DataType::Vector:
0350             if (data.vec.isEmpty()) {
0351                 warn(element, QStringLiteral("got empty list."));
0352             } else if (data.vec.at(0).dataType() == Var::DataType::Number) {
0353                 const QString fmt = element.findFmt(Var::DataType::Number);
0354                 result += QString::asprintf(qUtf8Printable(fmt), data.num);
0355             } else if (data.vec.at(0).dataType() == Var::DataType::String) {
0356                 const QString fmt = element.findFmt(Var::DataType::String);
0357                 result += QString::asprintf(qUtf8Printable(fmt), qUtf8Printable(data.str));
0358             } else {
0359                 warn(element,
0360                      QStringLiteral("the list entry data type (%1) is not supported in childrenless elements.").arg(data.vec.at(0).dataTypeAsString()));
0361             }
0362             break;
0363         case Var::DataType::Map:
0364             warn(element, QStringLiteral("map type is not supported in childrenless elements."));
0365             break;
0366         case Var::DataType::Invalid:
0367             break;
0368         }
0369         consumedDataItems = 1;
0370     }
0371 
0372     return consumedDataItems;
0373 }
0374 
0375 /*
0376 void dbgDumpTree(const Template::Element &element) {
0377     static int indent = 0;
0378     QString type;
0379     if(element.isCommand())
0380         type = QStringLiteral("command");
0381     else if(element.isComment)
0382         type = QStringLiteral("comment");
0383     else if(element.hasName() && element.inner.isEmpty())
0384         type = QStringLiteral("empty named");
0385     else if(element.hasName())
0386         type = QStringLiteral("named");
0387     else if(element.inner.isEmpty())
0388         type = QStringLiteral("empty anonymous");
0389     else
0390         type = QStringLiteral("anonymous");
0391 
0392     qDebug().noquote() << QStringLiteral("%1[%2] \"%3\" %4:%5")
0393                           .arg(QStringLiteral("·   ").repeated(indent), type, element.name)
0394                           .arg(element.line)
0395                           .arg(element.column);
0396     indent++;
0397     for(const auto &child: element.children) {
0398         dbgDumpTree(child);
0399     }
0400     indent--;
0401 }
0402 */