personthecat.catlib.linting.SyntaxLinter Maven / Gradle / Ivy
Show all versions of catlib-quilt Show documentation
package personthecat.catlib.linting;
import net.minecraft.class_124;
import net.minecraft.class_2561;
import net.minecraft.class_2568;
import net.minecraft.class_2583;
import net.minecraft.class_2585;
import net.minecraft.class_2588;
import net.minecraft.network.chat.*;
import javax.annotation.Nullable;
import javax.annotation.RegEx;
import javax.annotation.concurrent.ThreadSafe;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An extensible, modular linter for displaying formatted data in the chat.
*
* To use this class, create a new instance by passing in an array of
* {@link Highlighter highlighters}. A highlighter is an object which locates
* and stylizes text. For example, the {@link RegexHighlighter} is capable
* of matching entire patterns or groups in a regular expression and applying
* a constant style to the individual matches.
*
*
{@code
* static final SyntaxLinter TRUE_LINTER =
* new SyntaxLinter({
* new RegexHighlighter("true", color(ChatFormatting.GOLD))
* });
* }
*
* This object can then produce a formatted {@link class_2561} when provided
* a body of text.
*
*
Note that this object is valid and safe in a multithreaded context.
*/
@ThreadSafe
public class SyntaxLinter {
public static final Pattern MULTILINE_DOC = Pattern.compile("/\\*\\*[\\s\\S]*?\\*/", Pattern.DOTALL);
public static final Pattern MULTILINE_COMMENT = Pattern.compile("/\\*[\\s\\S]*?\\*/", Pattern.DOTALL);
public static final Pattern LINE_TODO = Pattern.compile("(?:#|//).*(?:todo|to-do).*$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
public static final Pattern LINE_DOC = Pattern.compile("(?:#!|///).*$", Pattern.MULTILINE);
public static final Pattern LINE_COMMENT = Pattern.compile("(?:#|//).*$", Pattern.MULTILINE);
public static final Pattern KEY = Pattern.compile("(\"[\\w\\s]*\"|\\w+)\\s*(?=:)|[-_\\w./]+\\s*(?:::|[aA][sS])\\s*\\w+(.*\\s[aA][sS]\\s+\\w+)?", Pattern.MULTILINE);
public static final Pattern BOOLEAN_VALUE = Pattern.compile("(true|false)(?=\\s*,?\\s*(?:$|#|//|/\\*))", Pattern.MULTILINE);
public static final Pattern NUMERIC_VALUE = Pattern.compile("(\\d+(\\.\\d+)?)(?=\\s*,?\\s*(?:$|#|//|/\\*))", Pattern.MULTILINE);
public static final Pattern NULL_VALUE = Pattern.compile("(null)(?=\\s*,?\\s*(?:$|#|//|/\\*))", Pattern.MULTILINE);
public static final Pattern BAD_CLOSER = Pattern.compile("[a-zA-Z]\\w*(? 0) {
// Append unformatted text;
formatted.method_10852(stc(text.substring(i, start)));
}
formatted.method_10852(h.replacement());
ctx.skipTo(end);
i = end;
}
return formatted.method_10852(stc(text.substring(i)));
}
/**
* Shorthand for constructing an error style when provided a raw tooltip.
*
* @param s The string to display as a tooltip.
* @return A new {@link class_2583} used for displaying error messages.
*/
public static class_2583 error(final String s) {
return class_2583.field_24360.method_27705(class_124.field_1061, class_124.field_1073)
.method_10949(new class_2568(class_2568.class_5247.field_24342, translate(s)));
}
/**
* Shorthand for a regular, formattable {@link class_2585}.
*
* @param s The text being wrapped.
* @return A wrapped, formattable output containing the original message.
*/
public static class_2585 stc(final String s) {
return new class_2585(s);
}
/**
* Shorthand for a {@link class_2588}.
*
* @param s The text being wrapped.
* @return A wrapped, formattable output containing the translated message.
*/
public static class_2588 translate(final String s) {
return new class_2588(s);
}
/**
* Returns a new {@link class_2583} with the given color.
*
* @param color The {@link class_124} color.
* @return A new style with the given color.
*/
public static class_2583 color(final class_124 color) {
return class_2583.field_24360.method_10977(color);
}
/**
* This interface represents any object storing instructions for how to
* highlight a body of text. It does not contain the text, nor should it
* contain any mutable data for tracking the text. Rather, it should
* provide an {@link Instance} which does once the text becomes available.
*/
public interface Highlighter {
Instance get(String text);
/**
* This interface represents an object which tracks and applies
* formatting changes to a body of text. It houses the logic
* responsible for locating the text and will provide the updated
* text when it becomes available.
*/
interface Instance {
void next();
boolean found();
int start();
int end();
class_2561 replacement();
}
}
/**
* A highlighter which applies a single pattern based on a regular expression.
*
*
Note that, by default, any part of the expression matched will be consumed
* by this highlighter. To explicitly match the individual groups within a pattern,
* apply the useGroups
flag to the constructor.
*/
public static class RegexHighlighter implements Highlighter {
final Pattern pattern;
final class_2583 style;
final boolean useGroups;
public RegexHighlighter(final @RegEx String pattern, final class_2583 style) {
this(Pattern.compile(pattern, Pattern.MULTILINE), style);
}
public RegexHighlighter(final Pattern pattern, final class_2583 style) {
this(pattern, style, false);
}
public RegexHighlighter(final Pattern pattern, final class_2583 style, final boolean useGroups) {
this.pattern = pattern;
this.style = style;
this.useGroups = useGroups;
}
@Override
public Instance get(final String text) {
return new RegexHighlighterInstance(text);
}
public class RegexHighlighterInstance implements Instance {
final Matcher matcher;
final String text;
final int groups;
boolean found;
int group;
int m;
public RegexHighlighterInstance(final String text) {
this.matcher = pattern.matcher(text);
this.text = text;
this.groups = useGroups ? matcher.groupCount() : 0;
this.found = matcher.find();
this.group = found && groups > 0 ? 1 : 0;
this.m = 0;
}
@Override
public void next() {
if (this.group < this.groups) {
this.group++;
} else {
this.found = this.matcher.find();
if (this.found) {
this.m++;
}
}
}
@Override
public boolean found() {
return this.found;
}
@Override
public int start() {
return this.matcher.start(this.group);
}
@Override
public int end() {
return this.matcher.end(this.group);
}
@Override
public class_2561 replacement() {
return new class_2585(this.text.substring(this.start(), this.end())).method_10862(this.getStyle());
}
private class_2583 getStyle() {
return style != null ? style : RANDOM_COLORS[this.m % RANDOM_COLORS.length];
}
}
}
/**
* A simple highlighter used for locating any unclosed or unexpected container
* elements. These characters will be highlighted in red and underlined with a
* tooltip displaying details about the issue.
*
*
Note that this highlighter is not designed for high accuracy. It is not
* aware that container elements will be consumed by raw, unquoted strings.
* Instead, it assumes that these errors are unrelated and expects that they
* will be highlighted by some other object. This is an intentionally pragmatic
* approach, but may need more testing.
*
*
Please share any feedback regarding this highlighter to PersonTheCat on
* the GitHub repository for this project.
*/
public static class UnbalancedTokenHighlighter implements Highlighter {
public static final UnbalancedTokenHighlighter INSTANCE = new UnbalancedTokenHighlighter();
private UnbalancedTokenHighlighter() {}
@Override
public Instance get(final String text) {
return new UnbalancedTokenHighlighterInstance(text);
}
public static class UnbalancedTokenHighlighterInstance implements Instance {
final BitSet unclosed = new BitSet();
final BitSet unexpected = new BitSet();
int unclosedIndex = 0;
int unexpectedIndex = 0;
final String text;
public UnbalancedTokenHighlighterInstance(final String text) {
this.text = text;
this.resolveErrors();
this.next();
}
private void resolveErrors() {
boolean esc = false;
int braces = 0;
int brackets = 0;
int i = 0;
while (i < this.text.length()) {
if (esc) {
esc = false;
i++;
continue;
}
switch (this.text.charAt(i)) {
case '\\' -> esc = true;
case '{' -> {
if (this.findClosing(i, braces, '{', '}') < 0) {
this.unclosed.set(i);
}
braces++;
}
case '[' -> {
if (this.findClosing(i, brackets, '[', ']') < 0) {
this.unclosed.set(i);
}
brackets++;
}
case '}' -> {
if (braces <= 0) {
this.unexpected.set(i);
}
braces--;
}
case ']' -> {
if (brackets <= 0) {
this.unexpected.set(i);
}
brackets--;
}
}
i++;
}
}
private int findClosing(int opening, int ongoing, char open, char close) {
int numP = ongoing;
boolean dq = false;
boolean sq = false;
boolean esc = false;
for (int i = opening; i < this.text.length(); i++) {
if (esc) {
esc = false;
continue;
}
final char c = this.text.charAt(i);
if (c == '\\') {
esc = true;
} else if (c == '"') {
dq = !dq;
} else if (c == '\'') {
sq = !sq;
} else if (c == open) {
if (!dq && !sq) numP++;
} else if (c == close) {
if (!dq && !sq && --numP == 0) return i;
}
}
return -1;
}
@Override
public void next() {
if (this.unclosedIndex >= 0) {
this.unclosedIndex = this.unclosed.nextSetBit(this.unclosedIndex + 1);
}
if (this.unexpectedIndex >= 0) {
this.unexpectedIndex = this.unexpected.nextSetBit(this.unexpectedIndex + 1);
}
}
@Override
public boolean found() {
return this.unclosedIndex >= 0 || this.unexpectedIndex >= 0;
}
@Override
public int start() {
if (this.unexpectedIndex < 0) {
return this.unclosedIndex;
} else if (this.unclosedIndex < 0) {
return this.unexpectedIndex;
}
return Math.min(this.unclosedIndex, this.unexpectedIndex);
}
@Override
public int end() {
return this.start() + 1;
}
@Override
public class_2561 replacement() {
final int start = this.start();
final char c = this.text.charAt(start);
final class_2583 style = start == this.unclosedIndex ? UNCLOSED_ERROR : UNEXPECTED_ERROR;
return stc(String.valueOf(c)).method_27696(style);
}
}
}
/**
* The context used for highlighting text output. Essentially just a list of
* {@link Matcher} -> {@link class_2583} for the given text.
*/
private static class Context {
final List highlighters = new ArrayList<>();
final String text;
Context(final String text, final Highlighter[] highlighters) {
this.text = text;
for (final Highlighter highlighter : highlighters) {
this.highlighters.add(highlighter.get(text));
}
}
@Nullable
Highlighter.Instance next(final int i) {
// Figure out whether any other matches have been found;
int start = Integer.MAX_VALUE;
Highlighter.Instance first = null;
for (final Highlighter.Instance h : this.highlighters) {
if (!h.found()) continue;
final int mStart = h.start();
if (mStart >= i && mStart < start) {
start = mStart;
first = h;
}
}
return first;
}
void skipTo(final int i) {
for (final Highlighter.Instance h : this.highlighters) {
if (!h.found()) continue;
if (h.end() <= i) {
h.next();
}
}
}
}
}