aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/reference/modules/texttemplate-module.qdoc118
-rw-r--r--share/qbs/modules/texttemplate/texttemplate.qbs64
-rw-r--r--tests/auto/blackbox/blackbox-android.pro3
-rw-r--r--tests/auto/blackbox/testdata/choose-module-instance/gerbil.txt.in4
-rw-r--r--tests/auto/blackbox/testdata/choose-module-instance/modules/texttemplate/texttemplate.qbs49
-rw-r--r--tests/auto/blackbox/testdata/erroneous/texttemplate-unknown-placeholder/boom.txt.in1
-rw-r--r--tests/auto/blackbox/testdata/erroneous/texttemplate-unknown-placeholder/texttemplate-unknown-placeholder.qbs8
-rw-r--r--tests/auto/blackbox/testdata/texttemplate/cdefgabc.txt.in1
-rw-r--r--tests/auto/blackbox/testdata/texttemplate/expected/lalala.txt1
-rw-r--r--tests/auto/blackbox/testdata/texttemplate/expected/output.txt12
-rw-r--r--tests/auto/blackbox/testdata/texttemplate/output.txt.in12
-rw-r--r--tests/auto/blackbox/testdata/texttemplate/texttemplatetest.qbs26
-rw-r--r--tests/auto/blackbox/tst_blackbox.cpp15
-rw-r--r--tests/auto/blackbox/tst_blackbox.h1
-rw-r--r--tests/auto/shared.h75
15 files changed, 339 insertions, 51 deletions
diff --git a/doc/reference/modules/texttemplate-module.qdoc b/doc/reference/modules/texttemplate-module.qdoc
new file mode 100644
index 000000000..6d42560be
--- /dev/null
+++ b/doc/reference/modules/texttemplate-module.qdoc
@@ -0,0 +1,118 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** $QT_BEGIN_LICENSE:FDL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Free Documentation License Usage
+** Alternatively, this file may be used under the terms of the GNU Free
+** Documentation License version 1.3 as published by the Free Software
+** Foundation and appearing in the file included in the packaging of
+** this file. Please review the following information to ensure
+** the GNU Free Documentation License version 1.3 requirements
+** will be met: https://www.gnu.org/licenses/fdl-1.3.html.
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+/*!
+ \contentspage index.html
+ \qmltype texttemplate
+ \inqmlmodule QbsModules
+ \since Qbs 1.13
+
+ \brief Provides support for text template files
+
+ The \c texttemplate module provides support for text template files.
+
+ \section2 Example
+
+ Consider the following text file \e{greeting.txt.in}.
+
+ \code
+ ${greeting} ${name}!
+ \endcode
+
+ This can be used in a project like this:
+
+ \code
+ Product {
+ type: ["text"]
+ files: ["greeting.txt.in"]
+ Depends { name: "texttemplate" }
+ texttemplate.dict: ({
+ greeting: "Hello",
+ name: "World"
+ })
+ }
+ \endcode
+
+ Which will create the file \e{greeting.txt}.
+
+ \code
+ Hello World!
+ \endcode
+
+
+ \section2 Placeholder Syntax
+
+ A placeholder \c{${foo}} is replaced by its corresponding value in
+ \e{texttemplate.dict}.
+ Placeholder names consist of alphanumeric characters only.
+
+ The placeholder \c{${$}} is always replaced with \c{$}.
+ If you need a literal \c{${foo}} in your template, use \c{${$}{foo}}.
+
+ Placeholders that are not defined in the dictionary will produce an error.
+
+
+ \section2 Relevant File Tags
+ \target filetags-texttemplate
+
+ \table
+ \header
+ \li Tag
+ \li Auto-tagged File Names
+ \li Since
+ \li Description
+ \row
+ \li \c{"texttemplate.input"}
+ \li \c{*.in}
+ \li 1.13.0
+ \li Source files with this tag serve as inputs for the text template rule.
+ \endtable
+*/
+
+/*!
+ \qmlproperty var texttemplate::dict
+
+ The dictionary containing values for all keys used in the template file.
+
+ \defaultvalue \c{{}}
+*/
+
+/*!
+ \qmlproperty string outputFileName
+
+ The output file name that is assigned to produced artifacts.
+
+ \defaultvalue Complete base name of the input file
+*/
+
+/*!
+ \qmlproperty string outputTag
+
+ The output tag that is assigned to produced artifacts.
+
+ \defaultvalue \c{"text"}
+*/
diff --git a/share/qbs/modules/texttemplate/texttemplate.qbs b/share/qbs/modules/texttemplate/texttemplate.qbs
new file mode 100644
index 000000000..c72929660
--- /dev/null
+++ b/share/qbs/modules/texttemplate/texttemplate.qbs
@@ -0,0 +1,64 @@
+import qbs.TextFile
+
+Module {
+ property var dict: ({})
+ property string outputTag: "text"
+ property string outputFileName
+ FileTagger {
+ patterns: ["*.in"]
+ fileTags: ["texttemplate.input"]
+ }
+ Rule {
+ inputs: ["texttemplate.input"]
+ outputFileTags: [product.texttemplate.outputTag]
+ outputArtifacts: [
+ {
+ fileTags: [product.texttemplate.outputTag],
+ filePath: input.texttemplate.outputFileName || input.completeBaseName
+ }
+ ]
+ prepare: {
+ var cmd = new JavaScriptCommand();
+ cmd.silent = true;
+ cmd.sourceCode = function() {
+ try {
+ var src = new TextFile(input.filePath, TextFile.ReadOnly);
+ var dst = new TextFile(output.filePath, TextFile.WriteOnly);
+ var rex = /\${(\$|\w+)}/g;
+ var match;
+ while (!src.atEof()) {
+ rex.lastIndex = 0;
+ var line = src.readLine();
+ var matches = [];
+ while (match = rex.exec(line))
+ matches.push(match);
+ for (var i = matches.length; --i >= 0;) {
+ match = matches[i];
+ var replacement;
+ if (match[1] === "$") {
+ replacement = "$";
+ } else {
+ replacement = input.texttemplate.dict[match[1]];
+ if (typeof replacement === "undefined") {
+ throw new Error("Placeholder '" + match[1]
+ + "' is not defined in textemplate.dict for '"
+ + input.fileName + "'.");
+ }
+ }
+ line = line.substr(0, match.index)
+ + replacement
+ + line.substr(match.index + match[0].length);
+ }
+ dst.writeLine(line);
+ }
+ } finally {
+ if (src)
+ src.close();
+ if (dst)
+ dst.close();
+ }
+ };
+ return [cmd];
+ }
+ }
+}
diff --git a/tests/auto/blackbox/blackbox-android.pro b/tests/auto/blackbox/blackbox-android.pro
index d27550301..7aca99e8d 100644
--- a/tests/auto/blackbox/blackbox-android.pro
+++ b/tests/auto/blackbox/blackbox-android.pro
@@ -16,3 +16,6 @@ for(data_dir, DATA_DIRS) {
}
OTHER_FILES += $$FILES
+
+DISTFILES += \
+ testdata/texttemplate/expected-output-one.txt
diff --git a/tests/auto/blackbox/testdata/choose-module-instance/gerbil.txt.in b/tests/auto/blackbox/testdata/choose-module-instance/gerbil.txt.in
index 53b91dbcd..4722829a3 100644
--- a/tests/auto/blackbox/testdata/choose-module-instance/gerbil.txt.in
+++ b/tests/auto/blackbox/testdata/choose-module-instance/gerbil.txt.in
@@ -1,5 +1,5 @@
I once had a gerbil named Bobby,
Who had an unusual hobby.
-He $DID on a $THING,
-and now -- oh my $IDOL,
+He ${DID} on a ${THING},
+and now -- oh my ${IDOL},
now all that's left is a blobby.
diff --git a/tests/auto/blackbox/testdata/choose-module-instance/modules/texttemplate/texttemplate.qbs b/tests/auto/blackbox/testdata/choose-module-instance/modules/texttemplate/texttemplate.qbs
deleted file mode 100644
index aca755373..000000000
--- a/tests/auto/blackbox/testdata/choose-module-instance/modules/texttemplate/texttemplate.qbs
+++ /dev/null
@@ -1,49 +0,0 @@
-import qbs.TextFile
-
-Module {
- property var dict: ({})
- FileTagger {
- patterns: ["*.in"]
- fileTags: ["texttemplate.input"]
- }
- Rule {
- inputs: ["texttemplate.input"]
- Artifact {
- fileTags: ["text"]
- filePath: input.completeBaseName
- }
- prepare: {
- var cmd = new JavaScriptCommand();
- cmd.silent = true;
- cmd.sourceCode = function() {
- try {
- var src = new TextFile(input.filePath, TextFile.ReadOnly);
- var dst = new TextFile(output.filePath, TextFile.WriteOnly);
- var rex = /\$([A-Z]+)/g;
- while (!src.atEof()) {
- rex.lastIndex = 0;
- var line = src.readLine();
- while (true) {
- var result = rex.exec(line);
- if (!result)
- break;
- var replacement = input.texttemplate.dict[result[1]];
- if (replacement) {
- line = line.substr(0, result.index)
- + replacement
- + line.substr(result.index + result[0].length);
- }
- }
- dst.writeLine(line);
- }
- } finally {
- if (src)
- src.close();
- if (dst)
- dst.close();
- }
- };
- return [cmd];
- }
- }
-}
diff --git a/tests/auto/blackbox/testdata/erroneous/texttemplate-unknown-placeholder/boom.txt.in b/tests/auto/blackbox/testdata/erroneous/texttemplate-unknown-placeholder/boom.txt.in
new file mode 100644
index 000000000..359e856ec
--- /dev/null
+++ b/tests/auto/blackbox/testdata/erroneous/texttemplate-unknown-placeholder/boom.txt.in
@@ -0,0 +1 @@
+Boom! shake-shake-shake the ${what}!
diff --git a/tests/auto/blackbox/testdata/erroneous/texttemplate-unknown-placeholder/texttemplate-unknown-placeholder.qbs b/tests/auto/blackbox/testdata/erroneous/texttemplate-unknown-placeholder/texttemplate-unknown-placeholder.qbs
new file mode 100644
index 000000000..3ad31a609
--- /dev/null
+++ b/tests/auto/blackbox/testdata/erroneous/texttemplate-unknown-placeholder/texttemplate-unknown-placeholder.qbs
@@ -0,0 +1,8 @@
+import qbs
+
+Product {
+ type: ["text"]
+ Depends { name: "texttemplate" }
+ texttemplate.dict: ({ wat: "room" }) // typo in key name
+ files: [ "boom.txt.in" ]
+}
diff --git a/tests/auto/blackbox/testdata/texttemplate/cdefgabc.txt.in b/tests/auto/blackbox/testdata/texttemplate/cdefgabc.txt.in
new file mode 100644
index 000000000..9e3a753bc
--- /dev/null
+++ b/tests/auto/blackbox/testdata/texttemplate/cdefgabc.txt.in
@@ -0,0 +1 @@
+${c} ${d} ${e} ${f} ${g} ${a} ${b} ${c}
diff --git a/tests/auto/blackbox/testdata/texttemplate/expected/lalala.txt b/tests/auto/blackbox/testdata/texttemplate/expected/lalala.txt
new file mode 100644
index 000000000..c47434717
--- /dev/null
+++ b/tests/auto/blackbox/testdata/texttemplate/expected/lalala.txt
@@ -0,0 +1 @@
+do re mi fa so la ti do
diff --git a/tests/auto/blackbox/testdata/texttemplate/expected/output.txt b/tests/auto/blackbox/testdata/texttemplate/expected/output.txt
new file mode 100644
index 000000000..5c4b1a82a
--- /dev/null
+++ b/tests/auto/blackbox/testdata/texttemplate/expected/output.txt
@@ -0,0 +1,12 @@
+foo bar baz
+fu bar baz
+foo BAR baz
+foo bar buzz
+fu BAR baz
+fu bar buzz
+fu BAR buzz
+fooBARbaz
+foo\BARbaz
+foo\\BARbaz
+foo\\\BARbaz
+foo${bar}baz
diff --git a/tests/auto/blackbox/testdata/texttemplate/output.txt.in b/tests/auto/blackbox/testdata/texttemplate/output.txt.in
new file mode 100644
index 000000000..f5f645b73
--- /dev/null
+++ b/tests/auto/blackbox/testdata/texttemplate/output.txt.in
@@ -0,0 +1,12 @@
+foo bar baz
+${foo} bar baz
+foo ${bar} baz
+foo bar ${baz}
+${foo} ${bar} baz
+${foo} bar ${baz}
+${foo} ${bar} ${baz}
+foo${bar}baz
+foo\${bar}baz
+foo\\${bar}baz
+foo\\\${bar}baz
+foo${$}{bar}baz
diff --git a/tests/auto/blackbox/testdata/texttemplate/texttemplatetest.qbs b/tests/auto/blackbox/testdata/texttemplate/texttemplatetest.qbs
new file mode 100644
index 000000000..8b312e7c6
--- /dev/null
+++ b/tests/auto/blackbox/testdata/texttemplate/texttemplatetest.qbs
@@ -0,0 +1,26 @@
+import qbs
+
+Product {
+ name: "one"
+ type: ["text"]
+ files: ["output.txt.in"]
+ Depends { name: "texttemplate" }
+ texttemplate.dict: ({
+ foo: "fu",
+ bar: "BAR",
+ baz: "buzz",
+ })
+ Group {
+ files: ["cdefgabc.txt.in"]
+ texttemplate.outputFileName: "lalala.txt"
+ texttemplate.dict: ({
+ c: "do",
+ d: "re",
+ e: "mi",
+ f: "fa",
+ g: "so",
+ a: "la",
+ b: "ti",
+ })
+ }
+}
diff --git a/tests/auto/blackbox/tst_blackbox.cpp b/tests/auto/blackbox/tst_blackbox.cpp
index 3c03c6b3e..1929296f4 100644
--- a/tests/auto/blackbox/tst_blackbox.cpp
+++ b/tests/auto/blackbox/tst_blackbox.cpp
@@ -258,6 +258,19 @@ void TestBlackbox::tar()
QCOMPARE(listContents.readAllStandardOutput(), listFile.readAll());
}
+void TestBlackbox::textTemplate()
+{
+ QVERIFY(QDir::setCurrent(testDataDir + "/texttemplate"));
+ rmDirR(relativeBuildDir());
+ QCOMPARE(runQbs(), 0);
+ QString outputFilePath = relativeProductBuildDir("one") + "/output.txt";
+ QString expectedOutputFilePath = QFINDTESTDATA("expected/output.txt");
+ TEXT_FILE_COMPARE(outputFilePath, expectedOutputFilePath);
+ outputFilePath = relativeProductBuildDir("one") + "/lalala.txt";
+ expectedOutputFilePath = QFINDTESTDATA("expected/lalala.txt");
+ TEXT_FILE_COMPARE(outputFilePath, expectedOutputFilePath);
+}
+
static QStringList sortedFileList(const QByteArray &ba)
{
auto list = QString::fromUtf8(ba).split(QRegExp("[\r\n]"), QString::SkipEmptyParts);
@@ -3113,6 +3126,8 @@ void TestBlackbox::erroneousFiles_data()
<< "Error in Rule\\.outputArtifacts\\[0\\]\n\r?"
"Property fileTags for artifact 'outputArtifacts-missing-fileTags\\.txt' "
"must be a non-empty string list\\.";
+ QTest::newRow("texttemplate-unknown-placeholder")
+ << "Placeholder 'what' is not defined in textemplate.dict for 'boom.txt.in'";
}
void TestBlackbox::erroneousFiles()
diff --git a/tests/auto/blackbox/tst_blackbox.h b/tests/auto/blackbox/tst_blackbox.h
index af95e12a6..3784a03b1 100644
--- a/tests/auto/blackbox/tst_blackbox.h
+++ b/tests/auto/blackbox/tst_blackbox.h
@@ -249,6 +249,7 @@ private slots:
void systemRunPaths();
void systemRunPaths_data();
void tar();
+ void textTemplate();
void toolLookup();
void topLevelSearchPath();
void trackAddFile();
diff --git a/tests/auto/shared.h b/tests/auto/shared.h
index b7a3e2b11..e049511ab 100644
--- a/tests/auto/shared.h
+++ b/tests/auto/shared.h
@@ -32,8 +32,10 @@
#include <tools/profile.h>
#include <tools/settings.h>
+#include <QtCore/qbytearray.h>
#include <QtCore/qcryptographichash.h>
#include <QtCore/qdatetime.h>
+#include <QtCore/qdebug.h>
#include <QtCore/qdir.h>
#include <QtCore/qfile.h>
#include <QtCore/qfileinfo.h>
@@ -103,6 +105,79 @@ inline bool directoryExists(const QString &dirPath)
return fi.exists() && fi.isDir();
}
+struct ReadFileContentResult
+{
+ QByteArray content;
+ QString errorString;
+};
+
+inline ReadFileContentResult readFileContent(const QString &filePath)
+{
+ ReadFileContentResult result;
+ QFile file(filePath);
+ if (!file.open(QIODevice::ReadOnly)) {
+ result.errorString = file.errorString();
+ return result;
+ }
+ result.content = file.readAll();
+ return result;
+}
+
+inline QByteArray diffText(const QByteArray &actual, const QByteArray &expected)
+{
+ QByteArray result;
+ QList<QByteArray> actualLines = actual.split('\n');
+ QList<QByteArray> expectedLines = expected.split('\n');
+ int n = 1;
+ while (!actualLines.isEmpty() && !expectedLines.isEmpty()) {
+ QByteArray actualLine = actualLines.takeFirst();
+ QByteArray expectedLine = expectedLines.takeFirst();
+ if (actualLine != expectedLine) {
+ result += QStringLiteral("%1: actual: %2\n%1:expected: %3\n")
+ .arg(n, 2)
+ .arg(QString::fromUtf8(actualLine))
+ .arg(QString::fromUtf8(expectedLine))
+ .toUtf8();
+ }
+ n++;
+ }
+ auto addLines = [&result, &n] (const QList<QByteArray> &lines) {
+ for (const QByteArray &line : qAsConst(lines)) {
+ result += QStringLiteral("%1: %2\n").arg(n).arg(QString::fromUtf8(line));
+ n++;
+ }
+ };
+ if (!actualLines.isEmpty()) {
+ result += "Extra unexpected lines:\n";
+ addLines(actualLines);
+ }
+ if (!expectedLines.isEmpty()) {
+ result += "Missing expected lines:\n";
+ addLines(expectedLines);
+ }
+ return result;
+}
+
+#define READ_TEXT_FILE(filePath, contentVariable) \
+ QByteArray contentVariable; \
+ { \
+ auto c = readFileContent(filePath); \
+ QVERIFY2(c.errorString.isEmpty(), \
+ qUtf8Printable(QStringLiteral("Cannot open file %1. %2") \
+ .arg(filePath, c.errorString))); \
+ contentVariable = std::move(c.content); \
+ }
+
+#define TEXT_FILE_COMPARE(actualFilePath, expectedFilePath) \
+ { \
+ READ_TEXT_FILE(actualFilePath, ba1); \
+ READ_TEXT_FILE(expectedFilePath, ba2); \
+ if (ba1 != ba2) { \
+ QByteArray msg = "File contents differ:\n" + diffText(ba1, ba2); \
+ QFAIL(msg.constData()); \
+ } \
+ }
+
template <typename T>
inline QString prefixedIfNonEmpty(const T &prefix, const QString &str)
{