Merge pull request #11455 from utkarshgautamnyu/feat/JS-Baker

Feat/JS-Baker
This commit is contained in:
Stephen Birarda 2017-10-12 16:16:02 -05:00 committed by GitHub
commit 8a331e29a2
9 changed files with 480 additions and 3 deletions

View file

@ -30,6 +30,7 @@
#include <ClientServerUtils.h>
#include <FBXBaker.h>
#include <JSBaker.h>
#include <NodeType.h>
#include <SharedUtil.h>
#include <PathUtils.h>
@ -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<BakingStatus, QString> 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;
}

View file

@ -15,6 +15,7 @@
#include <FBXBaker.h>
#include <PathUtils.h>
#include <JSBaker.h>
BakeAssetTask::BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) :
_assetHash(assetHash),
@ -52,6 +53,10 @@ void BakeAssetTask::run() {
_baker = std::unique_ptr<FBXBaker> {
new FBXBaker(QUrl("file:///" + _filePath), fn, tempOutputDir)
};
} else if (_assetPath.endsWith(".js", Qt::CaseInsensitive)) {
_baker = std::unique_ptr<JSBaker>{
new JSBaker(QUrl("file:///" + _filePath), PathUtils::generateTemporaryDir())
};
} else {
tempOutputDir = PathUtils::generateTemporaryDir();
_baker = std::unique_ptr<TextureBaker> {

View file

@ -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 <PathUtils.h>
#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 == '`');
}

View file

@ -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

View file

@ -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");

View file

@ -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 <QtCore/QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(js_baking)
#endif // hifi_ModelBakingLoggingCategory_h

View file

@ -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()

View file

@ -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);
}
}

View file

@ -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 <QtTest/QtTest>
#include <JSBaker.h>
class JSBakerTest : public QObject {
Q_OBJECT
private slots:
void setTestCases();
void testJSBaking();
private:
std::vector<std::pair<QByteArray, QByteArray>> _testCases;
};
#endif