package com.github.czyzby.lml.parser.impl.tag.macro;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ObjectMap;
import com.github.czyzby.kiwi.util.common.Nullables;
import com.github.czyzby.kiwi.util.common.Strings;
import com.github.czyzby.kiwi.util.gdx.collection.GdxArrays;
import com.github.czyzby.kiwi.util.tuple.immutable.Pair;
import com.github.czyzby.lml.parser.LmlParser;
import com.github.czyzby.lml.parser.LmlSyntax;
import com.github.czyzby.lml.parser.impl.tag.AbstractMacroLmlTag;
import com.github.czyzby.lml.parser.tag.LmlTag;
import com.github.czyzby.lml.parser.tag.LmlTagProvider;
import com.github.czyzby.lml.util.LmlUtilities;
import com.github.czyzby.lml.util.collection.IgnoreCaseStringMap;
/** Meta macro tag allows to create custom macros from within LML templates. It basically modifies LML syntax to include
* new macro tags, parsed from the data it receives. First attribute is an LML array of macro aliases. The second
* (optional) attribute is the name of the data between tags argument in evaluated macro. The rest are custom attributes
* expected by the macro. For example:
*
* <blockquote>
*
* <pre>
* <:macro newActor><actor/></:macro>
* <:newActor/>
* </pre>
*
*
* </blockquote>This is a very simple macro that adds a single tag on evaluation. It is not prepared to handle content
* between tags and does not have any extra arguments. After invoking (second line in the example), it will spawn a
* single tag: <actor/>. <blockquote>
*
* <pre>
* <:macro newActor "" id><actor id={id}/></:macro>
* <:newActor id=actorId/>
* <:newActor actorId/>
* <:newActor/>
* </pre>
*
* </blockquote>In this updated example, we added two arguments: empty quotation (as we still don't want to parse data
* between macro tags) and id attribute. Value assigned to "id" will replace all "{id}" arguments in the macro. First
* and second evaluation will produce <actor id=actorId/> attribute: note that both named ("id=actorId") and
* unnamed ("actorId") attribute passing is valid, as long as named and unnamed attributes are not mixed. If you use
* named attributes, you can pass attributes in any other; if you use unnamed attributes, their passing order must match
* declaration order. The third evaluation example will produce an actor with a null ID, as we didn't pass the attribute
* and it was replaced by null. <blockquote>
*
* <pre>
* <:macro newActor "" id=defaultId><actor id={id}/></:macro>
* <:newActor id=actorId/>
* <:newActor/>
* </pre>
*
* </blockquote> In this example, we added a default value to the "id" attribute. Now, if macro is evaluated without
* "id" attribute set, "{id}" will be replaced with default value. First invocation will produce <actor
* id=actorId/>, second - <actor id=defaultId/>. Note that quotations on default value - as well as on any
* attribute - is optional.<blockquote>
*
* <pre>
* <:macro newActor content><actor id={content}/></:macro>
* <:newActor>actorId</:newActor>
* </pre>
*
* </blockquote> In this example, we replaced second attribute with "content", allowing us to use data between macro
* tags to be used inside our defined macro. Macro invocation will produce <actor id=actorId/> tag, replacing
* "{content}" with data between tags. <blockquote>
*
* <pre>
* <:macro dialog content title includeCloseButton=true>
* <dialog title={title} defaultPad=4>
* {content}
* <:if {includeCloseButton}>
* <textButton expandX=true fillX=true onResult=close>@closeButton</textButton>
* </:if>
* </dialog>
* </:macro>
*
* <!-- This: -->
*
* <:dialog title=@error>
* <label style=big>@someWarning</label>
* </:dialog>
*
* <!-- ...evaluates to: -->
*
* <dialog title=@error defaultPad=4>
* <label style=big>@someWarning</label>
* <textButton expandX=true fillX=true onResult=close>@closeButton</textButton>
* </dialog>
* </pre>
*
* </blockquote> This is an example of more complex macro that might be used to quickly construct simple message
* dialogs. Note that text proceeded with {@literal @} sign is extracted from i18n bundle. If you construct some kind of
* widget multiple times with similar settings using plain LML tags, you should consider creating a macro instead for
* simplified tags content.
* <p>
* Note that named attributes are supported by this macro, but it ALWAYS HAS TO start with "alias" and "replace"
* attributes if you want to add custom macro attributes. For example:<blockquote>
*
* <pre>
* <:macro alias="mySimpleMacro">
* <label text="Hello."/>
* </:macro>
*
* <:macro replace="content" alias="myContentMacro">
* <label>{content}</label>
* </:macro>
*
* <:macro alias="myMacro" replace="content" id="default" >
* <table id="{id}">
* {content}
* </table>
* </:macro>
*
* <!-- Macro invocations/; -->
* <:mySimpleMacro/>
* <:myContentMacro>Hello.</:myContentMacro>
* <:myMacro id="custom"><label text="Hello."/></:myMacro>
* </pre>
*
* </blockquote>This meta-macro needs a way of detecting custom, user-added attributes to the new macro it creates -
* current implementation assumes that any third (or next) attribute is custom. First two attributes are reserved for
* "alias" and "replace". Note that DTD validation will not recognize your custom attributes in meta macro, so you might
* need to give up DTD in your macro files (that's why a global macro file is a good idea) or modify DTD files manually.
*
* @author MJ */
public class MetaLmlMacroTag extends AbstractMacroLmlTag {
/** Alias of the first macro attribute: new macro aliases array. */
public static final String ALIAS_ATTRIBUTE = "alias";
/** Alias of the second macro attribute: name of the argument to replace in macro with the content between tags. */
public static final String REPLACE_ATTRIBUTE = "replace";
public MetaLmlMacroTag(final LmlParser parser, final LmlTag parentTag, final StringBuilder rawTagData) {
super(parser, parentTag, rawTagData);
}
@Override
public void handleDataBetweenTags(final CharSequence rawMacroContent) {
if (GdxArrays.isEmpty(getAttributes())) {
getParser().throwErrorIfStrict("Custom macro tag needs at least one attribute: tag names array.");
return;
}
final Pair<Array<String>, Array<String>> attributeNamesAndDefaultValues = getAttributeNamesAndDefaultValues();
getParser().getSyntax().addMacroTagProvider(
new CustomLmlMacroTagProvider(getContentAttributeName(), attributeNamesAndDefaultValues.getFirst(),
attributeNamesAndDefaultValues.getSecond(), rawMacroContent.toString()),
getSupportedTagNames());
}
/** @return second macro attribute. */
protected String getContentAttributeName() {
if (hasAttribute(REPLACE_ATTRIBUTE)) {
return getAttribute(REPLACE_ATTRIBUTE);
} else if (GdxArrays.sizeOf(getAttributes()) > 1) {
return getAttributes().get(1);
}
return null;
}
/** @return parsed additional macro attributes. */
protected Pair<Array<String>, Array<String>> getAttributeNamesAndDefaultValues() {
final Array<String> attributes = getAttributes();
final Array<String> attributeNames = GdxArrays.newArray();
final Array<String> defaultValues = GdxArrays.newArray();
if (GdxArrays.sizeOf(attributes) > 2) {
final LmlSyntax syntax = getParser().getSyntax();
for (int index = 2, length = attributes.size; index < length; index++) {
final String rawAttribute = attributes.get(index);
if (Strings.contains(rawAttribute, syntax.getAttributeSeparator())) {
final int separatorIndex = rawAttribute.indexOf(syntax.getAttributeSeparator());
attributeNames.add(rawAttribute.substring(0, separatorIndex).trim());
defaultValues.add(LmlUtilities
.stripQuotation(rawAttribute.substring(separatorIndex + 1, rawAttribute.length())));
} else {
attributeNames.add(rawAttribute.trim());
defaultValues.add(Nullables.DEFAULT_NULL_STRING);
}
}
}
return Pair.of(attributeNames, defaultValues);
}
/** @return first macro argument parsed as an array. */
protected String[] getSupportedTagNames() {
final Actor actor = getActor(); // Needed to parse raw LML data.
final String attribute = hasAttribute(ALIAS_ATTRIBUTE) ? getAttribute(ALIAS_ATTRIBUTE)
: getAttributes().first();
final String[] names = getParser().parseArray(attribute, actor);
for (int index = 0, length = names.length; index < length; index++) {
// Arrays might not be fully parsed, but in this case, we need absolute macro names.
names[index] = getParser().parseString(names[index], actor);
}
return names;
}
@Override
public String[] getExpectedAttributes() {
return new String[] { ALIAS_ATTRIBUTE, REPLACE_ATTRIBUTE };
}
/** Provides a custom macro tag created in LML templates.
*
* @author MJ */
public static class CustomLmlMacroTagProvider implements LmlTagProvider {
private final String contentAttributeName;
private final Array<String> attributeNames;
private final Array<String> defaultAttributeValues;
private final String macroContent;
public CustomLmlMacroTagProvider(final String contentAttributeName, final Array<String> attributeNames,
final Array<String> defaultAttributeValues, final String macroContent) {
this.contentAttributeName = contentAttributeName;
this.attributeNames = attributeNames;
this.defaultAttributeValues = defaultAttributeValues;
this.macroContent = macroContent;
}
@Override
public LmlTag create(final LmlParser parser, final LmlTag parentTag, final StringBuilder rawTagData) {
return new CustomLmlMacroTag(parser, parentTag, rawTagData, contentAttributeName, attributeNames,
defaultAttributeValues, macroContent);
}
}
/** Represents a custom macro registered through LML template.
*
* @author MJ */
public static class CustomLmlMacroTag extends AbstractMacroLmlTag {
private final String contentAttributeName;
private final Array<String> attributeNames;
private final Array<String> defaultAttributeValues;
private final String macroContent;
private CharSequence contentBetweenTags;
public CustomLmlMacroTag(final LmlParser parser, final LmlTag parentTag, final StringBuilder rawTagData,
final String contentAttributeName, final Array<String> attributeNames,
final Array<String> defaultAttributeValues, final String macroContent) {
super(parser, parentTag, rawTagData);
this.contentAttributeName = contentAttributeName;
this.attributeNames = attributeNames;
this.defaultAttributeValues = defaultAttributeValues;
this.macroContent = macroContent;
}
@Override
protected boolean supportsOptionalNamedAttributes() {
return true;
}
@Override
public void handleDataBetweenTags(final CharSequence rawMacroContent) {
contentBetweenTags = rawMacroContent;
}
@Override
public void closeTag() {
appendTextToParse(replaceArguments(macroContent, getMacroArguments()));
}
private ObjectMap<String, CharSequence> getMacroArguments() {
final ObjectMap<String, CharSequence> arguments = new IgnoreCaseStringMap<CharSequence>();
if (contentAttributeName != null) {
arguments.put(contentAttributeName,
contentBetweenTags == null ? Strings.EMPTY_STRING : contentBetweenTags);
}
if (GdxArrays.isEmpty(getAttributes())) {
putDefaultAttributes(arguments);
} else if (areAttributesNamed()) {
putNamedAttributes(arguments);
} else {
putUnnamedAttributes(arguments);
}
return arguments;
}
private boolean areAttributesNamed() {
boolean allNamed = true;
boolean anyNamed = false;
final LmlSyntax syntax = getParser().getSyntax();
for (final String attribute : getAttributes()) {
if (Strings.contains(attribute, syntax.getAttributeSeparator())) {
anyNamed = true;
allNamed &= true;
} else {
allNamed = false;
}
}
if (anyNamed && !allNamed) {
getParser().throwError(
"Custom macros cannot have both named (\"attribute=value\") and unnamed (\"value\") attributes");
}
return allNamed;
}
private void putDefaultAttributes(final ObjectMap<String, CharSequence> arguments) {
for (int index = 0, length = attributeNames.size; index < length; index++) {
arguments.put(attributeNames.get(index), defaultAttributeValues.get(index));
}
}
private void putNamedAttributes(final ObjectMap<String, CharSequence> arguments) {
final ObjectMap<String, String> namedAttributes = getNamedAttributes();
for (int index = 0, length = attributeNames.size; index < length; index++) {
final String attributeName = attributeNames.get(index);
if (namedAttributes.containsKey(attributeName)) {
arguments.put(attributeName, namedAttributes.get(attributeName));
} else {
arguments.put(attributeName, defaultAttributeValues.get(index));
}
}
}
private void putUnnamedAttributes(final ObjectMap<String, CharSequence> arguments) {
final Array<String> attributes = getAttributes();
for (int index = 0, length = attributeNames.size; index < length; index++) {
arguments.put(attributeNames.get(index),
index < attributes.size ? attributes.get(index) : defaultAttributeValues.get(index));
}
}
@Override
public String[] getExpectedAttributes() {
// Creating typed array. This is a debugging method anyway.
final Array<String> typedArray = GdxArrays.newArray(String.class);
typedArray.addAll(attributeNames);
return typedArray.toArray();
}
}
}