From 1e5db01c23b7ae4e966faff870567618ad1fa5b5 Mon Sep 17 00:00:00 2001 From: Joerg Bornemann Date: Mon, 28 May 2018 09:35:54 +0200 Subject: Make the texttemplate module public Also, adjust the syntax to be closer to ES2015's template literals. [ChangeLog] Introduced the texttemplate module, a facility similar to qmake's SUBSTITUTES feature. Task-number: QBS-1050 Change-Id: Id4d45ac962d68f44a060aefafb20263d7f21ba9f Reviewed-by: Christian Kandeler --- doc/reference/modules/texttemplate-module.qdoc | 118 +++++++++++++++++++++ share/qbs/modules/texttemplate/texttemplate.qbs | 64 +++++++++++ tests/auto/blackbox/blackbox-android.pro | 3 + .../testdata/choose-module-instance/gerbil.txt.in | 4 +- .../modules/texttemplate/texttemplate.qbs | 49 --------- .../texttemplate-unknown-placeholder/boom.txt.in | 1 + .../texttemplate-unknown-placeholder.qbs | 8 ++ .../blackbox/testdata/texttemplate/cdefgabc.txt.in | 1 + .../testdata/texttemplate/expected/lalala.txt | 1 + .../testdata/texttemplate/expected/output.txt | 12 +++ .../blackbox/testdata/texttemplate/output.txt.in | 12 +++ .../testdata/texttemplate/texttemplatetest.qbs | 26 +++++ tests/auto/blackbox/tst_blackbox.cpp | 15 +++ tests/auto/blackbox/tst_blackbox.h | 1 + tests/auto/shared.h | 75 +++++++++++++ 15 files changed, 339 insertions(+), 51 deletions(-) create mode 100644 doc/reference/modules/texttemplate-module.qdoc create mode 100644 share/qbs/modules/texttemplate/texttemplate.qbs delete mode 100644 tests/auto/blackbox/testdata/choose-module-instance/modules/texttemplate/texttemplate.qbs create mode 100644 tests/auto/blackbox/testdata/erroneous/texttemplate-unknown-placeholder/boom.txt.in create mode 100644 tests/auto/blackbox/testdata/erroneous/texttemplate-unknown-placeholder/texttemplate-unknown-placeholder.qbs create mode 100644 tests/auto/blackbox/testdata/texttemplate/cdefgabc.txt.in create mode 100644 tests/auto/blackbox/testdata/texttemplate/expected/lalala.txt create mode 100644 tests/auto/blackbox/testdata/texttemplate/expected/output.txt create mode 100644 tests/auto/blackbox/testdata/texttemplate/output.txt.in create mode 100644 tests/auto/blackbox/testdata/texttemplate/texttemplatetest.qbs 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 #include +#include #include #include +#include #include #include #include @@ -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 actualLines = actual.split('\n'); + QList 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 &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 inline QString prefixedIfNonEmpty(const T &prefix, const QString &str) { -- cgit v1.2.3