Skip to content

Commit

Permalink
libdeng2|ScriptedInfo: Inheritance and groups
Browse files Browse the repository at this point in the history
Changed the record inheritance element to "inherits".

Group inheritance can be used for indicating that all members of the
group inherit a certain record.

Improved the apidoc.
  • Loading branch information
skyjake committed May 11, 2013
1 parent edd2436 commit db75647
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 28 deletions.
55 changes: 45 additions & 10 deletions doomsday/libdeng2/include/de/scriptsys/scriptedinfo.h
Expand Up @@ -28,15 +28,29 @@ 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.
* Analogous to an XML document with embedded JavaScript: Info acts as the
* generic, declarative, structured document and Doomsday Script is the
* procedural programming language.
*
* An instance of ScriptedInfo represents an Info document. It has its own
* An instance of ScriptedInfo contains 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.
* namespace (ScriptedInfo::names()).
*
* @par Grouping
*
* The block type "group" is reserved for generic grouping of contained
* elements. If the group is named, it will contribute its name to the path
* of the produced variable (same as with any named block):
* <pre>group test {
* type1 block { key = value }
* }</pre>
*
* In this example, the variable representing @c key would be
* <tt>test.block.key</tt> in the ScriptedInfo instance's namespace.
*
* @par Special elements
*
* Each block of a ScriptedInfo document has a couple of special elements
* that alter how the block is processed:
Expand All @@ -48,14 +62,31 @@ namespace de {
*
* - 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>
* (named "inherits"):
* <pre>type1 firstblock { key = value }
* type2 exampleblock inherits firstblock {}</pre>
*
* Here @c first-block would be treated as a variable name in the document's
* Here @c firstblock 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).
* beginning to end). The resulting Record is:
* <pre>exampleblock. __inherit__: firstblock
* __type__: type2
* key: value
* firstblock. __type__: type1
* key: value</pre>
*
* @par Group inheritance
*
* When the "inherits" element is used in a group, it will affect all the
* blocks in the group instead of inheriting anything into the group itself.
* <pre>thing A { key = value }
* group {
* inherits = A
* thing B {}
* thing C {}
* }</pre>
* Here B and C would both inherit from A.
*/
class DENG2_PUBLIC ScriptedInfo
{
Expand All @@ -80,6 +111,10 @@ class DENG2_PUBLIC ScriptedInfo
*/
Value *evaluate(String const &source);

Record &names();

Record const &names() const;

private:
DENG2_PRIVATE(d)
};
Expand Down
127 changes: 110 additions & 17 deletions doomsday/libdeng2/src/scriptsys/scriptedinfo.cpp
Expand Up @@ -24,6 +24,9 @@

namespace de {

static String const BLOCK_GROUP = "group";
static String const KEY_INHERIT = "inherits";

DENG2_PIMPL(ScriptedInfo)
{
typedef Info::Element::Value InfoValue;
Expand Down Expand Up @@ -72,7 +75,7 @@ DENG2_PIMPL(ScriptedInfo)
}
}

void execute(Info::BlockElement const *context)
void executeWithContext(Info::BlockElement const *context)
{
Record &ns = process.globals();

Expand Down Expand Up @@ -103,6 +106,61 @@ DENG2_PIMPL(ScriptedInfo)
}
}

void inherit(Info::BlockElement const &block, InfoValue const &target)
{
if(block.name().isEmpty())
{
// Nameless blocks cannot be inherited into.
return;
}

String varName = variableName(block);
if(!varName.isEmpty())
{
Record &ns = process.globals();
String targetName = target;
if(!ns.has(targetName))
{
// Assume it's an identifier rather than a regular variable.
targetName = targetName.toLower();
}

ns.add(varName.concatenatePath("__inherit__", '.')) =
new TextValue(targetName);

LOG_DEV_TRACE("setting __inherit__ of %s %s (%p) to %s",
block.blockType() << block.name() << &block << targetName);

DENG2_ASSERT(!varName.isEmpty());
DENG2_ASSERT(!targetName.isEmpty());

// Copy all present members of the target record.
ns.subrecord(varName)
.copyMembersFrom(ns[targetName].value<RecordValue>().dereference(),
Record::IgnoreDoubleUnderscoreMembers);
}
}

void inheritFromAncestors(Info::BlockElement const &block, Info::BlockElement const *from)
{
if(!from) return;

// The highest ancestor goes first.
if(from->parent())
{
inheritFromAncestors(block, from->parent());
}

// This only applies to groups.
if(from->blockType() == BLOCK_GROUP)
{
if(Info::KeyElement *key = from->findAs<Info::KeyElement>(KEY_INHERIT))
{
inherit(block, key->value());
}
}
}

void processBlock(Info::BlockElement const &block)
{
Record &ns = process.globals();
Expand All @@ -117,6 +175,20 @@ DENG2_PIMPL(ScriptedInfo)
}
}

// Inherit from all nameless parent blocks.
inheritFromAncestors(block, block.parent());

// Direct inheritance.
if(Info::KeyElement *key = block.findAs<Info::KeyElement>(KEY_INHERIT))
{
// Check for special attributes.
if(key->flags().testFlag(Info::KeyElement::Attribute))
{
// Inherit contents of an existing Record.
inherit(block, key->value());
}
}

// Script blocks are executed now.
if(block.blockType() == "script")
{
Expand All @@ -126,7 +198,7 @@ DENG2_PIMPL(ScriptedInfo)
script.reset(new Script(block.find("script")->values().first()));
script->setPath(sourcePath); // where the source comes from
process.run(*script);
execute(block.parent());
executeWithContext(block.parent());
}
else
{
Expand All @@ -140,35 +212,35 @@ DENG2_PIMPL(ScriptedInfo)
foreach(Info::Element const *sub, block.contentsInOrder())
{
// Handle special elements.
if(sub->name() == "condition")
{
continue;
}
if(sub->name() == ":" && !block.name().isEmpty())
if(sub->name() == "condition" || sub->name() == "inherits")
{
// Inheritance.
String target = sub->values().first();
ns.add(variableName(block).concatenatePath("__inherit__", '.')) =
new TextValue(target);

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

/**
* Determines the name of the variable representing an element. All named
* containing parent blocks contribute to the variable name.
*
* @param element Element whose variable name to determine.
*
* @return Variable name in the form <tt>ancestor.parent.elementname</tt>.
*/
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, '.');
if(varName.isEmpty())
varName = b->name();
else
varName = b->name().concatenatePath(varName, '.');
}
}
return varName;
Expand All @@ -178,10 +250,21 @@ DENG2_PIMPL(ScriptedInfo)
{
script.reset(new Script(source));
process.run(*script);
execute(context);
executeWithContext(context);
return process.context().evaluator().result().duplicate();
}

/**
* Constructs a Value from the value of an element. If the element value
* has been marked with the semantic hint for scripting, it will be
* evaluated as a script. The global __this__ will be pointed to the
* Record representing the @a context block.
*
* @param rawValue Value of an element.
* @param context Containing block element.
*
* @return Value instance. Caller gets ownership.
*/
Value *makeValue(InfoValue const &rawValue, Info::BlockElement const *context)
{
if(rawValue.flags.testFlag(InfoValue::Script))
Expand Down Expand Up @@ -234,4 +317,14 @@ Value *ScriptedInfo::evaluate(String const &source)
return d->evaluate(source, 0);
}

Record &ScriptedInfo::names()
{
return d->process.globals();
}

Record const &ScriptedInfo::names() const
{
return d->process.globals();
}

} // namespace de
29 changes: 28 additions & 1 deletion doomsday/tests/test_info/test_info.dei
Expand Up @@ -26,6 +26,33 @@ group {
condition: False
script test2 condition True { print "WILL NOT BE RUN" }
}

thing A {
member: value from A
}

thing B inherits A {
member2: value from B
script { print "Contents of B:"; print __this__ }
}

group {
# All members inherit A.
inherits = A

thing C {
member2: value from C
script { print "Contents of C:"; print __this__ }
}
thing D inherits B {
# Inherits A from group, then B.
member3: value from D
script { print "Contents of D:"; print __this__ }
}
}

type1 first-block { key = value }
type2 example-block inherits first-block {}

group font {
condition: Version.OS == 'macx'
Expand All @@ -45,7 +72,7 @@ group font {
}

# Identifiers are all case insensitive.
Font "Title" : font.default {
Font "Title" inherits font.default {
Size: 24pt
Weight $: if __this__.weight == "normal": "bold"; else: "normal"
}
Expand Down

0 comments on commit db75647

Please sign in to comment.