Import all classes
This commit is contained in:
parent
cfc0c813ab
commit
160a82adb7
18 changed files with 1296 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
|
target/
|
||||||
|
|
|
||||||
15
pom.xml
15
pom.xml
|
|
@ -8,4 +8,19 @@
|
||||||
<artifactId>composite-parse</artifactId>
|
<artifactId>composite-parse</artifactId>
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
<configuration>
|
||||||
|
<release>11</release>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
|
||||||
140
src/main/java/de/plugh/compositeparse/Block.java
Normal file
140
src/main/java/de/plugh/compositeparse/Block.java
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
package de.plugh.compositeparse;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Block} represents a single {@link Parser#parse(Block)} call. It helps implement back tracking and useful
|
||||||
|
* error messages.
|
||||||
|
* <p>
|
||||||
|
* Whenever a {@link Parser}'s parse function is called, it creates a new {@link Block} and passes it along to all parse
|
||||||
|
* calls of its sub-parsers. It also registers the {@link Block} it created with its super-parser's {@link Block}.
|
||||||
|
* <p>
|
||||||
|
* Each {@link Block} additionally saves the input's cursor position when it was created. This way, backtracking can be
|
||||||
|
* achieved by walking up the {@link Block} tree and setting the input's cursor position to a higher {@link Block}'s
|
||||||
|
* saved cursor position.
|
||||||
|
* <p>
|
||||||
|
* In addition to that, each {@link Block} remembers a naming scheme that operates on the {@link Block}'s sub-blocks.
|
||||||
|
* This naming scheme is used to create a useful error message when a {@link ParseException} is thrown.
|
||||||
|
*/
|
||||||
|
public class Block {
|
||||||
|
|
||||||
|
private static final int CONTEXT_LOOKBACK = 24;
|
||||||
|
private final int initialCursor;
|
||||||
|
private List<Block> subblocks;
|
||||||
|
private Function<List<Block>, String> namingScheme;
|
||||||
|
private StringInput input;
|
||||||
|
|
||||||
|
private Block(Function<List<Block>, String> namingScheme, StringInput input) {
|
||||||
|
subblocks = new ArrayList<>();
|
||||||
|
this.namingScheme = namingScheme;
|
||||||
|
|
||||||
|
this.input = input;
|
||||||
|
initialCursor = input.getCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a top-level block from an input {@link String}.
|
||||||
|
*
|
||||||
|
* @param text the input {@link String}
|
||||||
|
*/
|
||||||
|
public Block(String text) {
|
||||||
|
this(Block::alternative, new StringInput(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new block as a sub-block of an existing block.
|
||||||
|
*
|
||||||
|
* @param superblock the block that this block is a child to
|
||||||
|
* @param namingScheme the naming scheme to use for {@link #getName()}
|
||||||
|
*/
|
||||||
|
Block(Block superblock, Function<List<Block>, String> namingScheme) {
|
||||||
|
this(namingScheme, superblock.input);
|
||||||
|
|
||||||
|
superblock.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Naming schemes
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the name of the first sub-block (useful for sequential blocks).
|
||||||
|
*
|
||||||
|
* @param blocks a block's sub-blocks
|
||||||
|
* @return the first sub-block's name
|
||||||
|
*/
|
||||||
|
public static String first(List<Block> blocks) {
|
||||||
|
if (blocks.size() > 0) {
|
||||||
|
return blocks.get(0).getName();
|
||||||
|
} else {
|
||||||
|
throw new BlockException("No subblocks found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UCombine all sub-blocks' names using "or" (useful for optional parsers).
|
||||||
|
*
|
||||||
|
* @param blocks a block's sub-blocks
|
||||||
|
* @return all sub-blocks' names, joined with "or"
|
||||||
|
*/
|
||||||
|
public static String alternative(List<Block> blocks) {
|
||||||
|
if (blocks.size() > 0) {
|
||||||
|
return blocks.stream().map(Block::getName).collect(Collectors.joining(" or "));
|
||||||
|
} else {
|
||||||
|
throw new BlockException("No subblocks found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always return a constant name.
|
||||||
|
*
|
||||||
|
* @param name a block's sub-blocks
|
||||||
|
* @return the name
|
||||||
|
*/
|
||||||
|
public static Function<List<Block>, String> label(String name) {
|
||||||
|
return ignored -> name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void register(Block subblock) {
|
||||||
|
subblocks.add(subblock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the input {@link StringInput}
|
||||||
|
*/
|
||||||
|
public StringInput getInput() {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the name
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return namingScheme.apply(subblocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the input {@link StringInput}'s cursor to this block's initial cursor position
|
||||||
|
*/
|
||||||
|
public void resetCursor() {
|
||||||
|
input.setCursor(initialCursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a few characters from before this block's initial cursor position
|
||||||
|
*/
|
||||||
|
public String getContext() {
|
||||||
|
int currentCursor = input.getCursor();
|
||||||
|
|
||||||
|
input.setCursor(initialCursor);
|
||||||
|
String context = input.look(-CONTEXT_LOOKBACK);
|
||||||
|
|
||||||
|
input.setCursor(currentCursor);
|
||||||
|
|
||||||
|
return "..." + context;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
18
src/main/java/de/plugh/compositeparse/BlockException.java
Normal file
18
src/main/java/de/plugh/compositeparse/BlockException.java
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package de.plugh.compositeparse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link BlockException} is thrown when actions are executed on a malformed {@link Block} structure.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("serial") // This exception does not need to be serialised.
|
||||||
|
public class BlockException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link BlockException}.
|
||||||
|
*
|
||||||
|
* @param reason what went wrong
|
||||||
|
*/
|
||||||
|
public BlockException(String reason) {
|
||||||
|
super(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
64
src/main/java/de/plugh/compositeparse/Pair.java
Normal file
64
src/main/java/de/plugh/compositeparse/Pair.java
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package de.plugh.compositeparse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Pair} is an immutable class representing a tuple with two elements.
|
||||||
|
*
|
||||||
|
* @param <A> type of the first element
|
||||||
|
* @param <B> type of the second element
|
||||||
|
*/
|
||||||
|
public class Pair<A, B> {
|
||||||
|
|
||||||
|
private final A first;
|
||||||
|
private final B second;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Pair} from two elements.
|
||||||
|
*
|
||||||
|
* @param first the first element
|
||||||
|
* @param second the second element
|
||||||
|
*/
|
||||||
|
public Pair(A first, B second) {
|
||||||
|
this.first = first;
|
||||||
|
this.second = second;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the first element
|
||||||
|
*/
|
||||||
|
public A getFirst() {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the second element
|
||||||
|
*/
|
||||||
|
public B getSecond() {
|
||||||
|
return second;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
// Auto-generated by eclipse
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + ((first == null) ? 0 : first.hashCode());
|
||||||
|
result = prime * result + ((second == null) ? 0 : second.hashCode());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
// Auto-generated by eclipse, with small changes
|
||||||
|
if (this == obj) return true;
|
||||||
|
if (obj == null) return false;
|
||||||
|
if (getClass() != obj.getClass()) return false;
|
||||||
|
Pair<?, ?> other = (Pair<?, ?>) obj;
|
||||||
|
if (first == null) {
|
||||||
|
if (other.first != null) return false;
|
||||||
|
} else if (!first.equals(other.first)) return false;
|
||||||
|
if (second == null) {
|
||||||
|
return other.second == null;
|
||||||
|
} else return second.equals(other.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
46
src/main/java/de/plugh/compositeparse/ParseException.java
Normal file
46
src/main/java/de/plugh/compositeparse/ParseException.java
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package de.plugh.compositeparse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exception is thrown when a parser encounters incorrect input. It contains a bit of information about the
|
||||||
|
* failure:
|
||||||
|
* <p>
|
||||||
|
* The name of the parser that failed.
|
||||||
|
* <p>
|
||||||
|
* A few characters of context, ending at the position where the parser failed.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("serial") // This exception does not need to be serialised.
|
||||||
|
public class ParseException extends Exception {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final String context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link ParseException} at a block.
|
||||||
|
*
|
||||||
|
* @param block the block to take the extra information from
|
||||||
|
*/
|
||||||
|
public ParseException(Block block) {
|
||||||
|
name = block.getName();
|
||||||
|
context = block.getContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the name
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the context
|
||||||
|
*/
|
||||||
|
public String getContext() {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return getContext() + "<- expected: " + getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
111
src/main/java/de/plugh/compositeparse/Parser.java
Normal file
111
src/main/java/de/plugh/compositeparse/Parser.java
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package de.plugh.compositeparse;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* THIS LIBRARY
|
||||||
|
*
|
||||||
|
* This parsing system was inspired by haskell's megaparsec library and aims to
|
||||||
|
* somewhat recreate the feel and flexibility of megaparsec in java.
|
||||||
|
*
|
||||||
|
* The main concept of this library is that parsers can be combined into bigger
|
||||||
|
* parsers through either sequential parsing or by passing parsers into
|
||||||
|
* constructors of other parsers. This happens during parsing and thus can
|
||||||
|
* depend on previously parsed input and/or the current state of the program.
|
||||||
|
*
|
||||||
|
* For combining Parsers to work properly, all Parsers are immutable.
|
||||||
|
*
|
||||||
|
* BLOCKS
|
||||||
|
*
|
||||||
|
* While parsing, the library builds up a structure of Blocks representing the
|
||||||
|
* structure of the Parsers which have already been tried. This structure serves
|
||||||
|
* a dual purpose:
|
||||||
|
*
|
||||||
|
* 1) Each block stores the StringReader's cursor position when it is created.
|
||||||
|
* This allows for the Parsers to backtrack, should a branch fail.
|
||||||
|
*
|
||||||
|
* 2) Each block holds sub-blocks created by the parsers it consists of. When a
|
||||||
|
* ParseException is thrown, this information is used to figure out which syntax
|
||||||
|
* was expected at the point of failure. This allows for descriptive error
|
||||||
|
* messages which can be very useful in an interactive environment.
|
||||||
|
*
|
||||||
|
* A structure like this could not be constructed at compile time or cached,
|
||||||
|
* because it depends on the input that is being parsed: Depending on the input
|
||||||
|
* already parsed, a parser can decide to use different subparsers. Because of
|
||||||
|
* this, the structure is created while parsing is occurring.
|
||||||
|
*
|
||||||
|
* For more info, see the documentation for Block.
|
||||||
|
*
|
||||||
|
* COMBINING PARSERS
|
||||||
|
*
|
||||||
|
* The main method of combining parsers is by sequentially calling them one
|
||||||
|
* after the other. This is also the easiest way to collect results from the
|
||||||
|
* parsers. Loops and conditionals can also be used, as can previously parsed
|
||||||
|
* input.
|
||||||
|
*
|
||||||
|
* In some situations, there are multiple possible "branches" a parser could
|
||||||
|
* take. In those cases, the Options parser can try multiple different parsers,
|
||||||
|
* backtracking when one of them fails to try the next one.
|
||||||
|
*
|
||||||
|
* The Default parser can provide a default value in case a parser fails.
|
||||||
|
*
|
||||||
|
* The Repeat parser can repeat a parser a certain amount of times, with an
|
||||||
|
* optional separator parser in-between.
|
||||||
|
*
|
||||||
|
* One can also manually catch the ParseExceptions. In that case, the cursor
|
||||||
|
* position is reset (as if the parser that threw an exception never parsed any
|
||||||
|
* input) and another Parser can be used.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Parser} knows how to parse a specific bit of information.
|
||||||
|
* <p>
|
||||||
|
* {@link Parser}s are usually created by combining multiple smaller parsers. For more information, see the introductory
|
||||||
|
* comment in the source file of this class.
|
||||||
|
*
|
||||||
|
* @param <T> return type of the parser
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Parser<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the parser's naming scheme
|
||||||
|
*/
|
||||||
|
default Function<List<Block>, String> getNamingScheme() {
|
||||||
|
return Block::first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a specific bit of information from the input.
|
||||||
|
* <p>
|
||||||
|
* <b>Do not overwrite this function unless you know what you're doing!</b>
|
||||||
|
* <p>
|
||||||
|
* <i>This is the function you usually want to call.</i>
|
||||||
|
*
|
||||||
|
* @param block the calling parser's {@link Block}
|
||||||
|
* @return the information it parsed
|
||||||
|
* @throws ParseException if the input format was incorrect
|
||||||
|
*/
|
||||||
|
default T parse(Block block) throws ParseException {
|
||||||
|
Block subblock = new Block(block, getNamingScheme());
|
||||||
|
try {
|
||||||
|
return read(subblock);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
subblock.resetCursor();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The implementation regarding how to parse the specific bit of information.
|
||||||
|
* <p>
|
||||||
|
* <i>This is the function you usually want to overwrite.</i>
|
||||||
|
*
|
||||||
|
* @param block the calling parser's {@link Block}
|
||||||
|
* @return the information it parsed
|
||||||
|
* @throws ParseException if the input format was incorrect
|
||||||
|
*/
|
||||||
|
T read(Block block) throws ParseException;
|
||||||
|
|
||||||
|
}
|
||||||
128
src/main/java/de/plugh/compositeparse/StringInput.java
Normal file
128
src/main/java/de/plugh/compositeparse/StringInput.java
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
package de.plugh.compositeparse;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link StringInput} consists of a {@link String} and a cursor position on that {@link String}.
|
||||||
|
* <p>
|
||||||
|
* It provides a convenient way to view a {@link String}, in addition to a few useful functions.
|
||||||
|
*/
|
||||||
|
public class StringInput {
|
||||||
|
|
||||||
|
private String string;
|
||||||
|
private int cursor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link StringInput} over a {@link String}.
|
||||||
|
*
|
||||||
|
* @param string the content of the reader
|
||||||
|
*/
|
||||||
|
public StringInput(String string) {
|
||||||
|
this.string = string;
|
||||||
|
this.cursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int clampCursor(int position, int delta) {
|
||||||
|
/*
|
||||||
|
* A cursor can have position string.length() because its position is
|
||||||
|
* interpreted as between the characters, not on the characters, similar to
|
||||||
|
* python's slicing.
|
||||||
|
*
|
||||||
|
* Examples, using "|" as the cursor position and "aabc" as the string:
|
||||||
|
*
|
||||||
|
* |aabc - The cursor is in position 0.
|
||||||
|
*
|
||||||
|
* aab|c - The cursor is in position 3.
|
||||||
|
*
|
||||||
|
* aabc| - The cursor is in position 4.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This prevents an overflow/underflow if somebody tries to look(), read() or
|
||||||
|
* move() with Integer.MIN_VALUE or Integer.MAX_VALUE (like I did while testing
|
||||||
|
* this).
|
||||||
|
*/
|
||||||
|
int minDelta = -position;
|
||||||
|
int maxDelta = string.length() - position;
|
||||||
|
return position + Math.max(minDelta, Math.min(maxDelta, delta));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the cursor position
|
||||||
|
*/
|
||||||
|
public int getCursor() {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param cursor the cursor position
|
||||||
|
*/
|
||||||
|
public void setCursor(int cursor) {
|
||||||
|
this.cursor = clampCursor(cursor, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor a certain amount of characters relative to the cursor's current position. A positive amount moves
|
||||||
|
* forward (towards the end of the string), a negative moves backward (towards the beginning of the string).
|
||||||
|
*
|
||||||
|
* @param amount how many characters to move the cursor by
|
||||||
|
*/
|
||||||
|
public void move(int amount) {
|
||||||
|
setCursor(clampCursor(getCursor(), amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a certain amount of characters relative to the cursor's current position. A positive amount looks forward
|
||||||
|
* (towards the end of the string), a negative looks backward (towards the beginning of the string).
|
||||||
|
*
|
||||||
|
* @param amount how many characters to look up
|
||||||
|
* @return the specified section of the string
|
||||||
|
*/
|
||||||
|
public String look(int amount) {
|
||||||
|
if (amount >= 0) {
|
||||||
|
return string.substring(cursor, clampCursor(cursor, amount));
|
||||||
|
} else {
|
||||||
|
return string.substring(clampCursor(cursor, amount), cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines a {@link #look(int)} and a {@link #move(int)} operation.
|
||||||
|
*
|
||||||
|
* @param amount how many characters to look up and move
|
||||||
|
* @return the specified section of the string
|
||||||
|
*/
|
||||||
|
public String read(int amount) {
|
||||||
|
String result = look(amount);
|
||||||
|
move(amount);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match and {@link #read(int)} the regex passed, starting at the current cursor position.
|
||||||
|
* <p>
|
||||||
|
* This returns everything from the current cursor position to the end of the match that was found, so make sure to
|
||||||
|
* anchor your regexes (using ^) unless you need all of that.
|
||||||
|
*
|
||||||
|
* @param regex the regular expression to use
|
||||||
|
* @return the string matched (or null, if no match was found)
|
||||||
|
*/
|
||||||
|
public String match(String regex) {
|
||||||
|
Pattern pattern = Pattern.compile(regex);
|
||||||
|
Matcher matcher = pattern.matcher(string.substring(cursor));
|
||||||
|
if (matcher.find()) {
|
||||||
|
return read(matcher.end());
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return whether the whole input was consumed
|
||||||
|
*/
|
||||||
|
public boolean complete() {
|
||||||
|
return cursor >= string.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
package de.plugh.compositeparse.parsers;
|
||||||
|
|
||||||
|
import de.plugh.compositeparse.Block;
|
||||||
|
import de.plugh.compositeparse.ParseException;
|
||||||
|
import de.plugh.compositeparse.Parser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an integer between a lower and upper bound.
|
||||||
|
*/
|
||||||
|
public class BoundedInteger implements Parser<Integer> {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @formatter:off
|
||||||
|
*
|
||||||
|
* Regex breakdown:
|
||||||
|
*
|
||||||
|
* ^ - Start at the cursor's current position
|
||||||
|
* [+-]? - Optionally match a sign in the beginning of the string
|
||||||
|
* \\d+ - Match as many digits as you can find, at least one
|
||||||
|
*
|
||||||
|
* @formatter:on
|
||||||
|
*/
|
||||||
|
private static final String INTEGER_REGEX = "^[+-]?\\d+";
|
||||||
|
|
||||||
|
private final int min;
|
||||||
|
private final int max;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an integer between min and max. The integer is of the format {@code [+-]<digits>}.
|
||||||
|
*
|
||||||
|
* @param min minimum value of the integer
|
||||||
|
* @param max maximum value of the integer
|
||||||
|
* @see #between(int, int)
|
||||||
|
*/
|
||||||
|
public BoundedInteger(int min, int max) {
|
||||||
|
this.min = min;
|
||||||
|
this.max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an integer. The integer is of the format {@code [+-]<digits>}.
|
||||||
|
*/
|
||||||
|
public BoundedInteger() {
|
||||||
|
this(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an integer >= min.
|
||||||
|
*
|
||||||
|
* @param min minimun size of the integer
|
||||||
|
* @return the {@link BoundedInteger}
|
||||||
|
*/
|
||||||
|
public static BoundedInteger atLeast(int min) {
|
||||||
|
return new BoundedInteger(min, Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an integer <= max.
|
||||||
|
*
|
||||||
|
* @param max maximum size of the integer
|
||||||
|
* @return the {@link BoundedInteger}
|
||||||
|
*/
|
||||||
|
public static BoundedInteger atMost(int max) {
|
||||||
|
return new BoundedInteger(Integer.MIN_VALUE, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an integer with min <= integer <= max.
|
||||||
|
*
|
||||||
|
* @param min minimun size of the integer
|
||||||
|
* @param max maximum size of the integer
|
||||||
|
* @return the {@link BoundedInteger}
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static BoundedInteger between(int min, int max) {
|
||||||
|
return new BoundedInteger(min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Function<List<Block>, String> getNamingScheme() {
|
||||||
|
String description = "integer";
|
||||||
|
|
||||||
|
if (min > Integer.MIN_VALUE && max < Integer.MAX_VALUE) {
|
||||||
|
description += " (between " + min + " and " + max + ")";
|
||||||
|
} else if (min > Integer.MIN_VALUE) {
|
||||||
|
description += " (at least " + min + ")";
|
||||||
|
} else if (max < Integer.MAX_VALUE) {
|
||||||
|
description += " (at most " + max + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Block.label(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer read(Block block) throws ParseException {
|
||||||
|
String integerString;
|
||||||
|
try {
|
||||||
|
integerString = new Expression(INTEGER_REGEX).parse(block);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
// Mask the regex parse exception with our own (the error messages are better
|
||||||
|
// this way)
|
||||||
|
throw new ParseException(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
int integer;
|
||||||
|
try {
|
||||||
|
integer = Integer.parseInt(integerString);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new ParseException(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integer < min || integer > max) {
|
||||||
|
throw new ParseException(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
return integer;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
38
src/main/java/de/plugh/compositeparse/parsers/Constant.java
Normal file
38
src/main/java/de/plugh/compositeparse/parsers/Constant.java
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package de.plugh.compositeparse.parsers;
|
||||||
|
|
||||||
|
import de.plugh.compositeparse.Block;
|
||||||
|
import de.plugh.compositeparse.ParseException;
|
||||||
|
import de.plugh.compositeparse.Parser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes no input and returns a constant value.
|
||||||
|
*
|
||||||
|
* @param <T> return type of the parser
|
||||||
|
*/
|
||||||
|
public class Constant<T> implements Parser<T> {
|
||||||
|
|
||||||
|
private T value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Constant} parser.
|
||||||
|
*
|
||||||
|
* @param value the value to return
|
||||||
|
*/
|
||||||
|
public Constant(T value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Function<List<Block>, String> getNamingScheme() {
|
||||||
|
return Block.label("constant");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T read(Block block) throws ParseException {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
66
src/main/java/de/plugh/compositeparse/parsers/Decision.java
Normal file
66
src/main/java/de/plugh/compositeparse/parsers/Decision.java
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
package de.plugh.compositeparse.parsers;
|
||||||
|
|
||||||
|
import de.plugh.compositeparse.Block;
|
||||||
|
import de.plugh.compositeparse.Pair;
|
||||||
|
import de.plugh.compositeparse.ParseException;
|
||||||
|
import de.plugh.compositeparse.Parser;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide which parser to use from a list of "body" parsers and their "head"s.
|
||||||
|
* <p>
|
||||||
|
* If a "head" parses successfully, the corresponding "body" parser must be successful, otherwise a parse exception is
|
||||||
|
* raised. If no "head" is successful, the {@link Decision} parser fails too.
|
||||||
|
*
|
||||||
|
* @param <T> return type of the parser
|
||||||
|
*/
|
||||||
|
public class Decision<T> implements Parser<T> {
|
||||||
|
|
||||||
|
private final List<Pair<Parser<?>, Parser<T>>> pairs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Decision} parser from all passed arguments, which are "head"-"body" pairs.
|
||||||
|
*
|
||||||
|
* @param pairs multiple "head"-"body" pairs
|
||||||
|
*/
|
||||||
|
@SafeVarargs
|
||||||
|
public Decision(Pair<Parser<?>, Parser<T>>... pairs) {
|
||||||
|
this.pairs = new ArrayList<>();
|
||||||
|
for (Pair<Parser<?>, Parser<T>> pair : pairs) {
|
||||||
|
this.pairs.add(pair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Decision} parser from a list of "head"-"body" pairs.
|
||||||
|
*
|
||||||
|
* @param pairs a list of "head"-"body" pairs
|
||||||
|
*/
|
||||||
|
public Decision(List<Pair<Parser<?>, Parser<T>>> pairs) {
|
||||||
|
this.pairs = new ArrayList<>(pairs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Function<List<Block>, String> getNamingScheme() {
|
||||||
|
return Block::alternative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T read(Block block) throws ParseException {
|
||||||
|
for (Pair<Parser<?>, Parser<T>> pair : pairs) {
|
||||||
|
try {
|
||||||
|
pair.getFirst().parse(block);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pair.getSecond().parse(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ParseException(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
57
src/main/java/de/plugh/compositeparse/parsers/Default.java
Normal file
57
src/main/java/de/plugh/compositeparse/parsers/Default.java
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package de.plugh.compositeparse.parsers;
|
||||||
|
|
||||||
|
import de.plugh.compositeparse.Block;
|
||||||
|
import de.plugh.compositeparse.ParseException;
|
||||||
|
import de.plugh.compositeparse.Parser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try a parser and return its value, or a default value if the parser fails.
|
||||||
|
* <p>
|
||||||
|
* This parser will never throw a {@link ParseException}.
|
||||||
|
*
|
||||||
|
* @param <T> return type of the parser
|
||||||
|
*/
|
||||||
|
public class Default<T> implements Parser<T> {
|
||||||
|
|
||||||
|
private final T value;
|
||||||
|
private final Parser<T> parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Default} parser.
|
||||||
|
*
|
||||||
|
* @param value the value to return
|
||||||
|
* @param parser the parser to try
|
||||||
|
*/
|
||||||
|
public Default(T value, Parser<T> parser) {
|
||||||
|
this.value = value;
|
||||||
|
this.parser = parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Default} parser.
|
||||||
|
*
|
||||||
|
* @param parser the parser to try
|
||||||
|
*/
|
||||||
|
public Default(Parser<T> parser) {
|
||||||
|
this(null, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Function<List<Block>, String> getNamingScheme() {
|
||||||
|
// There is always a block 0 because of Default's read() implementation.
|
||||||
|
return blocks -> blocks.get(0).getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T read(Block block) {
|
||||||
|
try {
|
||||||
|
return parser.parse(block);
|
||||||
|
} catch (ParseException ignored) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package de.plugh.compositeparse.parsers;
|
||||||
|
|
||||||
|
import de.plugh.compositeparse.Block;
|
||||||
|
import de.plugh.compositeparse.ParseException;
|
||||||
|
import de.plugh.compositeparse.Parser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a value when the end of the input has been reached, fails otherwise.
|
||||||
|
*
|
||||||
|
* @param <T> return type of the parser
|
||||||
|
*/
|
||||||
|
public class EndOfInput<T> implements Parser<T> {
|
||||||
|
|
||||||
|
private final T value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link EndOfInput} parser.
|
||||||
|
*
|
||||||
|
* @param value the value to return
|
||||||
|
*/
|
||||||
|
public EndOfInput(T value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link EndOfInput} parser that always returns {@code null}.
|
||||||
|
*/
|
||||||
|
public EndOfInput() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Function<List<Block>, String> getNamingScheme() {
|
||||||
|
return Block.label("end of input");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T read(Block block) throws ParseException {
|
||||||
|
if (block.getInput().complete()) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
throw new ParseException(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package de.plugh.compositeparse.parsers;
|
||||||
|
|
||||||
|
import de.plugh.compositeparse.Block;
|
||||||
|
import de.plugh.compositeparse.ParseException;
|
||||||
|
import de.plugh.compositeparse.Parser;
|
||||||
|
import de.plugh.compositeparse.StringInput;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a regular expression from the input.
|
||||||
|
*/
|
||||||
|
public class Expression implements Parser<String> {
|
||||||
|
|
||||||
|
private final String regex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Expression} parser.
|
||||||
|
*
|
||||||
|
* @param regex the regular expression to use
|
||||||
|
* @see StringInput#match(String)
|
||||||
|
*/
|
||||||
|
public Expression(String regex) {
|
||||||
|
this.regex = regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Function<List<Block>, String> getNamingScheme() {
|
||||||
|
return Block.label("regex \"" + regex + "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String read(Block block) throws ParseException {
|
||||||
|
StringInput input = block.getInput();
|
||||||
|
String result = input.match(regex);
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
throw new ParseException(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
43
src/main/java/de/plugh/compositeparse/parsers/Label.java
Normal file
43
src/main/java/de/plugh/compositeparse/parsers/Label.java
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package de.plugh.compositeparse.parsers;
|
||||||
|
|
||||||
|
import de.plugh.compositeparse.Block;
|
||||||
|
import de.plugh.compositeparse.ParseException;
|
||||||
|
import de.plugh.compositeparse.Parser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches a name to a parser.
|
||||||
|
* <p>
|
||||||
|
* This can be useful for naming lambda parsers, or renaming existing parsers.
|
||||||
|
*
|
||||||
|
* @param <T> return type of the parser
|
||||||
|
*/
|
||||||
|
public class Label<T> implements Parser<T> {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final Parser<T> parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Label} parser.
|
||||||
|
*
|
||||||
|
* @param name the parser's new name
|
||||||
|
* @param parser the parser to rename
|
||||||
|
*/
|
||||||
|
public Label(String name, Parser<T> parser) {
|
||||||
|
this.name = name;
|
||||||
|
this.parser = parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Function<List<Block>, String> getNamingScheme() {
|
||||||
|
return Block.label(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T read(Block block) throws ParseException {
|
||||||
|
return parser.parse(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
85
src/main/java/de/plugh/compositeparse/parsers/Literal.java
Normal file
85
src/main/java/de/plugh/compositeparse/parsers/Literal.java
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package de.plugh.compositeparse.parsers;
|
||||||
|
|
||||||
|
import de.plugh.compositeparse.Block;
|
||||||
|
import de.plugh.compositeparse.ParseException;
|
||||||
|
import de.plugh.compositeparse.Parser;
|
||||||
|
import de.plugh.compositeparse.StringInput;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a string literal from the input (case sensitive).
|
||||||
|
* <p>
|
||||||
|
* For more flexible input, see {@link Expression}.
|
||||||
|
*
|
||||||
|
* @param <T> return type of the parser
|
||||||
|
*/
|
||||||
|
public class Literal<T> implements Parser<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single space {@code " "}
|
||||||
|
*/
|
||||||
|
public static final Literal<Void> SPACE = new Literal<>(" ");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single comma {@code ","}
|
||||||
|
*/
|
||||||
|
public static final Literal<Void> COMMA = new Literal<>(",");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single semicolon {@code ";"}
|
||||||
|
*/
|
||||||
|
public static final Literal<Void> SEMICOLON = new Literal<>(";");
|
||||||
|
|
||||||
|
private final String literal;
|
||||||
|
private final T value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume a string literal from the input and return a value if successful.
|
||||||
|
*
|
||||||
|
* @param literal the literal to consume from the input
|
||||||
|
* @param value the value to return
|
||||||
|
*/
|
||||||
|
public Literal(String literal, T value) {
|
||||||
|
this.literal = literal;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume a string literal from the input
|
||||||
|
*
|
||||||
|
* @param literal the literal to consume from the input
|
||||||
|
*/
|
||||||
|
public Literal(String literal) {
|
||||||
|
this(literal, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a {@link Literal} that returns (literally) the literal it consumes.
|
||||||
|
* <p>
|
||||||
|
* Shorthand for <code>new Literal(literal, literal)</code>
|
||||||
|
*
|
||||||
|
* @param literal the literal to consume from the input
|
||||||
|
* @return the {@link Literal}
|
||||||
|
*/
|
||||||
|
public static Literal<String> literally(String literal) {
|
||||||
|
return new Literal<>(literal, literal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Function<List<Block>, String> getNamingScheme() {
|
||||||
|
return Block.label("\"" + literal + "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T read(Block block) throws ParseException {
|
||||||
|
StringInput input = block.getInput();
|
||||||
|
if (input.read(literal.length()).equals(literal)) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
throw new ParseException(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
59
src/main/java/de/plugh/compositeparse/parsers/Options.java
Normal file
59
src/main/java/de/plugh/compositeparse/parsers/Options.java
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package de.plugh.compositeparse.parsers;
|
||||||
|
|
||||||
|
import de.plugh.compositeparse.Block;
|
||||||
|
import de.plugh.compositeparse.ParseException;
|
||||||
|
import de.plugh.compositeparse.Parser;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try a few parsers in order (backtracking if a parser fails) and return the result of the first successful parser.
|
||||||
|
*
|
||||||
|
* @param <T> return type of the parser
|
||||||
|
*/
|
||||||
|
public class Options<T> implements Parser<T> {
|
||||||
|
|
||||||
|
private final List<Parser<T>> parsers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Options} from all passed parsers.
|
||||||
|
*
|
||||||
|
* @param parsers the parsers to try.
|
||||||
|
*/
|
||||||
|
@SafeVarargs
|
||||||
|
public Options(Parser<T>... parsers) {
|
||||||
|
this.parsers = new ArrayList<>();
|
||||||
|
for (Parser<T> parser : parsers) {
|
||||||
|
this.parsers.add(parser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Options} from a list of parsers.
|
||||||
|
*
|
||||||
|
* @param parsers the parsers to try.
|
||||||
|
*/
|
||||||
|
public Options(List<Parser<T>> parsers) {
|
||||||
|
this.parsers = new ArrayList<>(parsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Function<List<Block>, String> getNamingScheme() {
|
||||||
|
return Block::alternative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T read(Block block) throws ParseException {
|
||||||
|
for (Parser<T> parser : parsers) {
|
||||||
|
try {
|
||||||
|
return parser.parse(block);
|
||||||
|
} catch (ParseException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ParseException(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
209
src/main/java/de/plugh/compositeparse/parsers/Repeat.java
Normal file
209
src/main/java/de/plugh/compositeparse/parsers/Repeat.java
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
package de.plugh.compositeparse.parsers;
|
||||||
|
|
||||||
|
import de.plugh.compositeparse.Block;
|
||||||
|
import de.plugh.compositeparse.ParseException;
|
||||||
|
import de.plugh.compositeparse.Parser;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repeats a parser a certain amount of times and compiles the results in a {@link List}.
|
||||||
|
* <p>
|
||||||
|
* Use another parser in-between the main parser to parse separators (e. g. commas).
|
||||||
|
*
|
||||||
|
* @param <T> return type of the parser
|
||||||
|
*/
|
||||||
|
public class Repeat<T> implements Parser<List<T>> {
|
||||||
|
|
||||||
|
private final Parser<?> separator;
|
||||||
|
private final Parser<T> parser;
|
||||||
|
private final int from;
|
||||||
|
private final int to;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Repeat} parser.
|
||||||
|
*
|
||||||
|
* @param from minimum amount of repeats
|
||||||
|
* @param to maximum amount of repeats
|
||||||
|
* @param separator the parser that separates the main parser
|
||||||
|
* @param parser the parser to repeatedly use
|
||||||
|
*/
|
||||||
|
public Repeat(int from, int to, Parser<?> separator, Parser<T> parser) {
|
||||||
|
// Just in case somebody enters incorrect values, attempt to interpret them as
|
||||||
|
// best as possible.
|
||||||
|
this.from = Math.max(0, Math.min(from, to));
|
||||||
|
this.to = Math.max(0, Math.max(from, to));
|
||||||
|
|
||||||
|
this.separator = separator;
|
||||||
|
this.parser = parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Repeat} parser (without separators).
|
||||||
|
*
|
||||||
|
* @param from minimum amount of repeats
|
||||||
|
* @param to maximum amount of repeats
|
||||||
|
* @param parser the parser to repeatedly use
|
||||||
|
*/
|
||||||
|
public Repeat(int from, int to, Parser<T> parser) {
|
||||||
|
this(from, to, null, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Repeat} parser that repeats zero or more times.
|
||||||
|
*
|
||||||
|
* @param separator the parser that separates the main parser
|
||||||
|
* @param parser the parser to repeatedly use
|
||||||
|
*/
|
||||||
|
public Repeat(Parser<?> separator, Parser<T> parser) {
|
||||||
|
this(0, Integer.MAX_VALUE, separator, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link Repeat} parser that repeats zero or more times (without separator).
|
||||||
|
*
|
||||||
|
* @param parser the parser to repeatedly use
|
||||||
|
*/
|
||||||
|
public Repeat(Parser<T> parser) {
|
||||||
|
this(null, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repeat a parser at least {@code amount} times.
|
||||||
|
*
|
||||||
|
* @param <T> the {@link Repeat}'s type
|
||||||
|
* @param amount how often to repeat the parser
|
||||||
|
* @param separator the parser that separates the main parser
|
||||||
|
* @param parser the parser to repeatedly use
|
||||||
|
* @return the {@link Repeat}
|
||||||
|
*/
|
||||||
|
public static <T> Repeat<T> atLeast(int amount, Parser<?> separator, Parser<T> parser) {
|
||||||
|
return new Repeat<>(amount, Integer.MAX_VALUE, separator, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repeat a parser at least {@code amount} times (without separator).
|
||||||
|
*
|
||||||
|
* @param <T> the {@link Repeat}'s type
|
||||||
|
* @param amount how often to repeat the parser
|
||||||
|
* @param parser the parser to repeatedly use
|
||||||
|
* @return the {@link Repeat}
|
||||||
|
*/
|
||||||
|
public static <T> Repeat<T> atLeast(int amount, Parser<T> parser) {
|
||||||
|
return new Repeat<>(amount, Integer.MAX_VALUE, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repeat a parser at most {@code amount} times.
|
||||||
|
*
|
||||||
|
* @param <T> the {@link Repeat}'s type
|
||||||
|
* @param amount how often to repeat the parser
|
||||||
|
* @param separator the parser that separates the main parser
|
||||||
|
* @param parser the parser to repeatedly use
|
||||||
|
* @return the {@link Repeat}
|
||||||
|
*/
|
||||||
|
public static <T> Repeat<T> atMost(int amount, Parser<?> separator, Parser<T> parser) {
|
||||||
|
return new Repeat<>(0, amount, separator, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repeat a parser at most {@code amount} times (without separator).
|
||||||
|
*
|
||||||
|
* @param <T> the {@link Repeat}'s type
|
||||||
|
* @param amount how often to repeat the parser
|
||||||
|
* @param parser the parser to repeatedly use
|
||||||
|
* @return the {@link Repeat}
|
||||||
|
*/
|
||||||
|
public static <T> Repeat<T> atMost(int amount, Parser<T> parser) {
|
||||||
|
return new Repeat<>(0, amount, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repeat a parser exactly {@code amount} times.
|
||||||
|
*
|
||||||
|
* @param <T> the {@link Repeat}'s type
|
||||||
|
* @param amount how often to repeat the parser
|
||||||
|
* @param separator the parser that separates the main parser
|
||||||
|
* @param parser the parser to repeatedly use
|
||||||
|
* @return the {@link Repeat}
|
||||||
|
*/
|
||||||
|
public static <T> Repeat<T> exactly(int amount, Parser<?> separator, Parser<T> parser) {
|
||||||
|
return new Repeat<>(amount, amount, separator, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repeat a parser exactly {@code amount} times (without separator).
|
||||||
|
*
|
||||||
|
* @param <T> the {@link Repeat}'s type
|
||||||
|
* @param amount how often to repeat the parser
|
||||||
|
* @param parser the parser to repeatedly use
|
||||||
|
* @return the {@link Repeat}
|
||||||
|
*/
|
||||||
|
public static <T> Repeat<T> exactly(int amount, Parser<T> parser) {
|
||||||
|
return new Repeat<>(amount, amount, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<T> read(Block block) throws ParseException {
|
||||||
|
List<T> results = new ArrayList<>();
|
||||||
|
|
||||||
|
if (from > 0) {
|
||||||
|
// The first element, not preceded by a "between"
|
||||||
|
results.add(parser.parse(block));
|
||||||
|
|
||||||
|
// All other elements, preceded by a "between"
|
||||||
|
results.addAll(parseRequired(block, from - 1));
|
||||||
|
results.addAll(parseOptional(block, to - from));
|
||||||
|
} else if (to > 0) {
|
||||||
|
// The first element, not preceded by a "between"
|
||||||
|
try {
|
||||||
|
results.add(parser.parse(block));
|
||||||
|
} catch (ParseException e) {
|
||||||
|
return results; // empty list
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other elements, preceded by a "between"
|
||||||
|
results.addAll(parseOptional(block, to - 1)); // from == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseBetween(Block block) throws ParseException {
|
||||||
|
if (separator != null) {
|
||||||
|
separator.parse(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<T> parseRequired(Block block, int amount) throws ParseException {
|
||||||
|
List<T> results = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < amount; i++) {
|
||||||
|
parseBetween(block);
|
||||||
|
results.add(parser.parse(block));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<T> parseOptional(Block block, int amount) {
|
||||||
|
List<T> results = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < amount; i++) {
|
||||||
|
try {
|
||||||
|
T result = ((Parser<T>) block2 -> {
|
||||||
|
parseBetween(block2);
|
||||||
|
return parser.parse(block2);
|
||||||
|
}).parse(block);
|
||||||
|
|
||||||
|
results.add(result);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue