package com.github.czyzby.lml.parser.impl;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ObjectMap;
import com.github.czyzby.kiwi.util.common.Strings;
import com.github.czyzby.kiwi.util.gdx.collection.GdxArrays;
import com.github.czyzby.kiwi.util.gdx.collection.GdxMaps;
import com.github.czyzby.lml.parser.LmlParser;
import com.github.czyzby.lml.parser.LmlStyleSheet;
import com.github.czyzby.lml.parser.LmlTemplateReader;
import com.github.czyzby.lml.parser.LssSyntax;
/** Allows to process LML style sheet code (with CSS-like syntax), extracting default attribute values of selected tags.
*
* <p>
* Parser instances should be generally one-time use only - since style sheets are parsed rather rarely (usually once
* per application run), reusing LSS parsers usually does not make much sense.
*
* @author MJ */
public class LssParser {
private final LmlParser parser;
private final LmlStyleSheet styleSheet;
private final LmlTemplateReader reader;
private final char inheritanceMarker;
private final char blockOpening;
private final char blockClosing;
private final char separator;
private final char lineEnd;
private final char tagSeparator;
private final char commentMarker;
private final char commentSecondary;
// Control variables:
private final StringBuilder builder = new StringBuilder();
private final Array<String> tags = GdxArrays.newArray();
private final Array<String> inherits = GdxArrays.newArray();
private String attribute;
private final ObjectMap<String, String> attributes = GdxMaps.newObjectMap();
/** @param parser will be used to extract style sheet and syntax data. */
public LssParser(final LmlParser parser) {
this.parser = parser;
styleSheet = parser.getStyleSheet();
reader = new DefaultLmlTemplateReader();
final LssSyntax syntax = parser.getSyntax().getLssSyntax();
inheritanceMarker = syntax.getInheritanceMarker();
blockOpening = syntax.getBlockOpening();
blockClosing = syntax.getBlockClosing();
separator = syntax.getSeparator();
lineEnd = syntax.getLineEnd();
tagSeparator = syntax.getTagSeparator();
commentMarker = syntax.getCommentMarker();
commentSecondary = syntax.getSecondaryCommentMarker();
}
/** @param lss LML style sheet data. Will be parsed and processed. */
public void parse(final String lss) {
reader.append(lss, "LML style sheet");
try {
while (reader.hasNextCharacter()) {
burnWhitespaces();
if (!reader.hasNextCharacter()) {
break;
}
parseNames();
parseAttributes();
processAttributes();
tags.clear();
inherits.clear();
attributes.clear();
}
} finally {
reader.clear();
}
}
/** @param string will become the exception message. */
protected void throwException(final String string) {
parser.throwError(string);
}
/** Parses names proceeding styles block. */
protected void parseNames() {
while (reader.hasNextCharacter()) {
final char character = next();
if (Strings.isWhitespace(character)) {
addName();
continue;
} else if (character == blockOpening) {
addName();
break;
} else {
builder.append(character);
}
}
if (GdxArrays.isEmpty(tags)) {
throwException("No tag names chosen.");
}
}
/** Appends tag or inheritance name from the current builder data. */
protected void addName() {
if (Strings.isNotEmpty(builder)) {
int endOffset = 0;
if (Strings.endsWith(builder, tagSeparator)) { // Ends with ','.
endOffset = 1;
}
if (Strings.startsWith(builder, inheritanceMarker)) { // Starts with '.'.
inherits.add(builder.substring(1, builder.length() - endOffset));
} else {
tags.add(builder.substring(0, builder.length() - endOffset));
}
Strings.clearBuilder(builder);
}
}
/** Parses attributes block. */
protected void parseAttributes() {
burnWhitespaces();
attribute = null;
while (reader.hasNextCharacter()) {
char character = reader.peekCharacter();
if (Strings.isNewLine(character) && (attribute != null || Strings.isNotEmpty(builder))) {
throwException("Expecting line end marker: '" + lineEnd + "'.");
}
character = next();
if (Strings.isNewLine(character) && (attribute != null || Strings.isNotEmpty(builder))) {
// Needs double check, possible comment along the way.
throwException("Expecting line end marker: '" + lineEnd + "'.");
} else if (character == blockClosing) {
if (attribute != null || Strings.isNotEmpty(builder)) {
throwException("Unexpected tag close.");
}
return;
} else if (Strings.isWhitespace(character) && attribute == null) {
continue;
} else if (character == separator && attribute == null) {
addAttributeName();
continue;
} else if (character == lineEnd) {
if (attribute == null) {
throwException("Found unexpected line end marker: '" + lineEnd + "'. Is separator (" + separator
+ ") missing?");
}
addAttribute();
} else {
builder.append(character);
}
}
}
/** Caches currently parsed attribute name. */
protected void addAttributeName() {
if (Strings.isNotEmpty(builder)) {
attribute = builder.toString();
Strings.clearBuilder(builder);
}
}
/** Clears attribute cache, adds default attribute value. */
protected void addAttribute() {
attributes.put(attribute, builder.toString().trim());
attribute = null;
Strings.clearBuilder(builder);
}
/** Adds the stored attribute values to the style sheet. Resolves inherited styles. */
protected void processAttributes() {
for (final String tag : tags) {
for (final String inherit : inherits) {
styleSheet.addStyles(tag, styleSheet.getStyles(inherit));
}
styleSheet.addStyles(tag, attributes);
}
}
/** Analyzes characters, raising the index. Stops after encountering first non-whitespace character. */
protected void burnWhitespaces() {
while (reader.hasNextCharacter()) {
final char character = reader.peekCharacter();
if (Strings.isWhitespace(character) || character == commentMarker && reader.hasNextCharacter(1)
&& reader.peekCharacter(1) == commentSecondary) {
next();
} else {
return;
}
}
}
/** @return next comment in the style sheet, with comments removed. */
protected char next() {
final char next = reader.nextCharacter();
if (next == commentMarker && reader.hasNextCharacter() && reader.peekCharacter() == commentSecondary) {
reader.nextCharacter();
while (reader.hasNextCharacter()) {
final char character = reader.nextCharacter();
if (character == commentSecondary && reader.hasNextCharacter()
&& reader.peekCharacter() == commentMarker) {
reader.nextCharacter();
return reader.hasNextCharacter() ? next() : '\n';
}
}
}
return next;
}
/** Clears control variables. */
public void reset() {
attribute = null;
tags.clear();
inherits.clear();
attributes.clear();
Strings.clearBuilder(builder);
}
}