XML macros provide a way to organize applications consisting of large XML configuration files.
In order to use macros, the macro namespace must be declared:
<root xmlns:macro="http://www.mira-project.org/xmlns:macro">
</root>
Invoking macros
When a macro is encountered in an XML file, it is evaluated and replaced with the resulting XML at the invocation site.
<macro:MyMacro MyParameter="MyValue" />
<!-- or -->
<macro:MyMacro>
<MyParameter>MyValue</MyParameter>
</macro:MyMacro>
Macros that do not require parameters can be coerced into strings:
<MyXMLNode MyAttribute="${macro MyMacroWithNoParameters}" />
What exactly the macro expands to depends on the definition of MyMacro.
Defining macros
In order to define a macro, the desired name of the macro is prefixed with macro:Define_. Then follows the definition of required and optional parameters. Parameters are evaluated in the same scope as the body of the definition. This scope has access to all macros that were available where the macro was defined, as well as all parameters. Note that parameters themselves are also macros and can be evaluated via invocation.
Example
<macro:Define_MyMacro>
<Parameters>
<MyParameter />
<!-- String default value: -->
<MyOptionalParameter1>MyDefaultValue</MyOptionalParameter1>
<!-- Nested XML as default value: -->
<MyOptionalParameter2>
<Foo>Bar</Foo>
</MyOptionalParameter2>
<!-- Use the value of another parameter as a default. -->
<MyOptionalParameter3>
<macro:MyParameter />
</MyOptionalParameter3>
</Parameters>
<Body>
<!-- Use the parameters to construct some XML. -->
<Foo><macro:MyParameter /></Foo>
</Body>
</macro:Define_MyMacro>
Importing macros
Macros from other XML files can be imported by pointing an XML namespace to the file containing macro definitions. Imported macros are then available under that namespace.
Example
<root
xmlns:macro="http://www.mira-project.org/xmlns:macro"
xmlns:mymodule="Package:path/to/file.xml"
>
<mymodule:MyMacro MyParameter="MyValue" />
<!-- Coerce namespaced macros into strings. -->
${macro mymodule:MyMacroWithNoParameters}
</root>
Single macros can also be imported in the same line as their invocation. This is useful when the macro is used only once.
Example
<macro:MyImportedMacro From="file/that/defines/MyImportedMacro.xml">
...
By default an <include> tag does not leak any macro definitions into the active file. Macros can be explicitly loaded into a namespace by providing the macroNamespace attribute. However, using XML namespace delcarations or "From" imports is recommended.
Example
<include
file=
"..." macroNamespace=
"mymodule" />
Scoping rules
In contrast to XML variables, macros are not global. Each macro operates in a scope which dictates how other macros are resolved. In addition to this, definitions are immutable and cannot be changed. As a consequence, you do not have to worry about coming up with globally unique names. The following rules apply for resolving definitions:
- Definitions from <Parameters> are only available inside the definition.
- Definitions from lower scopes (i.e. from <Parameters>) will shadow definitions from higher scopes.
- Definitions made inside other definitions are only available inside that definition.
- Each XML file operates in its own, separate scope.
Example
<macro:Define_Foo>
<Body>123</Body>
</macro:Define_Foo>
<macro:Define_Bar>
<Parameters>
<Foo />
</Parameters>
<Body>
<!-- This refers to the parameter, not the global Foo -->
<macro:Foo />
</Body>
</macro:Define_Bar>
<!-- Result: ABC -->
<macro:Bar><Foo>ABC</Foo></macro:Bar>
<!-- Result: 123 -->
<macro:Foo />
<!-- Result: 123 -->
<macro:Bar><macro:Foo /></macro:Bar>
Nested definitions allow you to define small helper macros that can capture parameters but aren't available outside of the parent definition.
Example
<macro:Define_MyMacro>
<Parameters>
<MyParameter />
<Parameters>
<Body>
<!-- Create a helper macro that only exists within this definition. -->
<macro:Define_MyHelperMacro>
<Parameters>
<Sentence />
</Parameters>
<Body>
<!-- Note that the helper has access to its parent scope and therefore to MyParameter. -->
<macro:Sentence /> <macro:MyParameter />
</Body>
</macro:Define_MyHelperMacro>
<macro:MyHelperMacro Sentence="The parameter is " />
<macro:MyHelperMacro Sentence="The parameter still is " />
<Body>
</macro:Define_MyMacro>
<!-- Result:
The parameter is Foo
The parameter still is Foo
-->
<macro:MyMacro MyParameter="Foo" />
<!-- Error: No definition for <macro:MyHelperMacro> found. -->
<macro:MyHelperMacro Sentence="This will not work!" />
Lazy evaluation
Macros are intentionally evaluated lazily. This means that an invocation is not turned into XML when being passed as a parameter to another macro. This is a crucial detail when working with global variables. While mixing global variables and macros should be avoided, it may be necessary for backward compatibility.
Example
<macro:Define_MyMacro>
<Parameters>
<MyParameter />
</Parameters>
<Body>
<var myVariable="1" overwrite="true" />
<macro:MyParameter />
<var myVariable="2" overwrite="true" />
<macro:MyParameter />
</Body>
</macro:Define_MyMacro>
<macro:MyMacro>
<MyParameter>
<!-- We have not defined myVariable yet, but this will not throw an exception. -->
<ValueOfVariable>${myVariable}</ValueOfVariable>
</MyParameter>
</macro:MyMacro>
The above code will expand to:
<ValueOfVariable>1</ValueOfVariable>
<ValueOfVariable>2</ValueOfVariable>
Built-in macros
The following macros are built-in and can be used within any scope:
- <macro:Nothing /> evaluates to nothing. Can be used as a default parameter.
- <macro:PrintXML /> prints the contained XML and evaluates to nothing. Intended for debugging macros.
- <macro:Split /> splits a space-separated string into <item> tags containing the words
- <macro:Zip /> zips an object consisting of several lists into a list of objects
<macro:Zip>
<MyNumber>
<item>1</item>
<item>2</item>
<item>3</item>
</MyNumber>
<MyLetter>
<item>A</item>
<item>B</item>
<item>C</item>
</MyLetter>
</macro:Zip>
<!-- Results in: -->
<item>
<MyNumber>1</MyNumber>
<MyLetter>A</MyLetter>
</item>
<item>
<MyNumber>2</MyNumber>
<MyLetter>B</MyLetter>
</item>
<item>
<MyNumber>3</MyNumber>
<MyLetter>C</MyLetter>
</item>
- <macro:For /> iterates over a range of values:
<macro:For>
<!-- Name this whatever you like; it will be available in the <Do> tag. -->
<MyIterator>
<item>1</item>
<item>2</item>
<item>3</item>
</MyIterator>
<Do>
<TheValueIs><macro:MyIterator /></TheValueIs>
</Do>
</macro:For>
<!-- Results in: -->
<TheValueIs>1</TheValueIs>
<TheValueIs>2</TheValueIs>
<TheValueIs>3</TheValueIs>
Example
This integrated example shows how to combine Split, Zip and For to define a list of complex items:
<var ids = "2 4" />
<var names = "foo bar" />
<macro:For>
<macro:Zip>
<Name>
<macro:Split Words="${names}" />
</Name>
<ID>
<macro:Split Words="${ids}" />
</ID>
</macro:Zip>
<Do>
<Name><macro:
Object.Name /></Name>
</Do>
</macro:For>
<!-- Results in: -->
<ID>2</ID>
<Name>foo</Name>
<ID>4</ID>
<Name>bar</Name>
Definition of more Object items requires nothing more than extending the variables for ids and names - they just need to be symmetric.