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 */