You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
384 lines
16 KiB
384 lines
16 KiB
/* |
|
This file is part of Konsole, a terminal emulator for KDE. |
|
|
|
SPDX-FileCopyrightText: 2018 Mariusz Glebocki <mglb@arccos-1.net> |
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
|
|
#include <QDebug> |
|
#include <QRegularExpression> |
|
#include <QRegularExpressionMatch> |
|
#include "template.h" |
|
|
|
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--; |
|
} |
|
*/
|
|
|