diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index c03721d097..9c03bdd3bb 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -49,10 +50,12 @@ static const int INTERFACE_RUNNING_CHECK_FREQUENCY_MS = 1000; const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server"; -static const QStringList BAKEABLE_MODEL_EXTENSIONS = { "fbx" }; +static const QStringList BAKEABLE_MODEL_EXTENSIONS = {"fbx"}; static QStringList BAKEABLE_TEXTURE_EXTENSIONS; +static const QStringList BAKEABLE_SCRIPT_EXTENSIONS = {"js"}; static const QString BAKED_MODEL_SIMPLE_NAME = "asset.fbx"; static const QString BAKED_TEXTURE_SIMPLE_NAME = "texture.ktx"; +static const QString BAKED_SCRIPT_SIMPLE_NAME = "asset.js"; void AssetServer::bakeAsset(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) { qDebug() << "Starting bake for: " << assetPath << assetHash; @@ -99,6 +102,8 @@ std::pair AssetServer::getAssetStatus(const AssetPath& pa bakedFilename = BAKED_MODEL_SIMPLE_NAME; } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit()) && hasMetaFile(hash)) { bakedFilename = BAKED_TEXTURE_SIMPLE_NAME; + } else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) { + bakedFilename = BAKED_SCRIPT_SIMPLE_NAME; } else { return { Irrelevant, "" }; } @@ -186,6 +191,8 @@ bool AssetServer::needsToBeBaked(const AssetPath& path, const AssetHash& assetHa bakedFilename = BAKED_MODEL_SIMPLE_NAME; } else if (loaded && BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit())) { bakedFilename = BAKED_TEXTURE_SIMPLE_NAME; + } else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) { + bakedFilename = BAKED_SCRIPT_SIMPLE_NAME; } else { return false; } @@ -488,6 +495,8 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode bakedRootFile = BAKED_MODEL_SIMPLE_NAME; } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(assetPathExtension.toLocal8Bit())) { bakedRootFile = BAKED_TEXTURE_SIMPLE_NAME; + } else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(assetPathExtension)) { + bakedRootFile = BAKED_SCRIPT_SIMPLE_NAME; } auto originalAssetHash = it->second; @@ -1141,6 +1150,7 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) { static const QString BAKED_ASSET_SIMPLE_FBX_NAME = "asset.fbx"; static const QString BAKED_ASSET_SIMPLE_TEXTURE_NAME = "texture.ktx"; +static const QString BAKED_ASSET_SIMPLE_JS_NAME = "asset.js"; QString getBakeMapping(const AssetHash& hash, const QString& relativeFilePath) { return HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + relativeFilePath; @@ -1204,14 +1214,14 @@ void AssetServer::handleCompletedBake(QString originalAssetHash, QString origina // setup the mapping for this bake file auto relativeFilePath = QUrl(filePath).fileName(); qDebug() << "Relative file path is: " << relativeFilePath; - if (relativeFilePath.endsWith(".fbx", Qt::CaseInsensitive)) { // for an FBX file, we replace the filename with the simple name // (to handle the case where two mapped assets have the same hash but different names) relativeFilePath = BAKED_ASSET_SIMPLE_FBX_NAME; + } else if (relativeFilePath.endsWith(".js", Qt::CaseInsensitive)) { + relativeFilePath = BAKED_ASSET_SIMPLE_JS_NAME; } else if (!originalAssetPath.endsWith(".fbx", Qt::CaseInsensitive)) { relativeFilePath = BAKED_ASSET_SIMPLE_TEXTURE_NAME; - } QString bakeMapping = getBakeMapping(originalAssetHash, relativeFilePath); @@ -1364,6 +1374,8 @@ bool AssetServer::setBakingEnabled(const AssetPathList& paths, bool enabled) { bakedFilename = BAKED_MODEL_SIMPLE_NAME; } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit()) && hasMetaFile(hash)) { bakedFilename = BAKED_TEXTURE_SIMPLE_NAME; + } else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) { + bakedFilename = BAKED_SCRIPT_SIMPLE_NAME; } else { continue; } diff --git a/assignment-client/src/assets/BakeAssetTask.cpp b/assignment-client/src/assets/BakeAssetTask.cpp index 94a0739612..6c78d2baf3 100644 --- a/assignment-client/src/assets/BakeAssetTask.cpp +++ b/assignment-client/src/assets/BakeAssetTask.cpp @@ -15,6 +15,7 @@ #include #include +#include BakeAssetTask::BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) : _assetHash(assetHash), @@ -52,6 +53,10 @@ void BakeAssetTask::run() { _baker = std::unique_ptr { new FBXBaker(QUrl("file:///" + _filePath), fn, tempOutputDir) }; + } else if (_assetPath.endsWith(".js", Qt::CaseInsensitive)) { + _baker = std::unique_ptr{ + new JSBaker(QUrl("file:///" + _filePath), PathUtils::generateTemporaryDir()) + }; } else { tempOutputDir = PathUtils::generateTemporaryDir(); _baker = std::unique_ptr { diff --git a/libraries/baking/src/JSBaker.cpp b/libraries/baking/src/JSBaker.cpp new file mode 100644 index 0000000000..75811bea49 --- /dev/null +++ b/libraries/baking/src/JSBaker.cpp @@ -0,0 +1,247 @@ +// +// JSBaker.cpp +// libraries/baking/src +// +// Created by Utkarsh Gautam on 9/18/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "JSBaker.h" +#include "Baker.h" + +const int ASCII_CHARACTERS_UPPER_LIMIT = 126; + +JSBaker::JSBaker(const QUrl& jsURL, const QString& bakedOutputDir) : + _jsURL(jsURL), + _bakedOutputDir(bakedOutputDir) +{ + +} + +void JSBaker::bake() { + qCDebug(js_baking) << "JS Baker " << _jsURL << "bake starting"; + + // Import file to start baking + QFile jsFile(_jsURL.toLocalFile()); + if (!jsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + handleError("Error opening " + _jsURL.fileName() + " for reading"); + return; + } + + // Read file into an array + QByteArray inputJS = jsFile.readAll(); + QByteArray outputJS; + + // Call baking on inputJS and store result in outputJS + bool success = bakeJS(inputJS, outputJS); + if (!success) { + qCDebug(js_baking) << "Bake Failed"; + handleError("Unterminated multi-line comment"); + return; + } + + // Bake Successful. Export the file + auto fileName = _jsURL.fileName(); + auto baseName = fileName.left(fileName.lastIndexOf('.')); + auto bakedFilename = baseName + BAKED_JS_EXTENSION; + + _bakedJSFilePath = _bakedOutputDir + "/" + bakedFilename; + + QFile bakedFile; + bakedFile.setFileName(_bakedJSFilePath); + if (!bakedFile.open(QIODevice::WriteOnly)) { + handleError("Error opening " + _bakedJSFilePath + " for writing"); + return; + } + + bakedFile.write(outputJS); + + // Export successful + _outputFiles.push_back(_bakedJSFilePath); + qCDebug(js_baking) << "Exported" << _jsURL << "minified to" << _bakedJSFilePath; + + // emit signal to indicate the JS baking is finished + emit finished(); +} + +bool JSBaker::bakeJS(const QByteArray& inputFile, QByteArray& outputFile) { + // Read from inputFile and write to outputFile per character + QTextStream in(inputFile, QIODevice::ReadOnly); + QTextStream out(outputFile, QIODevice::WriteOnly); + + // Algorithm requires the knowledge of previous and next character for each character read + QChar currentCharacter; + QChar nextCharacter; + // Initialize previousCharacter with new line + QChar previousCharacter = '\n'; + + in >> currentCharacter; + + while (!in.atEnd()) { + in >> nextCharacter; + + if (currentCharacter == '\r') { + out << '\n'; + } else if (currentCharacter == '/') { + // Check if single line comment i.e. // + if (nextCharacter == '/') { + handleSingleLineComments(in); + + //Start fresh after handling comments + previousCharacter = '\n'; + in >> currentCharacter; + continue; + } else if (nextCharacter == '*') { + // Check if multi line comment i.e. /* + bool success = handleMultiLineComments(in); + if (!success) { + // Errors present return false + return false; + } + //Start fresh after handling comments + previousCharacter = '\n'; + in >> currentCharacter; + continue; + } else { + // If '/' is not followed by '/' or '*' print '/' + out << currentCharacter; + } + } else if (isSpaceOrTab(currentCharacter)) { + // Check if white space or tab + + // Skip multiple spaces or tabs + while (isSpaceOrTab(nextCharacter)) { + in >> nextCharacter; + if (nextCharacter == '\n') { + break; + } + } + + // check if space can be omitted + if (!canOmitSpace(previousCharacter, nextCharacter)) { + out << ' '; + } + } else if (currentCharacter == '\n') { + // Check if new line + + //Skip multiple new lines + //Skip new line followed by space or tab + while (nextCharacter == '\n' || isSpaceOrTab(nextCharacter)) { + in >> nextCharacter; + } + + // Check if new line can be omitted + if (!canOmitNewLine(previousCharacter, nextCharacter)) { + out << '\n'; + } + } else if (isQuote(currentCharacter)) { + // Print the current quote and nextCharacter as is + out << currentCharacter; + out << nextCharacter; + + // Store the type of quote we are processing + QChar quote = currentCharacter; + + // Don't modify the quoted strings + while (nextCharacter != quote) { + in >> nextCharacter; + out << nextCharacter; + } + + //Start fresh after handling quoted strings + previousCharacter = nextCharacter; + in >> currentCharacter; + continue; + } else { + // In all other cases write the currentCharacter to outputFile + out << currentCharacter; + } + + previousCharacter = currentCharacter; + currentCharacter = nextCharacter; + } + + //write currentCharacter to output file when nextCharacter reaches EOF + if (currentCharacter != '\n') { + out << currentCharacter; + } + + // Successful bake. Return true + return true; +} + +void JSBaker::handleSingleLineComments(QTextStream& in) { + QChar character; + while (!in.atEnd()) { + in >> character; + if (character == '\n') { + break; + } + } +} + +bool JSBaker::handleMultiLineComments(QTextStream& in) { + QChar character; + while (!in.atEnd()) { + in >> character; + if (character == '*') { + if (in.read(1) == '/') { + return true; + } + } + } + return false; +} + +bool JSBaker::canOmitSpace(QChar previousCharacter, QChar nextCharacter) { + return(!((isAlphanum(previousCharacter) || isNonAscii(previousCharacter) || isSpecialCharacter(previousCharacter)) && + (isAlphanum(nextCharacter) || isNonAscii(nextCharacter) || isSpecialCharacter(nextCharacter))) + ); +} + +bool JSBaker::canOmitNewLine(QChar previousCharacter, QChar nextCharacter) { + return (!((isAlphanum(previousCharacter) || isNonAscii(previousCharacter) || isSpecialCharacterPrevious(previousCharacter)) && + (isAlphanum(nextCharacter) || isNonAscii(nextCharacter) || isSpecialCharacterNext(nextCharacter))) + ); +} + +//Check if character is alphabet, number or one of the following: '_', '$', '\\' or a non-ASCII character +bool JSBaker::isAlphanum(QChar c) { + return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') + || c == '_' || c == '$' || c == '\\' || c > ASCII_CHARACTERS_UPPER_LIMIT); +} + +bool JSBaker::isNonAscii(QChar c) { + return ((int)c.toLatin1() > ASCII_CHARACTERS_UPPER_LIMIT); +} + +// If previous and next characters are special characters, don't omit space +bool JSBaker::isSpecialCharacter(QChar c) { + return (c == '\'' || c == '$' || c == '_' || c == '/' || c== '+' || c == '-'); +} + +// If previous character is a special character, maybe don't omit new line (depends on next character as well) +bool JSBaker::isSpecialCharacterPrevious(QChar c) { + return (c == '\'' || c == '$' || c == '_' || c == '}' || c == ']' || c == ')' || c == '+' || c == '-' + || c == '"' || c == "'"); +} + +// If next character is a special character, maybe don't omit new line (depends on previous character as well) +bool JSBaker::isSpecialCharacterNext(QChar c) { + return (c == '\'' || c == '$' || c == '_' || c == '{' || c == '[' || c == '(' || c == '+' || c == '-'); +} + +// Check if white space or tab +bool JSBaker::isSpaceOrTab(QChar c) { + return (c == ' ' || c == '\t'); +} + +// Check If the currentCharacter is " or ' or ` +bool JSBaker::isQuote(QChar c) { + return (c == '"' || c == "'" || c == '`'); +} diff --git a/libraries/baking/src/JSBaker.h b/libraries/baking/src/JSBaker.h new file mode 100644 index 0000000000..b5889440cb --- /dev/null +++ b/libraries/baking/src/JSBaker.h @@ -0,0 +1,49 @@ +// +// JSBaker.h +// libraries/baking/src +// +// Created by Utkarsh Gautam on 9/18/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_JSBaker_h +#define hifi_JSBaker_h + +#include "Baker.h" +#include "JSBakingLoggingCategory.h" + +static const QString BAKED_JS_EXTENSION = ".baked.js"; + +class JSBaker : public Baker { + Q_OBJECT +public: + JSBaker(const QUrl& jsURL, const QString& bakedOutputDir); + static bool bakeJS(const QByteArray& inputFile, QByteArray& outputFile); + +public slots: + virtual void bake() override; + +private: + QUrl _jsURL; + QString _bakedOutputDir; + QString _bakedJSFilePath; + + static void handleSingleLineComments(QTextStream& in); + static bool handleMultiLineComments(QTextStream& in); + + static bool canOmitSpace(QChar previousCharacter, QChar nextCharacter); + static bool canOmitNewLine(QChar previousCharacter, QChar nextCharacter); + + static bool isAlphanum(QChar c); + static bool isNonAscii(QChar c); + static bool isSpecialCharacter(QChar c); + static bool isSpecialCharacterPrevious(QChar c); + static bool isSpecialCharacterNext(QChar c); + static bool isSpaceOrTab(QChar c); + static bool isQuote(QChar c); +}; + +#endif // !hifi_JSBaker_h diff --git a/libraries/baking/src/JSBakingLoggingCategory.cpp b/libraries/baking/src/JSBakingLoggingCategory.cpp new file mode 100644 index 0000000000..77c667922f --- /dev/null +++ b/libraries/baking/src/JSBakingLoggingCategory.cpp @@ -0,0 +1,14 @@ +// +// JSBakingLoggingCategory.cpp +// libraries/baking/src +// +// Created by Utkarsh Gautam on 9/18/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "JSBakingLoggingCategory.h" + +Q_LOGGING_CATEGORY(js_baking, "hifi.JS-baking"); diff --git a/libraries/baking/src/JSBakingLoggingCategory.h b/libraries/baking/src/JSBakingLoggingCategory.h new file mode 100644 index 0000000000..2c3ff535af --- /dev/null +++ b/libraries/baking/src/JSBakingLoggingCategory.h @@ -0,0 +1,19 @@ +// +// JSBakingLoggingCategory.h +// libraries/baking/src +// +// Created by Utkarsh Gautam on 9/18/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_JSBakingLoggingCategory_h +#define hifi_JSBakingLoggingCategory_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(js_baking) + +#endif // hifi_ModelBakingLoggingCategory_h diff --git a/tests/baking/CMakeLists.txt b/tests/baking/CMakeLists.txt new file mode 100644 index 0000000000..40217eded4 --- /dev/null +++ b/tests/baking/CMakeLists.txt @@ -0,0 +1,10 @@ + +# Declare dependencies +macro (setup_testcase_dependencies) + # link in the shared libraries + link_hifi_libraries(shared baking) + + package_libraries_for_deployment() +endmacro () + +setup_hifi_testcase() diff --git a/tests/baking/src/JSBakerTest.cpp b/tests/baking/src/JSBakerTest.cpp new file mode 100644 index 0000000000..082ffb047f --- /dev/null +++ b/tests/baking/src/JSBakerTest.cpp @@ -0,0 +1,92 @@ +// +// JSBakerTest.cpp +// tests/networking/src +// +// Created by Utkarsh Gautam on 09/26/17. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "JSBakerTest.h" +QTEST_MAIN(JSBakerTest) + +void JSBakerTest::setTestCases() { + // Test cases contain a std::pair(input, desiredOutput) + + _testCases.emplace_back("var a=1;", "var a=1;"); + _testCases.emplace_back("var a=1;//single line comment\nvar b=2;", "var a=1;var b=2;"); + _testCases.emplace_back("a\rb", "a\nb"); + _testCases.emplace_back("a/*multi\n line \n comment*/ b", "ab"); + _testCases.emplace_back("a/b", "a/b"); + _testCases.emplace_back("var a = 1;", "var a=1;"); // Multiple spaces omitted + _testCases.emplace_back("var a=\t\t\t1;", "var a=1;"); // Multiple tabs omitted + + // Cases for space not omitted + _testCases.emplace_back("var x", "var x"); + _testCases.emplace_back("a '", "a '"); + _testCases.emplace_back("a $", "a $"); + _testCases.emplace_back("a _", "a _"); + _testCases.emplace_back("a /", "a /"); + _testCases.emplace_back("a 1", "a 1"); + _testCases.emplace_back("1 a", "1 a"); + _testCases.emplace_back("$ a", "$ a"); + _testCases.emplace_back("_ a", "_ a"); + _testCases.emplace_back("/ a", "/ a"); + _testCases.emplace_back("$ $", "$ $"); + _testCases.emplace_back("_ _", "_ _"); + _testCases.emplace_back("/ /", "/ /"); + + _testCases.emplace_back("a\n\n\n\nb", "a\nb"); // Skip multiple new lines + _testCases.emplace_back("a\n\n b", "a\nb"); // Skip multiple new lines followed by whitespace + _testCases.emplace_back("a\n\n b", "a\nb"); // Skip multiple new lines followed by tab + + //Cases for new line not omitted + _testCases.emplace_back("a\nb", "a\nb"); + _testCases.emplace_back("a\n9", "a\n9"); + _testCases.emplace_back("9\na", "9\na"); + _testCases.emplace_back("a\n$", "a\n$"); + _testCases.emplace_back("a\n[", "a\n["); + _testCases.emplace_back("a\n{", "a\n{"); + _testCases.emplace_back("a\n(", "a\n("); + _testCases.emplace_back("a\n+", "a\n+"); + _testCases.emplace_back("a\n'", "a\n'"); + _testCases.emplace_back("a\n-", "a\n-"); + _testCases.emplace_back("$\na", "$\na"); + _testCases.emplace_back("$\na", "$\na"); + _testCases.emplace_back("_\na", "_\na"); + _testCases.emplace_back("]\na", "]\na"); + _testCases.emplace_back("}\na", "}\na"); + _testCases.emplace_back(")\na", ")\na"); + _testCases.emplace_back("+\na", "+\na"); + _testCases.emplace_back("-\na", "-\na"); + + // Cases to check quoted strings are not modified + _testCases.emplace_back("'abcd1234$%^&[](){}'\na", "'abcd1234$%^&[](){}'\na"); + _testCases.emplace_back("\"abcd1234$%^&[](){}\"\na", "\"abcd1234$%^&[](){}\"\na"); + _testCases.emplace_back("`abcd1234$%^&[](){}`\na", "`abcd1234$%^&[](){}`a"); + + // Edge Cases + + //No semicolon to terminate an expression, instead a new line used for termination + _testCases.emplace_back("var x=5\nvar y=6;", "var x=5\nvar y=6;"); + + //a + ++b is minified as a+ ++b. + _testCases.emplace_back("a + ++b", "a + ++b"); + + //a - --b is minified as a- --b. + _testCases.emplace_back("a - --b", "a - --b"); +} + +void JSBakerTest::testJSBaking() { + + for (int i = 0;i < _testCases.size();i++) { + QByteArray output; + auto input = _testCases.at(i).first; + JSBaker::bakeJS(input, output); + + auto desiredOutput = _testCases.at(i).second; + QCOMPARE(output, desiredOutput); + } +} diff --git a/tests/baking/src/JSBakerTest.h b/tests/baking/src/JSBakerTest.h new file mode 100644 index 0000000000..ea2cdc6696 --- /dev/null +++ b/tests/baking/src/JSBakerTest.h @@ -0,0 +1,29 @@ +// +// JSBakerTest.h +// tests/networking/src +// +// Created by Utkarsh Gautam on 9/26/17. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_JSBakerTest_h +#define hifi_JSBakerTest_h + +#include +#include + +class JSBakerTest : public QObject { + Q_OBJECT + +private slots: + void setTestCases(); + void testJSBaking(); + +private: + std::vector> _testCases; +}; + +#endif