Skip to content

Commit

Permalink
libdeng2|scriptsys: Added ScriptedInfo
Browse files Browse the repository at this point in the history
ScriptedInfo combines Info (a structured declarative document) with
Doomsday Script (procedural programming language). Each ScriptedInfo
instance has its own local namespace where all the embedded scripts and
expressions are processed/evaluated.

The ".dei" (Doomsday Engine Info) file extension is used for scripted
info files (cf. ".de" is used for plain Doomsday Script modules).

Also added "test_info".
  • Loading branch information
skyjake committed May 10, 2013
1 parent 8975fef commit 5f96659
Show file tree
Hide file tree
Showing 7 changed files with 400 additions and 3 deletions.
1 change: 1 addition & 0 deletions doomsday/libdeng2/include/de/ScriptedInfo
@@ -0,0 +1 @@
#include "scriptsys/scriptedinfo.h"
89 changes: 89 additions & 0 deletions doomsday/libdeng2/include/de/scriptsys/scriptedinfo.h
@@ -0,0 +1,89 @@
/** @file scriptedinfo.h Info document tree with script context.
*
* @authors Copyright (c) 2013 Jaakko Keränen <jaakko.keranen@iki.fi>
*
* @par License
* GPL: http://www.gnu.org/licenses/gpl.html
*
* <small>This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. This program is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details. You should have received a copy of the GNU
* General Public License along with this program; if not, see:
* http://www.gnu.org/licenses</small>
*/

#ifndef LIBDENG2_SCRIPTEDINFO_H
#define LIBDENG2_SCRIPTEDINFO_H

#include "../Info"
#include "../File"

namespace de {

/**
* Info document tree with a script context and built-in support for handling
* expressions and embedded scripts.
*
* Analogous to an HTML document with embedded JavaScript: Info acts as the
* declarative, structured document and Doomsday Script is the procedural
* programming language.
*
* An instance of ScriptedInfo represents an Info document. It has its own
* private script execution context, in which expressions can be evaluated and
* scripts run. After a ScriptedInfo has been parsed, all the embedded scripts
* are run and the Info elements become variables and values in the local
* namespace.
*
* Each block of a ScriptedInfo document has a couple of special elements
* that alter how the block is processed:
*
* - The "condition" element that may be present in any block determines
* whether the block is processed or skipped. The value of the "condition"
* element is evaluated as a script, and if it evaluates to False, the
* entire block is ignored.
*
* - The contents of any previously processed block (or any record available in
* the namespace) can be copied with the special inheritance element
* (named ":"):
* <pre>type1 first-block { key = value }
* type2 example-block : first-block {}</pre>
*
* Here @c first-block would be treated as a variable name in the document's
* local namespace, referring to the block above, which has already been
* added to the local namespace (elements are processed sequentially from
* beginning to end).
*/
class DENG2_PUBLIC ScriptedInfo
{
public:
ScriptedInfo();

void clear();

void parse(String const &source);

void parse(File const &file);

/**
* Evaluates one or more statements and returns the result. All processing
* is done in the document's local script context. Changes to the context's
* global namespace are allowed and will remain in effect after the
* evaluation finishes.
*
* @param source Statements or expression to evaluate.
*
* @return Result value. Caller gets ownership.
*/
Value *evaluate(String const &source);

private:
DENG2_PRIVATE(d)
};

} // namespace de

#endif // LIBDENG2_SCRIPTEDINFO_H
7 changes: 4 additions & 3 deletions doomsday/libdeng2/scriptsys.pri
Expand Up @@ -27,6 +27,7 @@ HEADERS += \
include/de/PrintStatement \
include/de/Process \
include/de/Script \
include/de/ScriptedInfo \
include/de/ScriptLex \
include/de/ScriptSystem \
include/de/Statement \
Expand Down Expand Up @@ -64,6 +65,7 @@ HEADERS += \
include/de/scriptsys/printstatement.h \
include/de/scriptsys/process.h \
include/de/scriptsys/script.h \
include/de/scriptsys/scriptedinfo.h \
include/de/scriptsys/scriptlex.h \
include/de/scriptsys/scriptsystem.h \
include/de/scriptsys/statement.h \
Expand Down Expand Up @@ -100,12 +102,11 @@ SOURCES += \
src/scriptsys/printstatement.cpp \
src/scriptsys/process.cpp \
src/scriptsys/script.cpp \
src/scriptsys/scriptedinfo.cpp \
src/scriptsys/scriptlex.cpp \
src/scriptsys/scriptsystem.cpp \
src/scriptsys/statement.cpp \
src/scriptsys/tokenbuffer.cpp \
src/scriptsys/tokenrange.cpp \
src/scriptsys/trystatement.cpp \
src/scriptsys/whilestatement.cpp

SOURCES += \
src/scriptsys/scriptsystem.cpp
206 changes: 206 additions & 0 deletions doomsday/libdeng2/src/scriptsys/scriptedinfo.cpp
@@ -0,0 +1,206 @@
/** @file scriptedinfo.cpp Info document tree with script context.
*
* @authors Copyright (c) 2013 Jaakko Keränen <jaakko.keranen@iki.fi>
*
* @par License
* GPL: http://www.gnu.org/licenses/gpl.html
*
* <small>This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. This program is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details. You should have received a copy of the GNU
* General Public License along with this program; if not, see:
* http://www.gnu.org/licenses</small>
*/

#include "de/ScriptedInfo"
#include "de/Script"
#include "de/Process"
#include "de/ArrayValue"
#include "de/RecordValue"

namespace de {

DENG2_PIMPL(ScriptedInfo)
{
Info info; ///< Original full parsed contents.
QScopedPointer<Script> script; ///< Current script being executed.
Process process; ///< Execution context.
String sourcePath;

Instance(Public *i) : Base(i)
{}

void clear()
{
info.clear();
process.clear();
script.reset();
}

/**
* Iterates through the parsed Info contents and processes each element.
* Script blocks are parsed as Doomsday Script and executed in the local
* process. Key/value elements become variables (and values) in the
* process's global namespace.
*/
void processAll()
{
processBlock(info.root());

LOG_DEBUG("Processed contents:\n") << process.globals().asText();
}

void processElement(Info::Element const *element)
{
if(element->isBlock())
{
processBlock(*static_cast<Info::BlockElement const *>(element));
}
else if(element->isKey())
{
processKey(*static_cast<Info::KeyElement const *>(element));
}
else if(element->isList())
{
processList(*static_cast<Info::ListElement const *>(element));
}
}

void processBlock(Info::BlockElement const &block)
{
if(Info::Element *condition = block.find("condition"))
{
// Any block will be ignored if its condition is false.
QScopedPointer<Value> result(evaluate(condition->values().first()));
if(result.isNull() || result->isFalse())
{
return;
}
}

if(block.blockType() == "script")
{
DENG2_ASSERT(block.find("script") != 0);
DENG2_ASSERT(process.state() == Process::Stopped);

script.reset(new Script(block.find("script")->values().first()));
script->setPath(sourcePath); // where the source comes from
process.run(*script);
process.execute();
}
else
{
// Block type placed into a special variable (only with named blocks, though).
if(!block.name().isEmpty())
{
String varName = variableName(block).concatenatePath("__type__", '.');
process.globals().add(varName) = new TextValue(block.blockType());
}

foreach(Info::Element const *sub, block.contentsInOrder())
{
if(sub->name() == "condition")
{
// Skip special elements.
continue;
}
if(sub->name() == ":" && !block.name().isEmpty())
{
// Inheritance.
String target = sub->values().first();
process.globals().add(variableName(block).concatenatePath("__inherit__", '.')) =
new TextValue(target);

// Copy all present members of the target record.
process.globals().subrecord(variableName(block))
.copyMembersFrom(process.globals()[target].value<RecordValue>().dereference());
continue;
}
processElement(sub);
}
}
}

String variableName(Info::Element const &element)
{
String varName = element.name();
for(Info::BlockElement *b = element.parent(); b != 0; b = b->parent())
{
if(!b->name().isEmpty())
{
varName = b->name().concatenatePath(varName, '.');
}
}
return varName;
}

static bool isEvaluatable(String const &value)
{
return value.startsWith('$');
}

Value *evaluate(String const &source)
{
script.reset(new Script(source));
process.run(*script);
process.execute();
return process.context().evaluator().result().duplicate();
}

Value *makeValue(String const &rawValue)
{
if(isEvaluatable(rawValue))
{
return evaluate(rawValue.substr(1));
}
return new TextValue(rawValue);
}

void processKey(Info::KeyElement const &key)
{
QScopedPointer<Value> v(makeValue(key.value()));
process.globals().add(variableName(key)) = v.take();
}

void processList(Info::ListElement const &list)
{
ArrayValue* av = new ArrayValue;
foreach(QString v, list.values())
{
*av << makeValue(v);
}
process.globals().addArray(variableName(list), av);
}
};

ScriptedInfo::ScriptedInfo() : d(new Instance(this))
{}

void ScriptedInfo::clear()
{
d->clear();
}

void ScriptedInfo::parse(String const &source)
{
d->clear();
d->info.parse(source);
d->processAll();
}

void ScriptedInfo::parse(File const &file)
{
d->sourcePath = file.path();
parse(String::fromUtf8(Block(file)));
}

Value *ScriptedInfo::evaluate(String const &source)
{
return d->evaluate(source);
}

} // namespace de
45 changes: 45 additions & 0 deletions doomsday/tests/test_info/main.cpp
@@ -0,0 +1,45 @@
/*
* The Doomsday Engine Project
*
* Copyright (c) 2009-2013 Jaakko Keränen <jaakko.keranen@iki.fi>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/

#include <de/TextApp>
#include <de/LogBuffer>
#include <de/ScriptedInfo>
#include <de/FS>
#include <QDebug>

using namespace de;

int main(int argc, char **argv)
{
try
{
TextApp app(argc, argv);
app.initSubsystems(App::DisablePlugins);

ScriptedInfo dei;
dei.parse(app.fileSystem().find("test_info.dei"));
}
catch(Error const &err)
{
qWarning() << err.asText();
}

qDebug("Exiting main()...");
return 0;
}

0 comments on commit 5f96659

Please sign in to comment.