mirror of
https://github.com/KDE/konsole.git
synced 2025-12-25 08:18:11 -05:00
403 lines
15 KiB
C++
403 lines
15 KiB
C++
/*
|
|
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 "template.h"
|
|
#include <QDebug>
|
|
#include <QRegularExpression>
|
|
|
|
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--;
|
|
}
|
|
*/
|