package dtool.sourcegen; import static dtool.util.NewUtils.emptyToNull; import static melnorme.utilbox.core.Assert.AssertNamespace.assertFail; import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull; import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue; import java.util.ArrayList; import melnorme.utilbox.misc.StringUtil; import dtool.tests.utils.SimpleParser; public class TemplatedSourceProcessorParser { @SuppressWarnings("serial") public static class TemplatedSourceException extends Exception { public int errorOffset; public TemplatedSourceException(int errorOffset) { this.errorOffset = errorOffset; } } protected String kMARKER; protected String[] kMARKER_array; protected ArrayList<TspElement> parseSplitCase(String source, String keyMarker) throws TemplatedSourceException { this.kMARKER = keyMarker; this.kMARKER_array = new String[]{ keyMarker }; ArrayList<TspElement> sourceElements = parseSource(source); return sourceElements; } protected ArrayList<TspElement> parseSource(String unprocessedSource) throws TemplatedSourceException { ArrayList<TspElement> unprocessedSourceElements = new ArrayList<TspElement>(); SimpleParser parser = new SimpleParser(unprocessedSource); while(true) { TspElement tspElem = parseElement(parser); if(tspElem == null) { break; } unprocessedSourceElements.add(tspElem); } return unprocessedSourceElements; } protected static abstract class TspElement { public static String DEFAULT_TYPE = "DEFAULT"; public String getElementType() { return DEFAULT_TYPE; }; public String getSource() { return null; }; } public static class TspStringElement extends TspElement { public static final String RAW_TEXT = "TextElement"; public final String producedText; public final String elemType; protected TspStringElement(String source) { this(source, RAW_TEXT); } protected TspStringElement(String producedText, String elemType) { this.producedText = assertNotNull(producedText); this.elemType = elemType; } @Override public String getElementType() { return elemType; } @Override public String getSource() { return producedText; } @Override public String toString() { return "STRING【" + producedText + "】"; } } protected TspElement parseElement(SimpleParser parser) throws TemplatedSourceException { return parseElementWithCustomStarts(parser, kMARKER_array); } protected TspElement parseElementWithCustomStarts(SimpleParser parser, String... tokenStarts) throws TemplatedSourceException { if(parser.lookaheadIsEOF()) { return null; } int alt = parser.consumeUntilAny(tokenStarts); final String string = parser.getLastConsumedString(); if(!string.isEmpty()) { return new TspStringElement(string); } String tokenStart = tokenStarts[alt]; parser.consume(tokenStart); if(!tokenStart.equals(kMARKER)) { return new TspStringElement(parser.getLastConsumedString(), tokenStart); } if(parser.tryConsume(kMARKER)) { return new TspStringElement(kMARKER); } for (String escapableTokenStart : tokenStarts) { if(parser.tryConsume(escapableTokenStart)) { return new TspStringElement(escapableTokenStart); } } if(parser.lookAhead() == '{' || parser.lookAhead() == '@') { return parseExpansionCommand(parser); } else if(parser.lookAhead() == '?') { return parseIfElseExpansionCommand(parser); } else if(Character.isJavaIdentifierStart(parser.lookAhead())) { return parseMetadataElement(parser); } if(parser.lookAhead() == ':') { return parseCommandElement(parser); } reportError(parser.getSourcePosition()); return null; } protected final void reportError(final int offset) throws TemplatedSourceException { handleParserError(new TemplatedSourceException(offset)); } protected final void checkError(boolean condition, SimpleParser parser) throws TemplatedSourceException { if(condition) { reportError(parser.getSourcePosition()); } } protected void handleParserError(TemplatedSourceException tse) throws TemplatedSourceException { throw tse; } public static class TspExpansionElement extends TspElement { public final String expansionId; public final String pairedExpansionId; public final ArrayList<Argument> arguments; public final boolean dontOuputSource; public final boolean defineOnly; public final boolean anonymousExpansion; public TspExpansionElement(String expansionId, String pairedExpansionId, ArrayList<Argument> arguments, boolean anonymousExpansion, boolean dontOuputSource) { this.expansionId = expansionId; this.pairedExpansionId = pairedExpansionId; this.arguments = arguments; this.anonymousExpansion = anonymousExpansion; this.dontOuputSource = dontOuputSource; this.defineOnly = dontOuputSource; if(defineOnly) { assertTrue(expansionId != null && arguments != null); } } public boolean isDefinition() { return expansionId != null && arguments != null; } @Override public String toString() { return "EXPANSION"+(anonymousExpansion?"^":"")+(dontOuputSource?"!":"")+ "["+ StringUtil.nullAsEmpty(expansionId)+ "]" + (pairedExpansionId == null ? "" : "("+pairedExpansionId+")")+ (arguments == null ? "" : "{"+StringUtil.collToString(arguments, "♦")+"}") ; } } @SuppressWarnings("serial") protected class Argument extends ArrayList<TspElement> { @Override public String toString() { return "["+StringUtil.collToString(this, "")+"]"; } } protected TspElement parseExpansionCommand(SimpleParser parser) throws TemplatedSourceException { assertTrue(parser.lookAhead() == '{' || parser.lookAhead() == '@'); String expansionId = null; boolean defineOnly = false; boolean anonymousExpansion = false; if(parser.tryConsume("@")) { if(parser.tryConsume("^")) { anonymousExpansion = true; } expansionId = emptyToNull(parser.consumeAlphaNumericUS(false)); if(parser.tryConsume("!")) { checkError(expansionId == null, parser); // TODO: No test case for this defineOnly = true; } } if(anonymousExpansion) { checkError(expansionId == null, parser); checkError(defineOnly, parser); } ArrayList<Argument> arguments = null; int alt = parser.tryConsume(OPEN_DELIMS); if(alt != -1) { String closeDelim = CLOSE_DELIMS[alt]; arguments = parseArgumentList(parser, closeDelim); } checkError(defineOnly && arguments == null, parser); checkError(anonymousExpansion && arguments != null, parser); String pairedExpansionId = null; if(parser.tryConsume("(")) { checkError(anonymousExpansion, parser); pairedExpansionId = consumeDelimitedId(parser, ")"); } // This is just an optional separator, useful when expansion only has an id, like: #@EXP•BLA parser.tryConsume("•"); checkError(expansionId == null && arguments == null, parser); return new TspExpansionElement(expansionId, pairedExpansionId, arguments, anonymousExpansion, defineOnly); } protected String consumeDelimitedId(SimpleParser parser, String closeDelim) throws TemplatedSourceException { String pairedExpansionId = emptyToNull(parser.consumeAlphaNumericUS(false)); checkError(pairedExpansionId == null, parser); checkError(parser.tryConsume(closeDelim) == false, parser); return pairedExpansionId; } protected ArrayList<Argument> parseArgumentList(SimpleParser parser, String closeDelim) throws TemplatedSourceException { String argSep = closeDelim.equals("}") ? "," : "●"; return parseArgumentList(parser, argSep, closeDelim, false); } protected ArrayList<Argument> parseArgumentList( SimpleParser parser, final String argSeparator, final String listEnd, boolean eofTerminates ) throws TemplatedSourceException { assertNotNull(listEnd); assertTrue(!eofTerminates || argSeparator == null); final String listEndPrefix = "¤"; final String argStart = "►"; final String[] tokenStarts; if(eofTerminates) { tokenStarts = (argSeparator != null ? new String[] { argStart, listEnd, kMARKER } : new String[] { listEnd, kMARKER }); } else { tokenStarts = argSeparator != null ? new String[] { argStart, argSeparator, listEndPrefix, listEnd, kMARKER } : new String[] { listEndPrefix, listEnd, kMARKER }; } boolean ignoreLastArg = false; boolean argumentStartFound = false; ArrayList<Argument> arguments = new ArrayList<Argument>(); Argument argument = new Argument(); while(true) { TspElement element = parseElementWithCustomStarts(parser, tokenStarts); if(element != null && element.getElementType() == listEnd) { break; } checkError(ignoreLastArg, parser); if(element == null) { checkError(!eofTerminates, parser); break; } if(element.getElementType() == argStart) { checkError(argumentStartFound || !argumentIsWhiteSpaceOnly(argument), parser); argumentStartFound = true; argument = new Argument(); } else if(element.getElementType() == listEndPrefix) { checkError(argumentStartFound || !argumentIsWhiteSpaceOnly(argument), parser); ignoreLastArg = true; argument = null; } else if(element.getElementType() == argSeparator) { arguments.add(argument); argumentStartFound = false; argument = new Argument(); } else { argument.add(element); } } if(ignoreLastArg) { assertTrue(argument == null); } else { arguments.add(assertNotNull(argument)); } return arguments; } protected boolean argumentIsWhiteSpaceOnly(Argument argument) { if(argument.size() != 0) { if(argument.size() == 1) { String argSource = argument.get(0).getSource(); return(argSource != null && argSource.trim().isEmpty()); } return false; } return true; } public static class TspMetadataElement extends TspElement { public final String tag; public final String value; public final Argument childElements; public final boolean outputSource; public TspMetadataElement(String tag, String value, Argument childElements, boolean outputSource) { this.tag = tag; this.value = value; this.childElements = childElements; this.outputSource = outputSource; } } public static final String[] OPEN_DELIMS = {"{","«","〈","《","「","『","【","〔","〖","〚" }; public static final String[] CLOSE_DELIMS = {"}","»","〉","》","」","』","】","〕","〗","〛"} ; protected TspMetadataElement parseMetadataElement(SimpleParser parser) throws TemplatedSourceException { String name = parser.consumeAlphaNumericUS(false); assertTrue(!name.isEmpty()); String value = parser.tryConsume("(") ? consumeDelimitedString(parser, ")", false) : null; Argument sourceValue = null; boolean colonSyntaxConsumed = false; if(value == null && parser.lookAhead() == ':') { String id = SimpleParser.readAlphaNumericUS(parser.getSource(), parser.getSourcePosition()+1); if(id.length() != 0) { value = id; parser.consumeAmount(id.length() + 1); colonSyntaxConsumed = true; } } boolean outputSource = parser.tryConsume("¤") == false; int alt = parser.tryConsume(OPEN_DELIMS); if(alt != -1) { String closeDelim = CLOSE_DELIMS[alt]; sourceValue = parseArgument(parser, closeDelim, false); } else if(colonSyntaxConsumed == false && parser.tryConsume(":")) { //checkError(parser.tryConsumeNewlineRule() == false, parser); parser.tryConsumeNewlineRule(); sourceValue = parseArgument(parser, "#:END:", true); outputSource = false; } checkError(outputSource == false && sourceValue == null, parser); return new TspMetadataElement(name, value, sourceValue, outputSource); } protected Argument parseArgument(SimpleParser parser, String listEnd, boolean eofTerminates) throws TemplatedSourceException { ArrayList<Argument> argumentList = parseArgumentList(parser, null, listEnd, eofTerminates); assertTrue(argumentList.size() == 1); return argumentList.get(0); } protected String consumeDelimitedString(SimpleParser parser, String closeSep, boolean eofTerminates) throws TemplatedSourceException { StringBuilder value = new StringBuilder(); final String[] alts = new String[]{closeSep, kMARKER}; while(true) { int alt = parser.consumeUntilAny(alts); value.append(parser.getLastConsumedString()); if(parser.lookaheadIsEOF()) { checkError(!eofTerminates, parser); // Unterminated break; } else if(alt == 0) { parser.consume(closeSep); break; } else if(alt == 1) { parser.consume(kMARKER); if(parser.tryConsume("#")) { value.append("#"); } else if(parser.tryConsume(closeSep)) { value.append(closeSep); } else { reportError(parser.getSourcePosition()); // Invalid Escape } } else { assertFail(); } } return value.toString(); } protected static class TspCommandElement extends TspElement { public static final String DISCARD_CASE = "DISCARD_CASE"; public final String name; public TspCommandElement(String name) { this.name = name; } } protected TspCommandElement parseCommandElement(SimpleParser parser) throws TemplatedSourceException { parser.consume(":"); String name = parser.consumeAlphaNumericUS(false); checkError(name.isEmpty(), parser); checkError(!name.equals(TspCommandElement.DISCARD_CASE), parser); return new TspCommandElement(name); } protected TspIfElseExpansionElement parseIfElseExpansionCommand(SimpleParser parser) throws TemplatedSourceException { parser.consume("?"); String mdConditionId = emptyToNull(parser.consumeAlphaNumericUS(false)); checkError(mdConditionId == null, parser); boolean invert = parser.tryConsume("!"); ArrayList<Argument> arguments = null; int alt = parser.tryConsume(OPEN_DELIMS); if(alt == -1) { reportError(parser.getSourcePosition()); } else { arguments = parseArgumentList(parser, CLOSE_DELIMS[alt]); } checkError(arguments.size() > 2, parser); Argument argElse = arguments.size() == 1 ? null: arguments.get(1); return new TspIfElseExpansionElement(mdConditionId, invert, arguments.get(0), argElse); } public static class TspIfElseExpansionElement extends TspElement { public final String mdConditionId; public final boolean invert; public final Argument argThen; public final Argument argElse; public TspIfElseExpansionElement(String mdConditionId, boolean invert, Argument argThen, Argument argElse) { this.mdConditionId = mdConditionId; this.invert = invert; this.argThen = argThen; this.argElse = argElse; } @Override public String toString() { return "IF?【"+ StringUtil.nullAsEmpty(mdConditionId)+"{"+argThen+","+argElse+"}"+"】"; } } }