1

feat: fluent.syntax

Портирование кода из fluent-kotlin
https://github.com/projectfluent/fluent-kotlin
This commit is contained in:
2024-03-29 13:44:27 +03:00
parent 3c36aec40d
commit 499c699cd1
329 changed files with 30594 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
plugins {
id "java"
}
compileJava {
targetCompatibility = sourceCompatibility = JavaVersion.VERSION_17
options.encoding = "UTF-8"
}
group = "ru.di9.fluent"
version = "1.0-SNAPSHOT"
repositories {
mavenLocal()
mavenCentral()
}
ext {
assertjVersion = "3.24.2"
gsonVersion = "2.9.1"
joorVersion = "0.9.15"
jsonAssertVersion = "1.5.1"
junitVersion = "5.9.2"
lombokVersion = "1.18.30"
}
dependencies {
annotationProcessor("org.projectlombok:lombok:$lombokVersion")
compileOnly("org.projectlombok:lombok:$lombokVersion")
testImplementation(platform("org.junit:junit-bom:$junitVersion"))
testImplementation("org.junit.jupiter:junit-jupiter")
testAnnotationProcessor("org.projectlombok:lombok:$lombokVersion")
testCompileOnly("org.projectlombok:lombok:$lombokVersion")
testImplementation("org.assertj:assertj-core:$assertjVersion")
testImplementation("com.google.code.gson:gson:$gsonVersion")
testImplementation("org.jooq:joor:$joorVersion")
testImplementation("org.skyscreamer:jsonassert:$jsonAssertVersion")
}
test {
useJUnitPlatform()
}

View File

@@ -0,0 +1,7 @@
package ru.di9.fluent.syntax;
public interface MathUtils {
static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(value, max));
}
}

View File

@@ -0,0 +1,38 @@
package ru.di9.fluent.syntax;
import java.util.Optional;
public interface StringUtils {
static Optional<Character> getCharAt(String string, int index) {
if (string == null || string.isEmpty()) {
return Optional.empty();
}
if (index >= 0 && index <= string.length() - 1) {
return Optional.of(string.charAt(index));
} else {
return Optional.empty();
}
}
static boolean inRange_az(int cc) {
return (97 <= cc && cc <= 122); // a-z
}
static boolean inRange_AZ(int cc) {
return (65 <= cc && cc <= 90); // A-Z
}
static boolean inRange_af(int cc) {
return (97 <= cc && cc <= 102); // a-f
}
static boolean inRange_AF(int cc) {
return (65 <= cc && cc <= 70); // A-F
}
static boolean inRange_09(int cc) {
return (48 <= cc && cc <= 57); // 0-9
}
}

View File

@@ -0,0 +1,32 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
public class Annotation extends SyntaxNode {
private final List<Object> arguments = new ArrayList<>();
private final String code;
private final String message;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Annotation that = (Annotation) o;
return Objects.equals(code, that.code) &&
Objects.equals(message, that.message) &&
Objects.equals(arguments, that.arguments);
}
@Override
public int hashCode() {
return Objects.hash(code, message, arguments);
}
}

View File

@@ -0,0 +1,28 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
public class Attribute extends SyntaxNode {
private final Identifier id;
private final Pattern value;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Attribute attribute = (Attribute) o;
return Objects.equals(id, attribute.id) &&
Objects.equals(value, attribute.value);
}
@Override
public int hashCode() {
return Objects.hash(id, value);
}
}

View File

@@ -0,0 +1,26 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
public abstract class BaseComment extends Entry {
private final String content;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
BaseComment that = (BaseComment) o;
return Objects.equals(content, that.content);
}
@Override
public int hashCode() {
return Objects.hash(content);
}
}

View File

@@ -0,0 +1,4 @@
package ru.di9.fluent.syntax.ast;
public abstract class BaseNode {
}

View File

@@ -0,0 +1,4 @@
package ru.di9.fluent.syntax.ast;
public interface CallArgument {
}

View File

@@ -0,0 +1,27 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Getter
public class CallArguments extends SyntaxNode {
private final List<Expression> positional = new ArrayList<>();
private final List<NamedArgument> named = new ArrayList<>();
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
CallArguments that = (CallArguments) o;
return Objects.equals(positional, that.positional) && Objects.equals(named, that.named);
}
@Override
public int hashCode() {
return Objects.hash(positional, named);
}
}

View File

@@ -0,0 +1,7 @@
package ru.di9.fluent.syntax.ast;
public class Comment extends BaseComment {
public Comment(String content) {
super(content);
}
}

View File

@@ -0,0 +1,4 @@
package ru.di9.fluent.syntax.ast;
public class Entry extends TopLevel {
}

View File

@@ -0,0 +1,4 @@
package ru.di9.fluent.syntax.ast;
public abstract class Expression extends SyntaxNode implements CallArgument, InsidePlaceable {
}

View File

@@ -0,0 +1,28 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
public class FunctionReference extends Expression {
private final Identifier id;
private final CallArguments arguments;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
FunctionReference that = (FunctionReference) o;
return Objects.equals(id, that.id) &&
Objects.equals(arguments, that.arguments);
}
@Override
public int hashCode() {
return Objects.hash(id, arguments);
}
}

View File

@@ -0,0 +1,7 @@
package ru.di9.fluent.syntax.ast;
public class GroupComment extends BaseComment {
public GroupComment(String content) {
super(content);
}
}

View File

@@ -0,0 +1,28 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
@ToString(of = {"name"})
public class Identifier extends SyntaxNode implements VariantKey {
private final String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Identifier that = (Identifier) o;
return Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}

View File

@@ -0,0 +1,4 @@
package ru.di9.fluent.syntax.ast;
public interface InsidePlaceable {
}

View File

@@ -0,0 +1,34 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
public class Junk extends TopLevel {
private final List<Annotation> annotations = new ArrayList<>();
private final String content;
public void addAnnotation(Annotation annotation) {
this.getAnnotations().add(annotation);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Junk junk = (Junk) o;
return Objects.equals(content, junk.content) &&
Objects.equals(annotations, junk.annotations);
}
@Override
public int hashCode() {
return Objects.hash(content, annotations);
}
}

View File

@@ -0,0 +1,26 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
public abstract class Literal extends Expression {
private final String value;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Literal literal = (Literal) o;
return Objects.equals(value, literal.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}

View File

@@ -0,0 +1,50 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@RequiredArgsConstructor
public class Message extends Entry {
@Getter
private final List<Attribute> attributes = new ArrayList<>();
@Getter
private final Identifier id;
private final Pattern value;
@Setter
private Comment comment;
public Optional<Comment> getComment() {
return Optional.ofNullable(comment);
}
public Optional<Pattern> getValue() {
return Optional.ofNullable(value);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Message message = (Message) o;
return Objects.equals(id, message.id) &&
Objects.equals(value, message.value) &&
Objects.equals(attributes, message.attributes) &&
Objects.equals(comment, message.comment);
}
@Override
public int hashCode() {
return Objects.hash(id, value, attributes, comment);
}
}

View File

@@ -0,0 +1,35 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Objects;
import java.util.Optional;
@RequiredArgsConstructor
public class MessageReference extends Expression {
@Getter
private final Identifier id;
private final Identifier attribute;
public Optional<Identifier> getAttribute() {
return Optional.ofNullable(attribute);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
MessageReference that = (MessageReference) o;
return Objects.equals(id, that.id) &&
Objects.equals(attribute, that.attribute);
}
@Override
public int hashCode() {
return Objects.hash(id, attribute);
}
}

View File

@@ -0,0 +1,28 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
public class NamedArgument extends SyntaxNode implements CallArgument {
private final Identifier name;
private final Literal value;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
NamedArgument that = (NamedArgument) o;
return Objects.equals(name, that.name) &&
Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(name, value);
}
}

View File

@@ -0,0 +1,7 @@
package ru.di9.fluent.syntax.ast;
public class NumberLiteral extends Literal implements VariantKey {
public NumberLiteral(String value) {
super(value);
}
}

View File

@@ -0,0 +1,39 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@Getter
public class Pattern extends SyntaxNode {
private final List<PatternElement> elements;
public Pattern() {
this.elements = new ArrayList<>();
}
public Pattern(PatternElement... elements) {
this.elements = new ArrayList<>(Arrays.asList(elements));
}
public Pattern(List<PatternElement> elements) {
this.elements = elements;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
var that = (Pattern) o;
return Objects.equals(this.elements, that.elements);
}
@Override
public int hashCode() {
return Objects.hash(elements);
}
}

View File

@@ -0,0 +1,4 @@
package ru.di9.fluent.syntax.ast;
public abstract class PatternElement extends SyntaxNode {
}

View File

@@ -0,0 +1,26 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
public class Placeable extends PatternElement implements InsidePlaceable {
private final InsidePlaceable expression;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Placeable placeable = (Placeable) o;
return Objects.equals(expression, placeable.expression);
}
@Override
public int hashCode() {
return Objects.hash(expression);
}
}

View File

@@ -0,0 +1,30 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Getter
public class Resource extends SyntaxNode {
private final List<TopLevel> body = new ArrayList<>();
public Resource(List<TopLevel> children) {
this.body.addAll(children);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Resource resource = (Resource) o;
return Objects.equals(body, resource.body);
}
@Override
public int hashCode() {
return Objects.hash(body);
}
}

View File

@@ -0,0 +1,7 @@
package ru.di9.fluent.syntax.ast;
public class ResourceComment extends BaseComment {
public ResourceComment(String content) {
super(content);
}
}

View File

@@ -0,0 +1,29 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.List;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
public class SelectExpression extends Expression {
private final Expression selector;
private final List<Variant> variants;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
SelectExpression that = (SelectExpression) o;
return Objects.equals(selector, that.selector) &&
Objects.equals(variants, that.variants);
}
@Override
public int hashCode() {
return Objects.hash(selector, variants);
}
}

View File

@@ -0,0 +1,28 @@
package ru.di9.fluent.syntax.ast;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.Objects;
@AllArgsConstructor
@Getter
@Setter
public class Span extends BaseNode {
private int start;
private int end;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Span span = (Span) o;
return start == span.start && end == span.end;
}
@Override
public int hashCode() {
return Objects.hash(start, end);
}
}

View File

@@ -0,0 +1,7 @@
package ru.di9.fluent.syntax.ast;
public class StringLiteral extends Literal {
public StringLiteral(String value) {
super(value);
}
}

View File

@@ -0,0 +1,29 @@
package ru.di9.fluent.syntax.ast;
import java.util.Objects;
import java.util.Optional;
public abstract class SyntaxNode extends BaseNode {
private Span span;
public Optional<Span> getSpan() {
return Optional.ofNullable(span);
}
public void addSpan(int start, int end) {
span = new Span(start, end);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SyntaxNode that = (SyntaxNode) o;
return Objects.equals(span, that.span);
}
@Override
public int hashCode() {
return Objects.hash(span);
}
}

View File

@@ -0,0 +1,44 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@RequiredArgsConstructor
public class Term extends Entry {
@Getter
private final List<Attribute> attributes = new ArrayList<>();
@Getter
private final Identifier id;
@Getter
private final Pattern value;
@Setter
private Comment comment;
public Optional<Comment> getComment() {
return Optional.ofNullable(comment);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Term term = (Term) o;
return Objects.equals(id, term.id) && Objects.equals(value, term.value) && Objects.equals(attributes, term.attributes) && Objects.equals(comment, term.comment);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), id, value, attributes, comment);
}
}

View File

@@ -0,0 +1,42 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Objects;
import java.util.Optional;
@RequiredArgsConstructor
public class TermReference extends Expression {
@Getter
private final Identifier id;
private final Identifier attribute;
private final CallArguments arguments;
public Optional<Identifier> getAttribute() {
return Optional.ofNullable(attribute);
}
public Optional<CallArguments> getArguments() {
return Optional.ofNullable(arguments);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
TermReference that = (TermReference) o;
return Objects.equals(id, that.id) &&
Objects.equals(attribute, that.attribute) &&
Objects.equals(arguments, that.arguments);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), id, attribute, arguments);
}
}

View File

@@ -0,0 +1,28 @@
package ru.di9.fluent.syntax.ast;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.Objects;
@AllArgsConstructor
@Getter
@Setter
public class TextElement extends PatternElement {
private String value;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
TextElement that = (TextElement) o;
return Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), value);
}
}

View File

@@ -0,0 +1,4 @@
package ru.di9.fluent.syntax.ast;
public abstract class TopLevel extends SyntaxNode {
}

View File

@@ -0,0 +1,26 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
public class VariableReference extends Expression {
private final Identifier id;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
VariableReference that = (VariableReference) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), id);
}
}

View File

@@ -0,0 +1,30 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
public class Variant extends SyntaxNode {
private final VariantKey key;
private final Pattern value;
private final boolean isDefault;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Variant variant = (Variant) o;
return Objects.equals(key, variant.key) &&
Objects.equals(value, variant.value) &&
Objects.equals(isDefault, variant.isDefault);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), key, value, isDefault);
}
}

View File

@@ -0,0 +1,4 @@
package ru.di9.fluent.syntax.ast;
public interface VariantKey {
}

View File

@@ -0,0 +1,26 @@
package ru.di9.fluent.syntax.ast;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Objects;
@RequiredArgsConstructor
@Getter
public class Whitespace extends TopLevel {
private final String content;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Whitespace that = (Whitespace) o;
return Objects.equals(content, that.content);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), content);
}
}

View File

@@ -0,0 +1,869 @@
package ru.di9.fluent.syntax.parser;
import ru.di9.fluent.syntax.ast.*;
import java.util.*;
import java.util.function.Predicate;
import static ru.di9.fluent.syntax.StringUtils.inRange_09;
import static ru.di9.fluent.syntax.parser.FluentStream.*;
import static ru.di9.fluent.syntax.parser.ParseException.ErrorCode.*;
public class FluentParser {
public boolean withSpans = false;
public boolean withJunkAnnotations = true;
public Resource parse(String source) {
var ps = new FluentStream(source);
int spanStart = ps.index;
List<TopLevel> entries = new ArrayList<>();
Comment lastComment = null;
var blankLines = ps.skipBlankBlock();
if (!blankLines.isEmpty()) {
entries.add(new Whitespace(blankLines));
}
while (ps.currentChar().isPresent()) {
var entry = getEntryOrJunk(ps);
blankLines = ps.skipBlankBlock();
// Regular Comments require special logic. Comments may be attached to
// Messages or Terms if they are followed immediately by them. However
// they should parse as standalone when they're followed by Junk.
// Consequently, we only attach Comments once we know that the Message
// or the Term parsed successfully.
if (entry instanceof Comment comment && blankLines.isEmpty() && ps.currentChar().isPresent()) {
// Stash the comment and decide what to do with it in the next pass.
lastComment = comment;
continue;
}
if (lastComment != null) {
if (entry instanceof Message message) {
message.setComment(lastComment);
if (withSpans && message.getSpan().isPresent() && lastComment.getSpan().isPresent()) {
message.getSpan().get().setStart(lastComment.getSpan().get().getStart());
}
} else if (entry instanceof Term term) {
term.setComment(lastComment);
if (withSpans && term.getSpan().isPresent() && lastComment.getSpan().isPresent()) {
term.getSpan().get().setStart(lastComment.getSpan().get().getStart());
}
} else {
entries.add(lastComment);
}
// In either case, the stashed comment has been dealt with; clear it.
lastComment = null;
}
// No special logic for other types of entries.
entries.add(entry);
if (!blankLines.isEmpty()) {
entries.add(new Whitespace(blankLines));
}
}
var resource = new Resource(entries);
if (withSpans) {
resource.addSpan(spanStart, ps.index);
}
return resource;
}
/// PRIVATE ////////////////////////////////////////////////////////////////////////////////////////////////////////
private TopLevel getEntryOrJunk(FluentStream ps) {
int entryStartPos = ps.index;
try {
var entry = getEntry(ps);
ps.expectLineEnd();
return entry;
} catch (ParseException e) {
int errorIndex = ps.index;
ps.skipToNextEntryStart(entryStartPos);
int nextEntryStart = ps.index;
if (nextEntryStart < errorIndex) {
// The position of the error must be inside of the Junk's span.
errorIndex = nextEntryStart;
}
// Create a Junk instance
var slice = ps.string.substring(entryStartPos, nextEntryStart);
var junk = new Junk(slice);
if (withSpans) {
junk.addSpan(entryStartPos, nextEntryStart);
}
if (withJunkAnnotations) {
var annot = new Annotation(e.getCode().name(), e.getMessage());
annot.getArguments().addAll(Arrays.asList(e.getArgs()));
annot.addSpan(errorIndex, errorIndex);
junk.addAnnotation(annot);
}
return junk;
}
}
private Entry getEntry(FluentStream ps) {
var currentChar = ps.currentChar();
if (currentChar.filter(v -> v == '#').isPresent()) {
return getComment(ps);
}
if (currentChar.filter(v -> v == '-').isPresent()) {
return getTerm(ps);
}
if (ps.isIdentifierStart()) {
return getMessage(ps);
}
throw new ParseException(E0002);
}
private Message getMessage(FluentStream ps) {
int spanStart = ps.index;
var id = getIdentifier(ps);
ps.skipBlankInline();
ps.expectChar('=');
var value = maybeGetPattern(ps);
var attrs = getAttributes(ps);
if (value == null && attrs.isEmpty()) {
throw new ParseException(E0005, id.getName());
}
var msg = new Message(id, value);
msg.getAttributes().addAll(attrs);
if (withSpans) {
msg.addSpan(spanStart, ps.index);
}
return msg;
}
private Term getTerm(FluentStream ps) {
int spanStart = ps.index;
ps.expectChar('-');
var id = getIdentifier(ps);
ps.skipBlankInline();
ps.expectChar('=');
var value = maybeGetPattern(ps);
if (value == null) {
throw new ParseException(E0006, id.getName());
}
var attrs = getAttributes(ps);
var term = new Term(id, value);
term.getAttributes().addAll(attrs);
if (withSpans) {
term.addSpan(spanStart, ps.index);
}
return term;
}
private Collection<Attribute> getAttributes(FluentStream ps) {
List<Attribute> attrs = new ArrayList<>();
ps.peekBlank();
while (ps.isAttributeStart()) {
ps.skipToPeek();
var attr = getAttribute(ps);
attrs.add(attr);
ps.peekBlank();
}
return attrs;
}
private Attribute getAttribute(FluentStream ps) {
int spanStart = ps.index;
ps.expectChar('.');
var key = getIdentifier(ps);
ps.skipBlankInline();
ps.expectChar('=');
var value = maybeGetPattern(ps);
if (value == null) {
throw new ParseException(E0012);
}
var attribute = new Attribute(key, value);
if (withSpans) {
attribute.addSpan(spanStart, ps.index);
}
return attribute;
}
// maybeGetPattern distinguishes between patterns which start on the same line
// as the identifier (a.k.a. inline signleline patterns and inline multiline
// patterns) and patterns which start on a new line (a.k.a. block multiline
// patterns). The distinction is important for the dedentation logic: the
// indent of the first line of a block pattern must be taken into account when
// calculating the maximum common indent.
private Pattern maybeGetPattern(FluentStream ps) {
ps.peekBlankInline();
if (ps.isValueStart()) {
ps.skipToPeek();
return getPattern(ps, false);
}
ps.peekBlankBlock();
if (ps.isValueContinuation()) {
ps.skipToPeek();
return getPattern(ps, true);
}
return null;
}
private Pattern getPattern(FluentStream ps, boolean isBlock) {
int spanStart = ps.index;
List<PatternElement> elements = new ArrayList<>();
int commonIndentLength = Integer.MAX_VALUE;
if (isBlock) {
// A block pattern is a pattern which starts on a new line. Store and
// measure the indent of this first line for the dedentation logic.
var blankStart = ps.index;
var firstIndent = ps.skipBlankInline();
elements.add(getIndent(ps, firstIndent, blankStart));
commonIndentLength = firstIndent.length();
}
Optional<Character> opt;
elements:
while ((opt = ps.currentChar()).isPresent()) {
var ch = opt.get();
switch (ch) {
case EOL -> {
var blankStart = ps.index;
var blankLines = ps.peekBlankBlock();
if (ps.isValueContinuation()) {
ps.skipToPeek();
var indent = ps.skipBlankInline();
commonIndentLength = Math.min(commonIndentLength, indent.length());
elements.add(getIndent(ps, blankLines + indent, blankStart));
continue;
}
// The end condition for getPattern's while loop is a newline
// which is not followed by a valid pattern continuation.
ps.resetPeek();
break elements;
}
case '{' -> elements.add(getPlaceable(ps));
case '}' -> throw new ParseException(E0027);
default -> elements.add(getTextElement(ps));
}
}
var dedented = dedent(elements, commonIndentLength);
var pattern = new Pattern(dedented);
if (withSpans) {
pattern.addSpan(spanStart, ps.index);
}
return pattern;
}
// Dedent a list of elements by removing the maximum common indent from the
// beginning of text lines. The common indent is calculated in getPattern.
private List<PatternElement> dedent(Collection<PatternElement> elements, int commonIndent) {
ArrayList<PatternElement> trimmed = new ArrayList<>();
for (var element : elements) {
if (element instanceof Placeable pl) {
trimmed.add(pl);
continue;
}
if (element instanceof Indent ind) {
// Strip common indent.
ind.setValue(ind.getValue().substring(0, (ind.getValue().length() - commonIndent)));
if (ind.getValue().isEmpty()) {
continue;
}
}
if (!trimmed.isEmpty()) {
var prev = trimmed.get(trimmed.size() - 1);
if (prev instanceof TextElement prevTE) {
// Join adjacent TextElements by replacing them with their sum.
String newVal;
if (element instanceof TextElement elmTE) {
newVal = elmTE.getValue();
} else if (element instanceof Indent elmInd) {
newVal = elmInd.getValue();
} else {
throw new IllegalStateException("Unexpected PatternElement type");
}
var sum = new TextElement(prevTE.getValue() + newVal);
if (withSpans && prevTE.getSpan().isPresent() && element.getSpan().isPresent()) {
sum.addSpan(prevTE.getSpan().get().getStart(), element.getSpan().get().getEnd());
}
trimmed.set(trimmed.size() - 1, sum);
continue;
}
}
if (element instanceof Indent elmInd) {
// If the indent hasn't been merged into a preceding TextElement,
// convert it into a new TextElement.
var textElement = new TextElement(elmInd.getValue());
if (withSpans && elmInd.getSpan().isPresent()) {
textElement.addSpan(elmInd.getSpan().get().getStart(), elmInd.getSpan().get().getEnd());
}
trimmed.add(textElement);
continue;
}
// The element is a TextElement or a Placeable
trimmed.add(element);
}
// Trim trailing whitespace from the Pattern.
var lastElement = trimmed.get(trimmed.size() - 1);
if (lastElement instanceof TextElement lastElmTE) {
lastElmTE.setValue(TRAILING_WS_RE.matcher(lastElmTE.getValue()).replaceAll(""));
if (lastElmTE.getValue().isEmpty()) {
trimmed.remove(trimmed.size() - 1);
}
}
return trimmed;
}
private TextElement getTextElement(FluentStream ps) {
var buffer = new StringBuilder();
int spanStart = ps.index;
Optional<Character> opt;
while ((opt = ps.currentChar()).isPresent()) {
char ch = opt.get();
if (ch == '{' || ch == '}' || ch == EOL) {
var textElement = new TextElement(buffer.toString());
if (withSpans) {
textElement.addSpan(spanStart, ps.index);
}
return textElement;
}
buffer.append(ch);
ps.next();
}
var textElement = new TextElement(buffer.toString());
if (withSpans) {
textElement.addSpan(spanStart, ps.index);
}
return textElement;
}
private PatternElement getPlaceable(FluentStream ps) {
var spanStart = ps.index;
ps.expectChar('{');
ps.skipBlank();
SyntaxNode expression;
if (ps.currentChar().filter(v -> v == '{').isPresent()) {
var child = getPlaceable(ps);
ps.skipBlank();
expression = child;
} else {
expression = getExpression(ps);
}
ps.expectChar('}');
var placeable = new Placeable((InsidePlaceable) expression);
if (withSpans) {
placeable.addSpan(spanStart, ps.index);
}
return placeable;
}
private Expression getExpression(FluentStream ps) {
int spanStart = ps.index;
var selector = getInlineExpression(ps);
ps.skipBlank();
if (ps.currentChar().filter(v -> v == '-').isPresent()) {
if (ps.peek().filter(v -> v != '>').isPresent()) {
ps.resetPeek();
return selector;
}
// Validate selector expression according to
// abstract.js in the Fluent specification
if (selector instanceof MessageReference mr) {
throw new ParseException(mr.getAttribute().isEmpty() ? E0016 : E0018);
} else if (selector instanceof TermReference tr && tr.getAttribute().isEmpty()) {
throw new ParseException(E0017);
}
ps.next();
ps.next();
ps.skipBlankInline();
ps.expectLineEnd();
var variants = getVariants(ps);
var selectExpression = new SelectExpression(selector, variants);
if (withSpans) {
selectExpression.addSpan(spanStart, ps.index);
}
return selectExpression;
}
if (selector instanceof TermReference tr && tr.getAttribute().isPresent()) {
throw new ParseException(E0019);
}
return selector;
}
private List<Variant> getVariants(FluentStream ps) {
List<Variant> variants = new ArrayList<>();
var hasDefault = false;
ps.skipBlank();
while (ps.isVariantStart()) {
var variant = getVariant(ps, hasDefault);
if (variant.isDefault()) {
hasDefault = true;
}
variants.add(variant);
ps.expectLineEnd();
ps.skipBlank();
}
if (variants.isEmpty()) {
throw new ParseException(E0011);
}
if (!hasDefault) {
throw new ParseException(E0010);
}
return variants;
}
private Variant getVariant(FluentStream ps, boolean hasDefault) {
int spanStart = ps.index;
var defaultIndex = false;
if (ps.currentChar().filter(v -> v == '*').isPresent()) {
if (hasDefault) {
throw new ParseException(E0015);
}
ps.next();
defaultIndex = true;
}
ps.expectChar('[');
ps.skipBlank();
var key = getVariantKey(ps);
ps.skipBlank();
ps.expectChar(']');
//val value = this.maybeGetPattern(ps) ?: throw ParseError("E0012")
var value = maybeGetPattern(ps);
if (value == null) {
throw new ParseException(E0012);
}
var variant = new Variant(key, value, defaultIndex);
if (withSpans) {
variant.addSpan(spanStart, ps.index);
}
return variant;
}
private VariantKey getVariantKey(FluentStream ps) {
int cc = ps.currentChar().orElseThrow(() -> new ParseException(E0013));
if (inRange_09(cc) || cc == 45 /*-*/) {
return getNumber(ps);
}
return getIdentifier(ps);
}
private Expression getInlineExpression(FluentStream ps) {
var spanStart = ps.index;
if (ps.isNumberStart()) {
return getNumber(ps);
}
if (ps.currentChar().filter(v -> v == '"').isPresent()) {
return getString(ps);
}
if (ps.currentChar().filter(v -> v == '$').isPresent()) {
ps.next();
var id = getIdentifier(ps);
var variableReference = new VariableReference(id);
if (withSpans) {
variableReference.addSpan(spanStart, ps.index);
}
return variableReference;
}
if (ps.currentChar().filter(v -> v == '-').isPresent()) {
ps.next();
var id = getIdentifier(ps);
Identifier attr = null;
if (ps.currentChar().filter(v -> v == '.').isPresent()) {
ps.next();
attr = getIdentifier(ps);
}
CallArguments args = null;
ps.peekBlank();
if (ps.currentPeek().filter(v -> v == '(').isPresent()) {
ps.skipToPeek();
args = getCallArguments(ps);
}
var termReference = new TermReference(id, attr, args);
if (withSpans) {
termReference.addSpan(spanStart, ps.index);
}
return termReference;
}
if (ps.isIdentifierStart()) {
var id = getIdentifier(ps);
ps.peekBlank();
if (ps.currentPeek().filter(v -> v == '(').isPresent()) {
// It's a Function. Ensure it's all upper-case.
if (!VALID_FUNCTION_NAME.matcher(id.getName()).matches()) {
throw new ParseException(E0008);
}
ps.skipToPeek();
var args = getCallArguments(ps);
var functionReference = new FunctionReference(id, args);
if (withSpans) {
functionReference.addSpan(spanStart, ps.index);
}
return functionReference;
}
Identifier attr = null;
if (ps.currentChar().filter(v -> v == '.').isPresent()) {
ps.next();
attr = this.getIdentifier(ps);
}
var messageReference = new MessageReference(id, attr);
if (withSpans) {
messageReference.addSpan(spanStart, ps.index);
}
return messageReference;
}
throw new ParseException(E0028);
}
private CallArguments getCallArguments(FluentStream ps) {
var spanStart = ps.index;
List<Expression> positional = new ArrayList<>();
List<NamedArgument> named = new ArrayList<>();
Set<String> argumentNames = new HashSet<>();
ps.expectChar('(');
ps.skipBlank();
while (true) {
if (ps.currentChar().filter(v -> v == ')').isPresent()) {
break;
}
var arg = getCallArgument(ps);
if (arg instanceof NamedArgument na) {
if (argumentNames.contains(na.getName().getName())) {
throw new ParseException(E0022);
}
named.add(na);
argumentNames.add(na.getName().getName());
} else if (arg instanceof Expression exp) {
if (!argumentNames.isEmpty()) {
throw new ParseException(E0021);
}
positional.add(exp);
}
ps.skipBlank();
if (ps.currentChar().filter(v -> v == ',').isPresent()) {
ps.next();
ps.skipBlank();
continue;
}
break;
}
ps.expectChar(')');
var args = new CallArguments();
args.getPositional().addAll(positional);
args.getNamed().addAll(named);
if (withSpans) {
args.addSpan(spanStart, ps.index);
}
return args;
}
private CallArgument getCallArgument(FluentStream ps) {
var spanStart = ps.index;
var exp = getInlineExpression(ps);
ps.skipBlank();
if (ps.currentChar().filter(v -> v != ':').isPresent()) {
return exp;
}
if (exp instanceof MessageReference mr && mr.getAttribute().isEmpty()) {
ps.next();
ps.skipBlank();
var value = getLiteral(ps);
var namedArgument = new NamedArgument(mr.getId(), value);
if (withSpans) {
namedArgument.addSpan(spanStart, ps.index);
}
return namedArgument;
}
throw new ParseException(E0009);
}
private Literal getLiteral(FluentStream ps) {
if (ps.isNumberStart()) {
return getNumber(ps);
}
if (ps.currentChar().filter(v -> v == '"').isPresent()) {
return getString(ps);
}
throw new ParseException(E0014);
}
private StringLiteral getString(FluentStream ps) {
var spanStart = ps.index;
ps.expectChar('"');
var value = new StringBuilder();
Predicate<Character> filter = x -> x != '"' && x != EOL;
Optional<Character> opt;
while ((opt = ps.takeChar(filter)).isPresent()) {
var ch = opt.get();
value.append(ch == '\\' ? getEscapeSequence(ps) : ch);
}
if (ps.currentChar().filter(v -> v == EOL).isPresent()) {
throw new ParseException(E0020);
}
ps.expectChar('"');
var stringLiteral = new StringLiteral(value.toString());
if (withSpans) {
stringLiteral.addSpan(spanStart, ps.index);
}
return stringLiteral;
}
private String getEscapeSequence(FluentStream ps) {
var nextOpt = ps.currentChar();
if (nextOpt.isEmpty()) {
throw new ParseException(E0025, (Character) null);
}
var next = nextOpt.get();
return switch (next) {
case '\\', '"' -> {
ps.next();
yield "\\" + next;
}
case 'u' -> getUnicodeEscapeSequence(ps, next, 4);
case 'U' -> getUnicodeEscapeSequence(ps, next, 6);
default -> throw new ParseException(E0025, next);
};
}
private String getUnicodeEscapeSequence(FluentStream ps, Character u, int digits) {
ps.expectChar(u);
var sequence = new StringBuilder();
for (int i = 0; i < digits; i++) {
var opt = ps.takeHexDigit();
if (opt.isEmpty()) {
throw new ParseException(E0026, "\\%s%s%s".formatted(u, sequence, ps.currentChar().orElseThrow()));
}
sequence.append(opt.get());
}
return "\\%s%s".formatted(u, sequence);
}
private NumberLiteral getNumber(FluentStream ps) {
var spanStart = ps.index;
var value = new StringBuilder();
if (ps.currentChar().filter(v -> v == '-').isPresent()) {
ps.next();
value.append("-");
}
value.append(getDigits(ps));
if (ps.currentChar().filter(v -> v == '.').isPresent()) {
ps.next();
value.append(".").append(getDigits(ps));
}
var numberLiteral = new NumberLiteral(value.toString());
if (withSpans) {
numberLiteral.addSpan(spanStart, ps.index);
}
return numberLiteral;
}
private String getDigits(FluentStream ps) {
var num = new StringBuilder();
Optional<Character> opt;
while ((opt = ps.takeDigit()).isPresent()) {
num.append(opt.get());
}
if (num.isEmpty()) {
throw new ParseException(E0004, "0-9");
}
return num.toString();
}
// Create a token representing an indent. It's not part of the AST and it will
// be trimmed and merged into adjacent TextElements, or turned into a new
// TextElement, if it's surrounded by two Placeables.
private PatternElement getIndent(FluentStream ps, String value, int start) {
return new Indent(value, start, ps.index);
}
private Identifier getIdentifier(FluentStream ps) {
int spanStart = ps.index;
var name = new StringBuilder().append(ps.takeIDStart());
Optional<Character> opt;
while ((opt = ps.takeIDChar()).isPresent()) {
name.append(opt.get());
}
var identifier = new Identifier(name.toString());
if (withSpans) {
identifier.addSpan(spanStart, ps.index);
}
return identifier;
}
private BaseComment getComment(FluentStream ps) {
int spanStart = ps.index;
final int ANY = -1;
final int COMMENT = 0;
final int GROUP_COMMENT = 1;
final int RESOURCE_COMMENT = 2;
var level = ANY;
var content = new StringBuilder();
while (true) {
var i = -1;
int thisLevel;
if (level == ANY) {
thisLevel = RESOURCE_COMMENT;
} else {
thisLevel = level;
}
while (ps.currentChar().filter(v -> v =='#').isPresent() && i < thisLevel) {
ps.next();
i++;
}
if (level == ANY) {
level = i;
}
if (ps.currentChar().filter(v -> v == EOL).isEmpty()) {
ps.expectChar(' ');
Optional<Character> opt;
while ((opt = ps.takeChar(v -> v != EOL)).isPresent()) {
content.append(opt.get());
}
}
if (ps.isNextLineComment(level)) {
ps.currentChar().ifPresent(content::append);
ps.next();
} else {
break;
}
}
BaseComment comment = switch (level) {
case COMMENT -> new Comment(content.toString());
case GROUP_COMMENT -> new GroupComment(content.toString());
default -> new ResourceComment(content.toString());
};
if (withSpans) {
comment.addSpan(spanStart, ps.index);
}
return comment;
}
}

View File

@@ -0,0 +1,259 @@
package ru.di9.fluent.syntax.parser;
import ru.di9.fluent.syntax.MathUtils;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static ru.di9.fluent.syntax.StringUtils.*;
import static ru.di9.fluent.syntax.parser.ParseException.ErrorCode.E0003;
import static ru.di9.fluent.syntax.parser.ParseException.ErrorCode.E0004;
public class FluentStream extends ParserStream {
public static final char EOL = '\n';
public static final String SPECIAL_LINE_START_CHARS = "}.[*";
public static final Pattern VALID_FUNCTION_NAME = Pattern.compile("^[A-Z][A-Z0-9_-]*$");
public static final Pattern TRAILING_WS_RE = Pattern.compile("[ \t\n\r]+$");
public FluentStream(String string) {
super(string);
}
public String peekBlankInline() {
int start = index + peekOffset;
while (currentPeek().filter(v -> v == ' ').isPresent()) {
peek();
}
return string.substring(start, (index + peekOffset));
}
public String skipBlankInline() {
var blank = peekBlankInline();
skipToPeek();
return blank;
}
public String peekBlankBlock() {
var blank = new StringBuilder();
while (true) {
var lineStart = peekOffset;
peekBlankInline();
var currentPeek = currentPeek();
if (currentPeek.filter(v -> v == EOL).isPresent()) {
blank.append(EOL);
peek();
continue;
}
if (currentPeek.isEmpty()/*EOF*/) {
// Treat the blank line at EOF as a blank block.
return blank.toString();
}
// Any other char; reset to column 1 on this line.
resetPeek(lineStart);
return blank.toString();
}
}
public String skipBlankBlock() {
var blank = peekBlankBlock();
skipToPeek();
return blank;
}
public void peekBlank() {
while (currentPeek().filter(v -> v == ' ' || v == EOL).isPresent()) {
peek();
}
}
public void skipBlank() {
peekBlank();
skipToPeek();
}
public void expectChar(char ch) {
currentChar().filter(v -> v == ch)
.orElseThrow(() -> new ParseException(E0003, ch));
next();
}
public void expectLineEnd() {
var opt = currentChar();
if (opt.isEmpty()/*EOF*/) {
// EOF is a valid line end in Fluent.
return;
}
if (opt.get() == EOL) {
next();
return;
}
// Unicode Character 'SYMBOL FOR NEWLINE' (U+2424)
//noinspection UnnecessaryUnicodeEscape
throw new ParseException(E0003, "\u2424");
}
public Optional<Character> takeChar(Predicate<Character> f) {
var ch = currentChar().filter(f);
ch.ifPresent(x -> next());
return ch;
}
public boolean isIdentifierStart() {
return currentPeek().map(this::isCharIdStart).orElse(false);
}
public boolean isNumberStart() {
var opt = currentChar();
if (opt.filter(v -> v == '-').isPresent()) {
opt = peek();
}
boolean result = opt.map(ch -> inRange_09((int) ch)).orElse(false);
resetPeek();
return result;
}
public boolean isValueStart() {
// Inline Patterns may start with any char.
return currentPeek().filter(v -> v != EOL).isPresent();
}
public boolean isValueContinuation() {
var column1 = peekOffset;
peekBlankInline();
if (currentPeek().filter(v -> v == '{').isPresent()) {
resetPeek(column1);
return true;
}
if (peekOffset - column1 == 0) {
return false;
}
if (currentPeek().map(this::isCharPatternContinuation).orElse(false)) {
resetPeek(column1);
return true;
}
return false;
}
/**
* @param level <ul>
* <li>-1 - any
* <li>0 - comment
* <li>1 - group comment
* <li>2 - resource comment
*/
public boolean isNextLineComment(int level) {
if (currentChar().filter(v -> v != EOL).isPresent()) {
return false;
}
int lvl = MathUtils.clamp(level, -1, 2);
var i = 0;
while (i <= lvl || (lvl == -1 && i < 3)) {
if (peek().filter(v -> v != '#').isPresent()) {
if (i <= lvl && lvl != -1) {
resetPeek();
return false;
}
break;
}
i++;
}
// The first char after #, ## or ###.
boolean result = peek().filter(ch -> ch == ' ' || ch == EOL).isPresent();
resetPeek();
return result;
}
public boolean isVariantStart() {
var currentPeekOffset = peekOffset;
if (currentPeek().filter(v -> v == '*').isPresent()) {
peek();
}
boolean result = currentPeek().filter(v -> v == '[').isPresent();
resetPeek(currentPeekOffset);
return result;
}
public boolean isAttributeStart() {
return currentPeek().filter(v -> v == '.').isPresent();
}
public void skipToNextEntryStart(int junkStart) {
var lastNewline = string.lastIndexOf(EOL, index);
if (junkStart < lastNewline) {
// Last seen newline is _after_ the junk start. It's safe to rewind
// without the risk of resuming at the same broken entry.
index = lastNewline;
}
while (currentChar().isPresent()) {
// We're only interested in beginnings of line.
if (currentChar().filter(v -> v != EOL).isPresent()) {
next();
continue;
}
// Break if the first char in this line looks like an entry start.
var firstOpt = next();
if (firstOpt.isEmpty()/*EOF*/) {
continue;
}
char first = firstOpt.get();
if (isCharIdStart(first) || first == '-' || first == '#') {
break;
}
}
}
public char takeIDStart() {
if (currentChar().map(this::isCharIdStart).orElse(false)) {
var ret = currentChar();
if (ret.isPresent()) {
next();
return ret.get();
}
}
throw new ParseException(E0004, "a-zA-Z");
}
public Optional<Character> takeIDChar() {
return takeChar(ch -> {
int cc = ch;
return inRange_az(cc) || inRange_AZ(cc) || inRange_09(cc) || cc == 95 /*_*/ || cc == 45 /*-*/;
});
}
public Optional<Character> takeDigit() {
return takeChar(ch -> inRange_09((int) ch));
}
public Optional<Character> takeHexDigit() {
return takeChar(ch -> {
int cc = ch;
return inRange_09(cc) || inRange_AF(cc) || inRange_af(cc);
});
}
/// PRIVATE ////////////////////////////////////////////////////////////////////////////////////////////////////////
private boolean isCharIdStart(char ch) {
return inRange_az(ch) || inRange_AZ(ch);
}
private boolean isCharPatternContinuation(char ch) {
return SPECIAL_LINE_START_CHARS.indexOf(ch) < 0;
}
}

View File

@@ -0,0 +1,18 @@
package ru.di9.fluent.syntax.parser;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import ru.di9.fluent.syntax.ast.PatternElement;
@AllArgsConstructor
@Getter
@Setter
public class Indent extends PatternElement {
private String value;
public Indent(String value, int start, int end) {
this(value);
this.addSpan(start, end);
}
}

View File

@@ -0,0 +1,50 @@
package ru.di9.fluent.syntax.parser;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
public class ParseException extends RuntimeException {
private final ErrorCode code;
private final Object[] args;
public ParseException(ErrorCode code, Object... args) {
super(code.getMessage().formatted(args));
this.code = code;
this.args = args;
}
@RequiredArgsConstructor
@Getter
public enum ErrorCode {
E0001("Generic Error"),
E0002("Expected an entry start"),
E0003("Expected token: \"%s\""),
E0004("Expected a character from range: \"%s\""),
E0005("Expected message \"%s\" to have a value or attributes"),
E0006("Expected term \"-%s\" to have a value"),
E0007("Keyword cannot end with a whitespace"),
E0008("The callee has to be an upper-case identifier or a term"),
E0009("The argument name has to be a simple identifier"),
E0010("Expected one of the variants to be marked as default (*)"),
E0011("Expected at least one variant after \"->\""),
E0012("Expected value"),
E0013("Expected variant key"),
E0014("Expected literal"),
E0015("Only one variant can be marked as default (*)"),
E0016("Message references cannot be used as selectors"),
E0017("Terms cannot be used as selectors"),
E0018("Attributes of messages cannot be used as selectors"),
E0019("Attributes of terms cannot be used as placeables"),
E0020("Unterminated string expression"),
E0021("Positional arguments must not follow named arguments"),
E0022("Named arguments must be unique"),
E0024("Cannot access variants of a message."),
E0025("Unknown escape sequence: \\%s."),
E0026("Invalid Unicode escape sequence: %s."),
E0027("Unbalanced closing brace in TextElement."),
E0028("Expected an inline expression");
private final String message;
}
}

View File

@@ -0,0 +1,80 @@
package ru.di9.fluent.syntax.parser;
import java.util.Optional;
import static ru.di9.fluent.syntax.StringUtils.getCharAt;
public class ParserStream {
protected int index = 0;
protected int peekOffset = 0;
protected String string;
public ParserStream(String string) {
this.string = string;
}
public Optional<Character> currentChar() {
return charAt(index);
}
public Optional<Character> currentPeek() {
return charAt(index + peekOffset);
}
public Optional<Character> next() {
peekOffset = 0;
if (index >= string.length()) {
return Optional.empty();
}
// Skip over the CRLF as if it was a single character.
getCharAt(string, index)
.filter(v -> v == '\r')
.flatMap(x -> getCharAt(string, index + 1).filter(v -> v == '\n'))
.ifPresent(x -> index++);
index++;
return getCharAt(string, index);
}
public Optional<Character> peek() {
// Skip over the CRLF as if it was a single character.
getCharAt(string, index + peekOffset)
.filter(v -> v == '\r')
.flatMap(x -> getCharAt(string, index + peekOffset + 1).filter(v -> v == '\n'))
.ifPresent(x -> peekOffset++);
peekOffset++;
return getCharAt(string, index + peekOffset);
}
public void resetPeek() {
resetPeek(0);
}
public void resetPeek(int offset) {
this.peekOffset = offset;
}
public void skipToPeek() {
this.index += this.peekOffset;
this.peekOffset = 0;
}
/// PRIVATE ////////////////////////////////////////////////////////////////////////////////////////////////////////
private Optional<Character> charAt(int offset) {
// When the cursor is at CRLF, return LF but don't move the cursor.
// The cursor still points to the EOL position, which in this case is the
// beginning of the compound CRLF sequence. This ensures slices of
// [inclusive, exclusive) continue to work properly.
var ch = getCharAt(string, offset);
var opt = ch.filter(v -> v == '\r')
.flatMap(x -> getCharAt(string, offset + 1).filter(v -> v == '\n'));
return opt.isPresent() ? opt : ch;
}
}

View File

@@ -0,0 +1,165 @@
package ru.di9.fluent.syntax.processor;
import ru.di9.fluent.syntax.ast.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
public class Processor {
private static final java.util.regex.Pattern SPECIAL
= java.util.regex.Pattern.compile("\\\\(([\\\\\"])|(u[0-9a-fA-F]{4})|(U[0-90a-fA-F]{6}))");
public Pattern unescapeLiteralsToText(Pattern pattern) {
var result = new Pattern();
textFromLiterals(pattern, result.getElements());
return result;
}
public Pattern escapeTextToLiterals(Pattern pattern) {
var result = new Pattern();
literalsFromText(pattern, result.getElements());
return result;
}
private void textFromLiterals(Pattern pattern, Collection<PatternElement> collection) {
TextElement lastText = null;
for (PatternElement element : pattern.getElements()) {
if (element instanceof TextElement txtElm) {
if (lastText == null) {
lastText = new TextElement(txtElm.getValue());
} else {
lastText.setValue(lastText.getValue() + txtElm.getValue());
}
} else if (element instanceof Placeable plcElm) {
InsidePlaceable expression = plcElm.getExpression();
if (expression instanceof StringLiteral strExp) {
String content = strExp.getValue();
{
Matcher matcher = SPECIAL.matcher(content);
var sb = new StringBuilder();
while (matcher.find()) {
String replacement = unescape(matcher.toMatchResult()).replace("\\", "\\\\");
matcher.appendReplacement(sb, replacement);
}
matcher.appendTail(sb);
content = sb.toString();
}
if (lastText == null) {
lastText = new TextElement("");
}
lastText.setValue(lastText.getValue() + content);
} else if (expression instanceof SelectExpression selExp) {
List<Variant> processedVariants = new ArrayList<>();
for (Variant variant : selExp.getVariants()) {
processedVariants.add(new Variant(variant.getKey(), unescapeLiteralsToText(variant.getValue()), variant.isDefault()));
}
var processedSelect = new SelectExpression(selExp.getSelector(), processedVariants);
var placeable = new Placeable(processedSelect);
if (lastText != null) {
collection.add(lastText);
lastText = null;
}
collection.add(placeable);
} else {
if (lastText != null) {
collection.add(lastText);
lastText = null;
}
collection.add(plcElm);
}
}
}
if (lastText != null) {
collection.add(lastText);
}
}
private void literalsFromText(Pattern pattern, Collection<PatternElement> collection) {
for (PatternElement element : pattern.getElements()) {
if (element instanceof TextElement txtElm) {
if (txtElm.getValue().startsWith(" ") || txtElm.getValue().startsWith("\n")) {
collection.add(new Placeable(new StringLiteral("")));
}
int startIndex = 0;
for (int i = 0; i < txtElm.getValue().length(); i++) {
char ch = txtElm.getValue().charAt(i);
if (ch == '{' || ch == '}') {
String before = txtElm.getValue().substring(startIndex, i);
if (!before.isEmpty()) {
collection.add(new TextElement(before));
}
collection.add(new Placeable(new StringLiteral(String.valueOf(ch))));
startIndex = i + 1;
} else if (ch == '[' || ch == '*' || ch == '.') {
if (i > 0 && txtElm.getValue().charAt(i - 1) == '\n') {
String before = txtElm.getValue().substring(startIndex, i);
collection.add(new TextElement(before));
collection.add(new Placeable(new StringLiteral(String.valueOf(ch))));
startIndex = i + 1;
}
}
}
if ((txtElm.getValue().length() - 1) > startIndex) {
collection.add(new TextElement(txtElm.getValue().substring(startIndex)));
}
if (txtElm.getValue().endsWith(" ") || txtElm.getValue().endsWith("\n")) {
collection.add(new Placeable(new StringLiteral("")));
}
} else if (element instanceof Placeable plcElm) {
if (plcElm.getExpression() instanceof SelectExpression selExp) {
List<Variant> rawVariants = new ArrayList<>();
for (Variant variant : selExp.getVariants()) {
rawVariants.add(new Variant(variant.getKey(), escapeTextToLiterals(variant.getValue()), variant.isDefault()));
}
collection.add(new Placeable(new SelectExpression(selExp.getSelector(), rawVariants)));
} else {
collection.add(element);
}
}
}
}
private static String unescape(MatchResult matchResult) {
List<String> groupValues = new ArrayList<>();
for (int i = 2; i < matchResult.groupCount() + 1; i++) {
groupValues.add(matchResult.group(i) != null ? matchResult.group(i) : "");
}
Iterator<String> matches = groupValues.iterator();
String simple = matches.next();
if (!simple.isEmpty()) {
return simple;
}
String uni4 = matches.next();
if (!uni4.isEmpty()) {
int codepoint = Integer.valueOf(uni4.substring(1), 16);
if (Character.isBmpCodePoint(codepoint)) {
char chr = (char) codepoint;
if (!Character.isSurrogate(chr)) {
return Character.toString(chr);
}
}
}
String uni6 = matches.next();
if (!uni6.isEmpty()) {
int codepoint = Integer.valueOf(uni6.substring(1), 16);
if (Character.isValidCodePoint(codepoint)) {
char chr = (char) codepoint;
if (!Character.isSurrogate(chr)) {
return String.valueOf(Character.highSurrogate(codepoint)) + Character.lowSurrogate(codepoint);
}
}
}
return "\uFFFD"; // <20>
}
}

View File

@@ -0,0 +1,269 @@
package ru.di9.fluent.syntax.serializer;
import ru.di9.fluent.syntax.ast.*;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Collectors;
public class FluentSerializer {
public boolean withJunk = false;
public String serialize(Resource resource) {
return resource.getBody()
.stream()
.map(this::serialize)
.collect(Collectors.joining());
}
public String serialize(TopLevel topLevel) {
if (topLevel instanceof Entry entry) {
return serializeEntry(entry);
} else if (topLevel instanceof Whitespace whitespace) {
return whitespace.getContent();
} else if (topLevel instanceof Junk junk) {
return withJunk ? junk.getContent() : "";
} else {
throw new SerializeException("Unknown top-level entry type '%s'".formatted(topLevel.getClass()));
}
}
/// PRIVATE ////////////////////////////////////////////////////////////////////////////////////////////////////////
private String serializeEntry(Entry entry) {
if (entry instanceof Message message) {
return serializeMessage(message);
} else if (entry instanceof Term term) {
return serializeTerm(term);
} else if (entry instanceof Comment comment) {
return serializeComment(comment, "#");
} else if (entry instanceof GroupComment comment) {
return serializeComment(comment, "##");
} else if (entry instanceof ResourceComment comment) {
return serializeComment(comment, "###");
} else {
throw new SerializeException("Unknown entry type '%s'".formatted(entry.getClass()));
}
}
private String serializeMessage(Message message) {
var builder = new StringBuilder();
message.getComment().ifPresent(comment ->
builder.append(serializeComment(comment, "#")));
builder.append(message.getId().getName()).append(" =");
message.getValue().ifPresent(value ->
builder.append(serializePattern(value)));
for (Attribute attribute : message.getAttributes()) {
builder.append(serializeAttribute(attribute));
}
return builder.append('\n').toString();
}
private String serializeTerm(Term term) {
var builder = new StringBuilder();
term.getComment().ifPresent(comment ->
builder.append(serializeComment(comment, "#")));
builder
.append('-').append(term.getId().getName()).append(" =")
.append(serializePattern(term.getValue()));
for (Attribute attribute : term.getAttributes()) {
builder.append(serializeAttribute(attribute));
}
return builder.append('\n').toString();
}
private String serializeComment(BaseComment comment, String prefix) {
return Arrays.stream(comment.getContent().split("\n"))
.map(line -> {
if (line.isEmpty()) {
return prefix + "\n";
} else {
return prefix + " " + line + "\n";
}
})
.collect(Collectors.joining());
}
private String serializePattern(Pattern pattern) {
var builder = new StringBuilder();
String content = pattern.getElements()
.stream()
.map(this::serializeElement)
.filter(Objects::nonNull)
.map(string -> string.replaceAll("\n", "\n "))
.collect(Collectors.joining());
if (shouldStartOnNewLine(pattern)) {
builder.append("\n ");
} else {
builder.append(' ');
}
return builder.append(content).toString();
}
private String serializeAttribute(Attribute attribute) {
String value = serializePattern(attribute.getValue()).replaceAll("\n", "\n ");
return "\n ." + attribute.getId().getName() + " =" + value;
}
private String serializeElement(PatternElement patternElement) {
if (patternElement instanceof TextElement textElement) {
return textElement.getValue();
} else if (patternElement instanceof Placeable placeable) {
return serializePlaceable(placeable);
} else {
throw new SerializeException("Unknown element type: '%s".formatted(patternElement.getClass()));
}
}
private String serializePlaceable(Placeable placeable) {
InsidePlaceable expression = placeable.getExpression();
if (expression instanceof Placeable placeable1) {
return "{" + serializePlaceable(placeable1) + "}";
} else if (expression instanceof SelectExpression selectExpression) {
return "{ " + serializeExpression(selectExpression) + "}";
} else if (expression instanceof Expression expression1) {
return "{ " + serializeExpression(expression1) + " }";
} else {
throw new SerializeException("Unknown placeable type '%s'".formatted(expression.getClass()));
}
}
private String serializeExpression(Expression expression) {
var builder = new StringBuilder();
if (expression instanceof StringLiteral stringLiteral) {
builder.append('"').append(stringLiteral.getValue()).append('"');
} else if (expression instanceof NumberLiteral numberLiteral) {
builder.append(numberLiteral.getValue());
} else if (expression instanceof VariableReference variableReference) {
builder.append('$').append(variableReference.getId().getName());
} else if (expression instanceof TermReference termReference) {
builder.append('-').append(termReference.getId().getName());
termReference.getAttribute().ifPresent(attribute ->
builder.append('.').append(attribute.getName()));
termReference.getArguments().ifPresent(arguments ->
builder.append(serializeCallArguments(arguments)));
} else if (expression instanceof MessageReference messageReference) {
builder.append(messageReference.getId().getName());
messageReference.getAttribute().ifPresent(attribute ->
builder.append('.').append(attribute.getName()));
} else if (expression instanceof FunctionReference functionReference) {
builder
.append(functionReference.getId().getName())
.append(serializeCallArguments(functionReference.getArguments()));
} else if (expression instanceof SelectExpression selectExpression) {
builder.append(serializeExpression(selectExpression.getSelector())).append(" ->");
for (Variant variant : selectExpression.getVariants()) {
builder.append(serializeVariant(variant));
}
builder.append('\n');
} else {
throw new SerializeException("Unknown expression type '%s".formatted(expression.getClass()));
}
return builder.toString();
}
private String serializeCallArguments(CallArguments callArguments) {
boolean hasPositional = !callArguments.getPositional().isEmpty();
boolean hasNamed = !callArguments.getNamed().isEmpty();
var builder = new StringBuilder("(");
if (hasPositional) {
var positional = callArguments.getPositional()
.stream()
.map(this::serializeExpression)
.collect(Collectors.joining(", "));
builder.append(positional);
}
if (hasNamed) {
var named = callArguments.getNamed()
.stream()
.map(this::serializeNamedArgument)
.collect(Collectors.joining(", "));
if (hasPositional) {
builder.append(", ");
}
builder.append(named);
}
return builder.append(')').toString();
}
private String serializeVariant(Variant variant) {
var key = serializeVariantKey(variant.getKey());
var value = serializePattern(variant.getValue()).replaceAll("\n", "\n ");
var builder = new StringBuilder("\n ");
if (variant.isDefault()) {
builder.append('*');
} else {
builder.append(' ');
}
return builder.append('[').append(key).append(']').append(value).toString();
}
private String serializeNamedArgument(NamedArgument namedArgument) {
return namedArgument.getName().getName() + ": " + serializeExpression(namedArgument.getValue());
}
private String serializeVariantKey(VariantKey variantKey) {
if (variantKey instanceof Identifier identifier) {
return identifier.getName();
} else if (variantKey instanceof NumberLiteral numberLiteral) {
return numberLiteral.getValue();
} else {
throw new SerializeException("Unknown variant key type '%s'".formatted(variantKey.getClass()));
}
}
private boolean shouldStartOnNewLine(Pattern pattern) {
boolean isMultiline = pattern.getElements().stream().anyMatch(it -> isSelectExpr(it) || includesLine(it));
if (isMultiline) {
if (!pattern.getElements().isEmpty()) {
var firstElement = pattern.getElements().get(0);
if (firstElement instanceof TextElement te) {
if (!te.getValue().isEmpty()) {
char firstChar = te.getValue().charAt(0);
// Due to the indentation requirement the following characters may not appear
// as the first character on a new line.
return firstChar != '[' && firstChar != '.' && firstChar != '*';
}
}
}
return true;
}
return false;
}
private boolean isSelectExpr(PatternElement patternElement) {
return patternElement instanceof Placeable pl
&& pl.getExpression() instanceof SelectExpression;
}
private boolean includesLine(PatternElement patternElement) {
return patternElement instanceof TextElement te
&& te.getValue().contains("\n");
}
}

View File

@@ -0,0 +1,7 @@
package ru.di9.fluent.syntax.serializer;
public class SerializeException extends RuntimeException {
public SerializeException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,113 @@
package ru.di9.fluent.syntax.visitor;
import ru.di9.fluent.syntax.ast.BaseNode;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
public class Visitor {
private final Map<String, Method> handlers = new HashMap<>();
public Visitor() {
Method[] methods = this.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.getName().startsWith("visit")) {
handlers.put(method.getName().substring("visit".length()), method);
}
}
}
public void visit(BaseNode node) {
String cName = node.getClass().getSimpleName();
Method handler = handlers.get(cName);
if (handler != null) {
try {
handler.invoke(this, node);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
} else {
genericVisit(node);
}
}
public void genericVisit(BaseNode node) {
Iterator<Pair> itr = childrenOf(node);
while (itr.hasNext()) {
Pair pair = itr.next();
Object value = pair.invokeResult();
if (value instanceof Optional<?> opt) {
value = opt.orElse(null);
}
if (value instanceof BaseNode baseNode) {
this.visit(baseNode);
} else if (value instanceof Collection<?> collection) {
for (Object it : collection) {
if (it instanceof BaseNode baseNode) {
this.visit(baseNode);
}
}
}
}
}
record Pair(String name, Object invokeResult){}
static Iterator<Pair> childrenOf(BaseNode node) {
final Method[] methods = node.getClass().getMethods();
Arrays.sort(methods, Comparator.comparing(Method::getName));
return new Iterator<>() {
private int idx = 0;
private Method nextMethod;
@Override
public boolean hasNext() {
if (nextMethod != null) return true;
else if (idx >= methods.length) return false;
return (nextMethod = loadNext()) != null;
}
@Override
public Pair next() {
if (idx >= methods.length) {
throw new NoSuchElementException();
} else if (nextMethod == null) {
if ((nextMethod = loadNext()) == null) {
throw new NoSuchElementException();
}
}
Object invokeResult;
try {
invokeResult = nextMethod.invoke(node);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
var pair = new Pair(nextMethod.getName(), invokeResult);
nextMethod = null;
return pair;
}
private Method loadNext() {
do {
Method method = methods[idx++];
if (Modifier.isPublic(method.getModifiers())
&& method.getName().startsWith("get")
&& method.getParameterCount() == 0
&& !method.getName().equals("getClass")) {
return method;
}
} while (idx < methods.length);
return null;
}
};
}
}

View File

@@ -0,0 +1,15 @@
package ru.di9.fluent.syntax;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class MathUtilsTest {
@Test
void clamp() {
assertThat(MathUtils.clamp(7, 1, 10)).isEqualTo(7);
assertThat(MathUtils.clamp(0, 1, 10)).isEqualTo(1);
assertThat(MathUtils.clamp(11, 1, 10)).isEqualTo(10);
}
}

View File

@@ -0,0 +1,33 @@
package ru.di9.fluent.syntax;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Optional;
class StringUtilsTest {
@Test
void testGetChar() {
String string = "abcd";
Optional<Character> chr = StringUtils.getCharAt(string, 0);
Assertions.assertThat(chr)
.hasValue('a');
chr = StringUtils.getCharAt(string, 4);
Assertions.assertThat(chr)
.isEmpty();
chr = StringUtils.getCharAt(null, 0);
Assertions.assertThat(chr)
.isEmpty();
chr = StringUtils.getCharAt("", 0);
Assertions.assertThat(chr)
.isEmpty();
}
}

View File

@@ -0,0 +1,24 @@
package ru.di9.fluent.syntax.ast;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
class BaseNodeTest {
@Test
void testEquals() {
var m1 = new Message(new Identifier("test-id"), new Pattern(new TextElement("localized")));
var m11 = new Message(new Identifier("test-id"), new Pattern(new TextElement("localized")));
var m2 = new Message(new Identifier("test-id"), new Pattern(new TextElement("different")));
assertEquals(m1, m11);
assertNotEquals(m1, m2);
assertEquals(m1.getId(), m2.getId());
assertNotEquals(m1.getValue(), m2.getValue());
//Шта?
assertNotEquals(m1, null);
assertNotEquals(null, m1);
}
}

View File

@@ -0,0 +1,66 @@
package ru.di9.fluent.syntax.parser;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import ru.di9.fluent.test.utils.AstAssert;
import ru.di9.fluent.test.utils.Tuple3;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import static ru.di9.fluent.test.utils.FileUtils.getExt;
import static ru.di9.fluent.test.utils.FileUtils.getName;
public abstract class AbstractFixturesTest {
@TestFactory
public Iterable<DynamicTest> fixturesTest() throws IOException {
List<DynamicTest> tests = new ArrayList<>();
try (Stream<Path> walker = Files.walk(getFixturesPath())) {
walker
.filter(path -> path.toFile().isFile())
.filter(path -> getExt(path.toFile().getName()).equals("ftl"))
.map(path -> {
Path parent = path.getParent();
String name = getName(path.getFileName().toString());
try {
return new Tuple3(
name,
Files.readString(parent.resolve(name + ".ftl")),
Files.readString(parent.resolve(name + ".json"))
);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.map(tuple -> DynamicTest.dynamicTest(tuple.value1(), () -> {
var parser = new FluentParser();
parser.withSpans = isWithSpans(tuple.value1());
parser.withJunkAnnotations = isWithJunkAnnotations(tuple.value1());
var resource = parser.parse(tuple.value2());
AstAssert.assertThat(resource)
.isEqualAstJson(tuple.value3());
}))
.forEachOrdered(tests::add);
}
return tests;
}
protected abstract Path getFixturesPath();
protected boolean isWithSpans(String testName) {
return true;
}
protected boolean isWithJunkAnnotations(String testName) {
return true;
}
}

View File

@@ -0,0 +1,158 @@
package ru.di9.fluent.syntax.parser;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static ru.di9.fluent.syntax.parser.ParseException.ErrorCode.E0003;
class FluentStreamTest {
@Test
void testPeekBlankInline() {
var ps = new FluentStream(" ");
assertThat(ps.peekBlankInline()).isEqualTo(" ");
ps = new FluentStream(" | ");
assertThat(ps.peekBlankInline()).isEqualTo(" ");
}
@Test
void testPeekBlankBlock() {
var ps = new FluentStream(" \n \n ");
assertThat(ps.peekBlankBlock()).isEqualTo("\n\n");
ps = new FluentStream(" \n . ");
assertThat(ps.peekBlankBlock()).isEqualTo("\n");
}
@Test
void testSkipBlankBlock() {
var ps = new FluentStream(" \n \n ");
assertThat(ps.skipBlankBlock()).isEqualTo("\n\n");
assertThat(ps.index).isEqualTo(12);
assertThat(ps.peekOffset).isEqualTo(0);
}
@Test
void testExpectLineEnd() {
var ps1 = new FluentStream("");
assertThatNoException().isThrownBy(ps1::expectLineEnd);
var ps2 = new FluentStream("\n");
assertThatNoException().isThrownBy(ps2::expectLineEnd);
var ps3 = new FluentStream(" ");
var catchException = catchThrowableOfType(ps3::expectLineEnd, ParseException.class);
assertThat(catchException.getCode()).isEqualTo(E0003);
}
@Test
void testExpectChar() {
var ps = new FluentStream("z");
assertThatNoException().isThrownBy(() -> ps.expectChar('z'));
var catchException = catchThrowableOfType(() -> ps.expectChar('a'), ParseException.class);
assertThat(catchException.getCode()).isEqualTo(E0003);
}
@Test
void testTakeChar() {
var ps = new FluentStream("abc");
assertThat(ps.currentChar()).hasValue('a');
assertThat(ps.takeChar(c -> c == 'a')).hasValue('a');
assertThat(ps.currentChar()).hasValue('b');
assertThat(ps.takeChar(c -> c == 'c')).isEmpty();
assertThat(ps.currentChar()).hasValue('b');
}
@Test
void testIsNextLineComment() {
var ps = new FluentStream("# 123");
for (int i = 0; i < 5; i++) {
ps.next();
}
assertThat(ps.isNextLineComment(0)).isFalse();
var ps1 = new FluentStream("# 123\n# 456");
for (int i = 0; i < 5; i++) {
ps1.next();
}
assertThat(ps1.isNextLineComment(0)).isTrue();
var ps2 = new FluentStream("# 123\nkey = value");
for (int i = 0; i < 5; i++) {
ps2.next();
}
assertThat(ps2.isNextLineComment(0)).isFalse();
var ps3 = new FluentStream("# 123");
assertThat(ps3.isNextLineComment(0)).isFalse();
}
@Test
void testSkipBlankInline() {
var ps = new FluentStream(" \n123");
assertThat(ps.skipBlankInline()).isEqualTo(" ");
assertThat(ps.index).isEqualTo(4);
assertThat(ps.peekOffset).isEqualTo(0);
assertThat(ps.currentChar()).hasValue('\n');
}
@Test
void testPeekBlank() {
var ps = new FluentStream(" 123");
assertThat(ps.currentChar()).hasValue(' ');
assertThat(ps.currentPeek()).hasValue(' ');
ps.peekBlank();
assertThat(ps.currentChar()).hasValue(' ');
assertThat(ps.currentPeek()).hasValue('1');
}
@Test
void testSkipBlank() {
var ps = new FluentStream(" 123");
assertThat(ps.currentChar()).hasValue(' ');
assertThat(ps.currentPeek()).hasValue(' ');
ps.skipBlank();
assertThat(ps.currentChar()).hasValue('1');
assertThat(ps.currentPeek()).hasValue('1');
}
@Test
void testIsNumberStart() {
var ps = new FluentStream("123");
assertThat(ps.isNumberStart()).isTrue();
var ps1 = new FluentStream("-123");
assertThat(ps1.isNumberStart()).isTrue();
var ps2 = new FluentStream("a123");
assertThat(ps2.isNumberStart()).isFalse();
}
@Test
void testIsIdentifierStart() {
var ps = new FluentStream("foo = Bar");
assertThat(ps.isIdentifierStart()).isTrue();
var ps1 = new FluentStream("# foo = Bar");
assertThat(ps1.isIdentifierStart()).isFalse();
}
@Test
void testIsValueStart() {
var ps = new FluentStream("foo = Bar\n");
assertThat(ps.isValueStart()).isTrue();
for (int i = 0; i < 9; i++) {
ps.next();
}
assertThat(ps.isValueStart()).isFalse();
}
}

View File

@@ -0,0 +1,181 @@
package ru.di9.fluent.syntax.parser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class ParserStreamTest {
ParserStream ps;
@BeforeEach
void setUp() {
ps = new ParserStream("abcd");
}
@Test
void next() {
assertThat(ps.currentChar()).hasValue('a');
assertThat(ps.index).isEqualTo(0);
assertThat(ps.next()).hasValue('b');
assertThat(ps.currentChar()).hasValue('b');
assertThat(ps.index).isEqualTo(1);
assertThat(ps.next()).hasValue('c');
assertThat(ps.currentChar()).hasValue('c');
assertThat(ps.index).isEqualTo(2);
assertThat(ps.next()).hasValue('d');
assertThat(ps.currentChar()).hasValue('d');
assertThat(ps.index).isEqualTo(3);
assertThat(ps.next()).isEmpty();
assertThat(ps.currentChar()).isEmpty();
assertThat(ps.index).isEqualTo(4);
}
@Test
void peek() {
assertThat(ps.currentPeek()).hasValue('a');
assertThat(ps.peekOffset).isEqualTo(0);
assertThat(ps.peek()).hasValue('b');
assertThat(ps.currentPeek()).hasValue('b');
assertThat(ps.peekOffset).isEqualTo(1);
assertThat(ps.peek()).hasValue('c');
assertThat(ps.currentPeek()).hasValue('c');
assertThat(ps.peekOffset).isEqualTo(2);
assertThat(ps.peek()).hasValue('d');
assertThat(ps.currentPeek()).hasValue('d');
assertThat(ps.peekOffset).isEqualTo(3);
assertThat(ps.peek()).isEmpty();
assertThat(ps.currentPeek()).isEmpty();
assertThat(ps.peekOffset).isEqualTo(4);
}
@Test
void peekAndNext() {
assertThat(ps.peek()).hasValue('b');
assertThat(ps.peekOffset).isEqualTo(1);
assertThat(ps.index).isEqualTo(0);
assertThat(ps.next()).hasValue('b');
assertThat(ps.peekOffset).isEqualTo(0);
assertThat(ps.index).isEqualTo(1);
assertThat(ps.peek()).hasValue('c');
assertThat(ps.peekOffset).isEqualTo(1);
assertThat(ps.index).isEqualTo(1);
assertThat(ps.next()).hasValue('c');
assertThat(ps.peekOffset).isEqualTo(0);
assertThat(ps.index).isEqualTo(2);
assertThat(ps.currentChar()).hasValue('c');
assertThat(ps.currentPeek()).hasValue('c');
assertThat(ps.peek()).hasValue('d');
assertThat(ps.peekOffset).isEqualTo(1);
assertThat(ps.index).isEqualTo(2);
assertThat(ps.next()).hasValue('d');
assertThat(ps.peekOffset).isEqualTo(0);
assertThat(ps.index).isEqualTo(3);
assertThat(ps.currentChar()).hasValue('d');
assertThat(ps.currentPeek()).hasValue('d');
assertThat(ps.peek()).isEmpty();
assertThat(ps.peekOffset).isEqualTo(1);
assertThat(ps.index).isEqualTo(3);
assertThat(ps.currentChar()).hasValue('d');
assertThat(ps.currentPeek()).isEmpty();
assertThat(ps.peek()).isEmpty();
assertThat(ps.peekOffset).isEqualTo(2);
assertThat(ps.index).isEqualTo(3);
assertThat(ps.next()).isEmpty();
assertThat(ps.peekOffset).isEqualTo(0);
assertThat(ps.index).isEqualTo(4);
}
@Test
void skipAndPeek() {
ps.peek();
ps.peek();
ps.skipToPeek();
assertThat(ps.currentChar()).hasValue('c');
assertThat(ps.currentPeek()).hasValue('c');
assertThat(ps.peekOffset).isEqualTo(0);
assertThat(ps.index).isEqualTo(2);
ps.peek();
assertThat(ps.currentChar()).hasValue('c');
assertThat(ps.currentPeek()).hasValue('d');
assertThat(ps.peekOffset).isEqualTo(1);
assertThat(ps.index).isEqualTo(2);
ps.next();
assertThat(ps.currentChar()).hasValue('d');
assertThat(ps.currentPeek()).hasValue('d');
assertThat(ps.peekOffset).isEqualTo(0);
assertThat(ps.index).isEqualTo(3);
}
@Test
void resetPeek() {
ps.next();
ps.peek();
ps.peek();
ps.resetPeek();
assertThat(ps.currentChar()).hasValue('b');
assertThat(ps.currentPeek()).hasValue('b');
assertThat(ps.peekOffset).isEqualTo(0);
assertThat(ps.index).isEqualTo(1);
ps.peek();
assertThat(ps.currentChar()).hasValue('b');
assertThat(ps.currentPeek()).hasValue('c');
assertThat(ps.peekOffset).isEqualTo(1);
assertThat(ps.index).isEqualTo(1);
ps.peek();
ps.peek();
ps.peek();
ps.resetPeek();
assertThat(ps.currentChar()).hasValue('b');
assertThat(ps.currentPeek()).hasValue('b');
assertThat(ps.peekOffset).isEqualTo(0);
assertThat(ps.index).isEqualTo(1);
assertThat(ps.peek()).hasValue('c');
assertThat(ps.currentChar()).hasValue('b');
assertThat(ps.currentPeek()).hasValue('c');
assertThat(ps.peekOffset).isEqualTo(1);
assertThat(ps.index).isEqualTo(1);
assertThat(ps.peek()).hasValue('d');
assertThat(ps.peek()).isEmpty();
}
@Test
void indexOutOfRange() {
ps.next();
ps.next();
ps.next();
ps.next();
assertThat(ps.index).isGreaterThanOrEqualTo(ps.string.length());
assertThat(ps.next()).isEmpty();
}
}

View File

@@ -0,0 +1,21 @@
package ru.di9.fluent.syntax.parser;
import java.nio.file.Path;
public class ReferenceTest extends AbstractFixturesTest {
@Override
protected Path getFixturesPath() {
return Path.of("src", "test", "resources", "reference_fixtures");
}
@Override
protected boolean isWithSpans(String testName) {
return !testName.equals("crlf") && !testName.equals("select_expressions");
}
@Override
protected boolean isWithJunkAnnotations(String testName) {
return !testName.equals("crlf") && !testName.equals("select_expressions");
}
}

View File

@@ -0,0 +1,11 @@
package ru.di9.fluent.syntax.parser;
import java.nio.file.Path;
public class StructureTest extends AbstractFixturesTest {
@Override
protected Path getFixturesPath() {
return Path.of("src", "test", "resources", "structure_fixtures");
}
}

View File

@@ -0,0 +1,210 @@
package ru.di9.fluent.syntax.processor;
import org.junit.jupiter.api.Test;
import ru.di9.fluent.syntax.ast.*;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ProcessorTest {
@Test
void toProcessedPattern() {
var processor = new Processor();
var pattern = new Pattern();
pattern.getElements().clear();
pattern.getElements().add(new TextElement("Hi"));
assertEquals(pattern, processor.unescapeLiteralsToText(pattern));
pattern.getElements().clear();
pattern.getElements().addAll(List.of(
new Placeable(new StringLiteral("\\\\")),
new TextElement(" "),
new Placeable(new StringLiteral("\\\""))
));
assertEquals(
new Pattern(new TextElement("\\ \"")),
processor.unescapeLiteralsToText(pattern)
);
pattern.getElements().clear();
pattern.getElements().addAll(List.of(
new TextElement("Foo "),
new Placeable(new StringLiteral("Bar"))
));
assertEquals(
new Pattern(new TextElement("Foo Bar")),
processor.unescapeLiteralsToText(pattern)
);
assertEquals(
new Pattern(
new TextElement("Foo "),
new Placeable(new StringLiteral("Bar"))
),
pattern
);
pattern.getElements().clear();
pattern.getElements().addAll(List.of(
new TextElement("Hi,"),
new Placeable(new StringLiteral("\\u0020")),
new TextElement("there")
));
assertEquals(
new Pattern(new TextElement("Hi, there")),
processor.unescapeLiteralsToText(pattern)
);
pattern.getElements().clear();
pattern.getElements().addAll(List.of(
new TextElement("Emoji: "),
new Placeable(new StringLiteral("\\U01f602"))
));
assertEquals(
new Pattern(new TextElement("Emoji: \uD83D\uDE02")),
processor.unescapeLiteralsToText(pattern)
);
assertEquals(
new Pattern(new TextElement("Emoji: 😂")),
processor.unescapeLiteralsToText(pattern)
);
pattern.getElements().clear();
pattern.getElements().addAll(List.of(
new TextElement("Illegal escape sequence: "),
new Placeable(new StringLiteral("\\ud800"))
));
assertEquals(
new Pattern(new TextElement("Illegal escape sequence: <20>")),
processor.unescapeLiteralsToText(pattern)
);
pattern.getElements().clear();
pattern.getElements().addAll(List.of(
new TextElement("Illegal escape sequence: "),
new Placeable(new StringLiteral("\\U00d800"))
));
assertEquals(
new Pattern(new TextElement("Illegal escape sequence: <20>")),
processor.unescapeLiteralsToText(pattern)
);
pattern.getElements().clear();
pattern.getElements().addAll(List.of(
new TextElement("Hi, "),
new Placeable(new StringLiteral("{")),
new TextElement(" there")
));
assertEquals(
new Pattern(new TextElement("Hi, { there")),
processor.unescapeLiteralsToText(pattern)
);
pattern.getElements().clear();
pattern.getElements().addAll(List.of(
new TextElement("Foo\n"),
new Placeable(new StringLiteral(".")),
new TextElement("bar")
));
assertEquals(
new Pattern(new TextElement("Foo\n.bar")),
processor.unescapeLiteralsToText(pattern)
);
pattern.getElements().clear();
pattern.getElements().addAll(List.of(
new TextElement("Foo "),
new Placeable(
new SelectExpression(
new NumberLiteral("1"),
List.of(
new Variant(
new Identifier("other"),
new Pattern(
new TextElement("bar "),
new Placeable(
new StringLiteral("{-_-}")
)
),
true
)
)
)
),
new TextElement(" baz")
));
assertEquals(
new Pattern(
new TextElement("Foo "),
new Placeable(
new SelectExpression(
new NumberLiteral("1"),
List.of(
new Variant(
new Identifier("other"),
new Pattern(new TextElement("bar {-_-}")),
true)
)
)
),
new TextElement(" baz")
),
processor.unescapeLiteralsToText(pattern)
);
}
@Test
void toRawPattern() {
var processor = new Processor();
var pattern = new Pattern();
pattern.getElements().clear();
pattern.getElements().add(new TextElement("Hi"));
assertEquals(pattern, processor.escapeTextToLiterals(pattern));
pattern.getElements().clear();
pattern.getElements().add(new TextElement("\\ \""));
assertEquals(pattern, processor.escapeTextToLiterals(pattern));
pattern.getElements().clear();
pattern.getElements().add(new TextElement("Hi, {-_-}"));
assertEquals(
new Pattern(
new TextElement("Hi, "),
new Placeable(new StringLiteral("{")),
new TextElement("-_-"),
new Placeable(new StringLiteral("}"))
),
processor.escapeTextToLiterals(pattern)
);
pattern.getElements().clear();
pattern.getElements().add(new TextElement("Foo\nbar"));
assertEquals(pattern, processor.escapeTextToLiterals(pattern));
pattern.getElements().clear();
pattern.getElements().add(new TextElement("Foo\n*bar"));
assertEquals(
new Pattern(
new TextElement("Foo\n"),
new Placeable(new StringLiteral("*")),
new TextElement("bar")
),
processor.escapeTextToLiterals(pattern)
);
pattern.getElements().clear();
pattern.getElements().add(new TextElement("\nFoo\nbar "));
assertEquals(
new Pattern(
new Placeable(new StringLiteral("")),
new TextElement("\nFoo\nbar "),
new Placeable(new StringLiteral(""))
),
processor.escapeTextToLiterals(pattern)
);
}
}

View File

@@ -0,0 +1,24 @@
package ru.di9.fluent.syntax.serializer;
import org.junit.jupiter.api.Test;
import ru.di9.fluent.syntax.parser.FluentParser;
import static org.assertj.core.api.Assertions.assertThat;
class SerializeEntryTest {
@Test
void test() {
String input = """
# Attached comment
key = Value""";
var parser = new FluentParser();
var topLevel = parser.parse(input).getBody().get(0);
var serializer = new FluentSerializer();
var serialized = serializer.serialize(topLevel).trim();
assertThat(serialized).isEqualTo(input);
}
}

View File

@@ -0,0 +1,68 @@
package ru.di9.fluent.syntax.serializer;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import ru.di9.fluent.syntax.ast.Resource;
import ru.di9.fluent.syntax.parser.FluentParser;
import ru.di9.fluent.test.utils.Tuple3;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static ru.di9.fluent.test.utils.FileUtils.getExt;
import static ru.di9.fluent.test.utils.FileUtils.getName;
class SerializeResourceTest {
@TestFactory
Iterable<DynamicTest> test() throws IOException {
SortedSet<DynamicTest> tests = new TreeSet<>(Comparator.comparing(DynamicNode::getDisplayName));
try (Stream<Path> walker = Files.walk(Path.of("src", "test", "resources", "serialized"))) {
walker
.filter(path -> path.toFile().isFile())
.filter(path -> getExt(path).equals("ftl"))
.map(path -> {
Path parent = path.getParent();
String name = getName(path);
try {
Path ftlSourcePath = parent.resolve(name + ".ftl");
Path expSourcePath = parent.resolve(name + ".exp.txt");
String ftlSource = Files.readString(ftlSourcePath);
String expSource;
if (Files.exists(expSourcePath)) {
expSource = Files.readString(expSourcePath);
} else {
expSource = ftlSource;
}
return new Tuple3(name, ftlSource, expSource);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.map(tuple -> DynamicTest.dynamicTest(tuple.value1(), () -> {
FluentParser parser = new FluentParser();
Resource resource = parser.parse(tuple.value2());
FluentSerializer serializer = new FluentSerializer();
String serialized = serializer.serialize(resource);
assertThat(serialized).isEqualTo(tuple.value3());
}))
.forEach(tests::add);
}
return tests;
}
}

View File

@@ -0,0 +1,80 @@
package ru.di9.fluent.syntax.visitor;
import lombok.Getter;
import lombok.Setter;
import org.junit.jupiter.api.Test;
import ru.di9.fluent.syntax.ast.Identifier;
import ru.di9.fluent.syntax.ast.Pattern;
import ru.di9.fluent.syntax.ast.TextElement;
import ru.di9.fluent.syntax.ast.Variant;
import ru.di9.fluent.syntax.parser.FluentParser;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.regex.Matcher;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static org.junit.jupiter.api.Assertions.*;
class VisitorTest {
FluentParser parser = new FluentParser();
@Test
void test_basics() {
var visitor = new TestableVisitor();
var source = """
msg = foo {$var ->
*[other] bar
} baz""";
var res = parser.parse(source);
visitor.visit(res);
assertEquals(3, visitor.wordCount);
assertEquals(2, visitor.patternCount);
assertEquals(1, visitor.variantCount);
}
@Test
void test_childrenOf() {
var variant = new Variant(new Identifier("other"), new Pattern(), true);
Iterator<Visitor.Pair> itr = Visitor.childrenOf(variant);
Spliterator<Visitor.Pair> spliterator = Spliterators.spliteratorUnknownSize(itr, Spliterator.ORDERED);
Stream<Visitor.Pair> stream = StreamSupport.stream(spliterator, false);
assertEquals(
List.of("getKey", "getSpan", "getValue"),
stream.map(Visitor.Pair::name).sorted().toList()
);
}
@Getter
@Setter
@SuppressWarnings("unused")
static class TestableVisitor extends Visitor {
private static final java.util.regex.Pattern WORDS = java.util.regex.Pattern.compile("\\w+");
private int patternCount = 0;
private int variantCount = 0;
private int wordCount = 0;
public void visitPattern(Pattern node) {
super.genericVisit(node);
patternCount++;
}
public void visitVariant(Variant node) {
super.genericVisit(node);
variantCount++;
}
public void visitTextElement(TextElement node) {
Matcher matcher = WORDS.matcher(node.getValue());
while (matcher.find()) {
wordCount++;
}
}
}
}

View File

@@ -0,0 +1,49 @@
package ru.di9.fluent.test.utils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.assertj.core.api.AbstractAssert;
import org.json.JSONException;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import ru.di9.fluent.syntax.ast.BaseNode;
import java.io.PrintStream;
@SuppressWarnings("UnusedReturnValue")
public class AstAssert extends AbstractAssert<AstAssert, BaseNode> {
private final Gson gson;
private final NullIgnoreComparator nullIgnoreComparator;
protected AstAssert(BaseNode actualNode) {
super(actualNode, AstAssert.class);
gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeHierarchyAdapter(BaseNode.class, new BaseNodeJsonSerializer())
.create();
nullIgnoreComparator = new NullIgnoreComparator(JSONCompareMode.STRICT);
}
@SuppressWarnings("unused")
public AstAssert printAstJson(PrintStream printStream) {
var json = gson.toJson(actual);
printStream.println(json);
return this;
}
public AstAssert isEqualAstJson(String astJson) {
var actualJson = gson.toJson(actual);
try {
JSONAssert.assertEquals(astJson, actualJson, nullIgnoreComparator);
} catch (JSONException e) {
failWithMessage(e.getMessage());
}
return this;
}
public static AstAssert assertThat(BaseNode baseNode) {
return new AstAssert(baseNode);
}
}

View File

@@ -0,0 +1,43 @@
package ru.di9.fluent.test.utils;
import com.google.gson.*;
import org.joor.Reflect;
import ru.di9.fluent.syntax.ast.BaseNode;
import ru.di9.fluent.syntax.ast.Whitespace;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Map;
public class BaseNodeJsonSerializer implements JsonSerializer<BaseNode> {
@Override
public JsonElement serialize(BaseNode baseNode, Type type, JsonSerializationContext ctx) {
var root = new JsonObject();
root.add("type", new JsonPrimitive(baseNode.getClass().getSimpleName()));
var fields = Reflect.on(baseNode).fields();
for (Map.Entry<String, Reflect> field : fields.entrySet()) {
String name = field.getKey().equalsIgnoreCase("isDefault") ? "default" : field.getKey();
Object value = field.getValue().get();
if (value instanceof Collection<?> collection) {
var jsonArray = new JsonArray();
for (Object item : collection) {
if (item instanceof Whitespace) {
continue;
}
jsonArray.add(ctx.serialize(item));
}
root.add(name, jsonArray);
} else {
root.add(name, ctx.serialize(value));
}
}
return root;
}
}

View File

@@ -0,0 +1,26 @@
package ru.di9.fluent.test.utils;
import java.nio.file.Path;
public interface FileUtils {
static String getExt(String fileName) {
var idx = fileName.lastIndexOf('.');
if (idx < 0) return "";
return fileName.substring(idx + 1);
}
static String getName(String fileName) {
var idx = fileName.lastIndexOf('.');
if (idx < 0) return fileName;
return fileName.substring(0, idx);
}
static String getName(Path pathToFile) {
return getName(pathToFile.getFileName().toString());
}
static String getExt(Path pathToFile) {
return getExt(pathToFile.getFileName().toString());
}
}

View File

@@ -0,0 +1,32 @@
package ru.di9.fluent.test.utils;
import org.json.JSONException;
import org.json.JSONObject;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.skyscreamer.jsonassert.JSONCompareResult;
import org.skyscreamer.jsonassert.comparator.DefaultComparator;
import java.util.Set;
import static org.skyscreamer.jsonassert.comparator.JSONCompareUtil.getKeys;
import static org.skyscreamer.jsonassert.comparator.JSONCompareUtil.qualify;
public class NullIgnoreComparator extends DefaultComparator {
public NullIgnoreComparator(JSONCompareMode mode) {
super(mode);
}
@Override
protected void checkJsonObjectKeysExpectedInActual(String prefix, JSONObject expected, JSONObject actual, JSONCompareResult result) throws JSONException {
Set<String> expectedKeys = getKeys(expected);
for (String key : expectedKeys) {
Object expectedValue = expected.get(key);
if (actual.has(key)) {
Object actualValue = actual.get(key);
compareValues(qualify(prefix, key), expectedValue, actualValue, result);
} else if (!expectedValue.equals(JSONObject.NULL)) {
result.missing(prefix, key);
}
}
}
}

View File

@@ -0,0 +1,4 @@
package ru.di9.fluent.test.utils;
public record Tuple3(String value1, String value2, String value3) {
}

View File

@@ -0,0 +1,2 @@
crlf.ftl eol=crlf
cr.ftl eol=cr

View File

@@ -0,0 +1,8 @@
# ↓ BEL, U+0007
control0 = abcdef
# ↓ DEL, U+007F
delete = abcdef
# ↓ BPM, U+0082
control1 = abc‚def

View File

@@ -0,0 +1,148 @@
{
"type": "Resource",
"body": [
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "control0",
"span": {
"type": "Span",
"start": 28,
"end": 36
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "abc\u0007def",
"span": {
"type": "Span",
"start": 39,
"end": 46
}
}
],
"span": {
"type": "Span",
"start": 39,
"end": 46
}
},
"attributes": [],
"comment": {
"content": " ↓ BEL, U+0007",
"type": "Comment",
"span": {
"type": "Span",
"start": 0,
"end": 27
}
},
"span": {
"type": "Span",
"start": 0,
"end": 46
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "delete",
"span": {
"type": "Span",
"start": 74,
"end": 80
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "abcdef",
"span": {
"type": "Span",
"start": 83,
"end": 90
}
}
],
"span": {
"type": "Span",
"start": 83,
"end": 90
}
},
"attributes": [],
"comment": {
"content": " ↓ DEL, U+007F",
"type": "Comment",
"span": {
"type": "Span",
"start": 48,
"end": 73
}
},
"span": {
"type": "Span",
"start": 48,
"end": 90
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "control1",
"span": {
"type": "Span",
"start": 120,
"end": 128
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "abc‚def",
"span": {
"type": "Span",
"start": 131,
"end": 138
}
}
],
"span": {
"type": "Span",
"start": 131,
"end": 138
}
},
"attributes": [],
"comment": {
"content": " ↓ BPM, U+0082",
"type": "Comment",
"span": {
"type": "Span",
"start": 92,
"end": 119
}
},
"span": {
"type": "Span",
"start": 92,
"end": 138
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 139
}
}

View File

@@ -0,0 +1,20 @@
face-with-tears-of-joy = 😂
tetragram-for-centre = 𝌆
surrogates-in-text = \uD83D\uDE02
surrogates-in-string = {"\uD83D\uDE02"}
surrogates-in-adjacent-strings = {"\uD83D"}{"\uDE02"}
emoji-in-text = A face 😂 with tears of joy.
emoji-in-string = {"A face 😂 with tears of joy."}
# ERROR Invalid identifier
err-😂 = Value
# ERROR Invalid expression
err-invalid-expression = { 😂 }
# ERROR Invalid variant key
err-invalid-variant-key = { $sel ->
*[😂] Value
}

View File

@@ -0,0 +1,414 @@
{
"type": "Resource",
"body": [
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "face-with-tears-of-joy",
"span": {
"type": "Span",
"start": 0,
"end": 22
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "😂",
"span": {
"type": "Span",
"start": 25,
"end": 27
}
}
],
"span": {
"type": "Span",
"start": 25,
"end": 27
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 0,
"end": 27
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "tetragram-for-centre",
"span": {
"type": "Span",
"start": 28,
"end": 48
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "𝌆",
"span": {
"type": "Span",
"start": 51,
"end": 53
}
}
],
"span": {
"type": "Span",
"start": 51,
"end": 53
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 28,
"end": 53
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "surrogates-in-text",
"span": {
"type": "Span",
"start": 55,
"end": 73
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "\\uD83D\\uDE02",
"span": {
"type": "Span",
"start": 76,
"end": 88
}
}
],
"span": {
"type": "Span",
"start": 76,
"end": 88
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 55,
"end": 88
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "surrogates-in-string",
"span": {
"type": "Span",
"start": 89,
"end": 109
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "\\uD83D\\uDE02",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 113,
"end": 127
}
},
"span": {
"type": "Span",
"start": 112,
"end": 128
}
}
],
"span": {
"type": "Span",
"start": 112,
"end": 128
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 89,
"end": 128
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "surrogates-in-adjacent-strings",
"span": {
"type": "Span",
"start": 129,
"end": 159
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "\\uD83D",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 163,
"end": 171
}
},
"span": {
"type": "Span",
"start": 162,
"end": 172
}
},
{
"type": "Placeable",
"expression": {
"value": "\\uDE02",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 173,
"end": 181
}
},
"span": {
"type": "Span",
"start": 172,
"end": 182
}
}
],
"span": {
"type": "Span",
"start": 162,
"end": 182
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 129,
"end": 182
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "emoji-in-text",
"span": {
"type": "Span",
"start": 184,
"end": 197
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "A face 😂 with tears of joy.",
"span": {
"type": "Span",
"start": 200,
"end": 228
}
}
],
"span": {
"type": "Span",
"start": 200,
"end": 228
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 184,
"end": 228
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "emoji-in-string",
"span": {
"type": "Span",
"start": 229,
"end": 244
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "A face 😂 with tears of joy.",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 248,
"end": 278
}
},
"span": {
"type": "Span",
"start": 247,
"end": 279
}
}
],
"span": {
"type": "Span",
"start": 247,
"end": 279
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 229,
"end": 279
}
},
{
"content": "ERROR Invalid identifier",
"type": "Comment",
"span": {
"type": "Span",
"start": 281,
"end": 307
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
"="
],
"message": "Expected token: \"=\"",
"span": {
"type": "Span",
"start": 312,
"end": 312
}
}
],
"content": "err-😂 = Value\n\n",
"span": {
"type": "Span",
"start": 308,
"end": 324
}
},
{
"content": "ERROR Invalid expression",
"type": "Comment",
"span": {
"type": "Span",
"start": 324,
"end": 350
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0028",
"arguments": [],
"message": "Expected an inline expression",
"span": {
"type": "Span",
"start": 378,
"end": 378
}
}
],
"content": "err-invalid-expression = { 😂 }\n\n",
"span": {
"type": "Span",
"start": 351,
"end": 384
}
},
{
"content": "ERROR Invalid variant key",
"type": "Comment",
"span": {
"type": "Span",
"start": 384,
"end": 411
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0004",
"arguments": [
"a-zA-Z"
],
"message": "Expected a character from range: \"a-zA-Z\"",
"span": {
"type": "Span",
"start": 454,
"end": 454
}
}
],
"content": "err-invalid-variant-key = { $sel ->\n *[😂] Value\n}\n",
"span": {
"type": "Span",
"start": 412,
"end": 466
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 466
}
}

View File

@@ -0,0 +1,120 @@
## Function names
valid-func-name-01 = {FUN1()}
valid-func-name-02 = {FUN_FUN()}
valid-func-name-03 = {FUN-FUN()}
# JUNK 0 is not a valid Identifier start
invalid-func-name-01 = {0FUN()}
# JUNK Function names may not be lowercase
invalid-func-name-02 = {fun()}
# JUNK Function names may not contain lowercase character
invalid-func-name-03 = {Fun()}
# JUNK ? is not a valid Identifier character
invalid-func-name-04 = {FUN?()}
## Arguments
positional-args = {FUN(1, "a", msg)}
named-args = {FUN(x: 1, y: "Y")}
dense-named-args = {FUN(x:1, y:"Y")}
mixed-args = {FUN(1, "a", msg, x: 1, y: "Y")}
# ERROR Positional arg must not follow keyword args
shuffled-args = {FUN(1, x: 1, "a", y: "Y", msg)}
# ERROR Named arguments must be unique
duplicate-named-args = {FUN(x: 1, x: "X")}
## Whitespace around arguments
sparse-inline-call = {FUN ( "a" , msg, x: 1 )}
empty-inline-call = {FUN( )}
multiline-call = {FUN(
"a",
msg,
x: 1
)}
sparse-multiline-call = {FUN
(
"a" ,
msg
, x: 1
)}
empty-multiline-call = {FUN(
)}
unindented-arg-number = {FUN(
1)}
unindented-arg-string = {FUN(
"a")}
unindented-arg-msg-ref = {FUN(
msg)}
unindented-arg-term-ref = {FUN(
-msg)}
unindented-arg-var-ref = {FUN(
$var)}
unindented-arg-call = {FUN(
OTHER())}
unindented-named-arg = {FUN(
x:1)}
unindented-closing-paren = {FUN(
x
)}
## Optional trailing comma
one-argument = {FUN(1,)}
many-arguments = {FUN(1, 2, 3,)}
inline-sparse-args = {FUN( 1, 2, 3, )}
mulitline-args = {FUN(
1,
2,
)}
mulitline-sparse-args = {FUN(
1
,
2
,
)}
## Syntax errors for trailing comma
one-argument = {FUN(1,,)}
missing-arg = {FUN(,)}
missing-sparse-arg = {FUN( , )}
## Whitespace in named arguments
sparse-named-arg = {FUN(
x : 1,
y : 2,
z
:
3
)}
unindented-colon = {FUN(
x
:1)}
unindented-value = {FUN(
x:
1)}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
## Callees in placeables.
function-callee-placeable = {FUNCTION()}
term-callee-placeable = {-term()}
# ERROR Messages cannot be parameterized.
message-callee-placeable = {message()}
# ERROR Equivalent to a MessageReference callee.
mixed-case-callee-placeable = {Function()}
# ERROR Message attributes cannot be parameterized.
message-attr-callee-placeable = {message.attr()}
# ERROR Term attributes may not be used in Placeables.
term-attr-callee-placeable = {-term.attr()}
# ERROR Variables cannot be parameterized.
variable-callee-placeable = {$variable()}
## Callees in selectors.
function-callee-selector = {FUNCTION() ->
*[key] Value
}
term-attr-callee-selector = {-term.attr() ->
*[key] Value
}
# ERROR Messages cannot be parameterized.
message-callee-selector = {message() ->
*[key] Value
}
# ERROR Equivalent to a MessageReference callee.
mixed-case-callee-selector = {Function() ->
*[key] Value
}
# ERROR Message attributes cannot be parameterized.
message-attr-callee-selector = {message.attr() ->
*[key] Value
}
# ERROR Term values may not be used as selectors.
term-callee-selector = {-term() ->
*[key] Value
}
# ERROR Variables cannot be parameterized.
variable-callee-selector = {$variable() ->
*[key] Value
}

View File

@@ -0,0 +1,706 @@
{
"type": "Resource",
"body": [
{
"content": "Callees in placeables.",
"type": "GroupComment",
"span": {
"type": "Span",
"start": 0,
"end": 25
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "function-callee-placeable",
"span": {
"type": "Span",
"start": 27,
"end": 52
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"type": "FunctionReference",
"id": {
"type": "Identifier",
"name": "FUNCTION",
"span": {
"type": "Span",
"start": 56,
"end": 64
}
},
"arguments": {
"type": "CallArguments",
"positional": [],
"named": [],
"span": {
"type": "Span",
"start": 64,
"end": 66
}
},
"span": {
"type": "Span",
"start": 56,
"end": 66
}
},
"span": {
"type": "Span",
"start": 55,
"end": 67
}
}
],
"span": {
"type": "Span",
"start": 55,
"end": 67
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 27,
"end": 67
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "term-callee-placeable",
"span": {
"type": "Span",
"start": 68,
"end": 89
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"type": "TermReference",
"id": {
"type": "Identifier",
"name": "term",
"span": {
"type": "Span",
"start": 94,
"end": 98
}
},
"attribute": null,
"arguments": {
"type": "CallArguments",
"positional": [],
"named": [],
"span": {
"type": "Span",
"start": 98,
"end": 100
}
},
"span": {
"type": "Span",
"start": 93,
"end": 100
}
},
"span": {
"type": "Span",
"start": 92,
"end": 101
}
}
],
"span": {
"type": "Span",
"start": 92,
"end": 101
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 68,
"end": 101
}
},
{
"content": "ERROR Messages cannot be parameterized.",
"type": "Comment",
"span": {
"type": "Span",
"start": 103,
"end": 144
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0008",
"arguments": [],
"message": "The callee has to be an upper-case identifier or a term",
"span": {
"type": "Span",
"start": 180,
"end": 180
}
}
],
"content": "message-callee-placeable = {message()}\n",
"span": {
"type": "Span",
"start": 145,
"end": 184
}
},
{
"content": "ERROR Equivalent to a MessageReference callee.",
"type": "Comment",
"span": {
"type": "Span",
"start": 184,
"end": 232
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0008",
"arguments": [],
"message": "The callee has to be an upper-case identifier or a term",
"span": {
"type": "Span",
"start": 272,
"end": 272
}
}
],
"content": "mixed-case-callee-placeable = {Function()}\n",
"span": {
"type": "Span",
"start": 233,
"end": 276
}
},
{
"content": "ERROR Message attributes cannot be parameterized.",
"type": "Comment",
"span": {
"type": "Span",
"start": 276,
"end": 327
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
"}"
],
"message": "Expected token: \"}\"",
"span": {
"type": "Span",
"start": 373,
"end": 373
}
}
],
"content": "message-attr-callee-placeable = {message.attr()}\n",
"span": {
"type": "Span",
"start": 328,
"end": 377
}
},
{
"content": "ERROR Term attributes may not be used in Placeables.",
"type": "Comment",
"span": {
"type": "Span",
"start": 377,
"end": 431
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0019",
"arguments": [],
"message": "Attributes of terms cannot be used as placeables",
"span": {
"type": "Span",
"start": 474,
"end": 474
}
}
],
"content": "term-attr-callee-placeable = {-term.attr()}\n",
"span": {
"type": "Span",
"start": 432,
"end": 476
}
},
{
"content": "ERROR Variables cannot be parameterized.",
"type": "Comment",
"span": {
"type": "Span",
"start": 476,
"end": 518
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
"}"
],
"message": "Expected token: \"}\"",
"span": {
"type": "Span",
"start": 557,
"end": 557
}
}
],
"content": "variable-callee-placeable = {$variable()}\n\n\n",
"span": {
"type": "Span",
"start": 519,
"end": 563
}
},
{
"content": "Callees in selectors.",
"type": "GroupComment",
"span": {
"type": "Span",
"start": 563,
"end": 587
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "function-callee-selector",
"span": {
"type": "Span",
"start": 589,
"end": 613
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"type": "SelectExpression",
"selector": {
"type": "FunctionReference",
"id": {
"type": "Identifier",
"name": "FUNCTION",
"span": {
"type": "Span",
"start": 617,
"end": 625
}
},
"arguments": {
"type": "CallArguments",
"positional": [],
"named": [],
"span": {
"type": "Span",
"start": 625,
"end": 627
}
},
"span": {
"type": "Span",
"start": 617,
"end": 627
}
},
"variants": [
{
"type": "Variant",
"key": {
"type": "Identifier",
"name": "key",
"span": {
"type": "Span",
"start": 636,
"end": 639
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "Value",
"span": {
"type": "Span",
"start": 641,
"end": 646
}
}
],
"span": {
"type": "Span",
"start": 641,
"end": 646
}
},
"default": true,
"span": {
"type": "Span",
"start": 634,
"end": 646
}
}
],
"span": {
"type": "Span",
"start": 617,
"end": 647
}
},
"span": {
"type": "Span",
"start": 616,
"end": 648
}
}
],
"span": {
"type": "Span",
"start": 616,
"end": 648
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 589,
"end": 648
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "term-attr-callee-selector",
"span": {
"type": "Span",
"start": 649,
"end": 674
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"type": "SelectExpression",
"selector": {
"type": "TermReference",
"id": {
"type": "Identifier",
"name": "term",
"span": {
"type": "Span",
"start": 679,
"end": 683
}
},
"attribute": {
"type": "Identifier",
"name": "attr",
"span": {
"type": "Span",
"start": 684,
"end": 688
}
},
"arguments": {
"type": "CallArguments",
"positional": [],
"named": [],
"span": {
"type": "Span",
"start": 688,
"end": 690
}
},
"span": {
"type": "Span",
"start": 678,
"end": 690
}
},
"variants": [
{
"type": "Variant",
"key": {
"type": "Identifier",
"name": "key",
"span": {
"type": "Span",
"start": 699,
"end": 702
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "Value",
"span": {
"type": "Span",
"start": 704,
"end": 709
}
}
],
"span": {
"type": "Span",
"start": 704,
"end": 709
}
},
"default": true,
"span": {
"type": "Span",
"start": 697,
"end": 709
}
}
],
"span": {
"type": "Span",
"start": 678,
"end": 710
}
},
"span": {
"type": "Span",
"start": 677,
"end": 711
}
}
],
"span": {
"type": "Span",
"start": 677,
"end": 711
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 649,
"end": 711
}
},
{
"content": "ERROR Messages cannot be parameterized.",
"type": "Comment",
"span": {
"type": "Span",
"start": 713,
"end": 754
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0008",
"arguments": [],
"message": "The callee has to be an upper-case identifier or a term",
"span": {
"type": "Span",
"start": 789,
"end": 789
}
}
],
"content": "message-callee-selector = {message() ->\n *[key] Value\n}\n",
"span": {
"type": "Span",
"start": 755,
"end": 813
}
},
{
"content": "ERROR Equivalent to a MessageReference callee.",
"type": "Comment",
"span": {
"type": "Span",
"start": 813,
"end": 861
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0008",
"arguments": [],
"message": "The callee has to be an upper-case identifier or a term",
"span": {
"type": "Span",
"start": 900,
"end": 900
}
}
],
"content": "mixed-case-callee-selector = {Function() ->\n *[key] Value\n}\n",
"span": {
"type": "Span",
"start": 862,
"end": 924
}
},
{
"content": "ERROR Message attributes cannot be parameterized.",
"type": "Comment",
"span": {
"type": "Span",
"start": 924,
"end": 975
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
"}"
],
"message": "Expected token: \"}\"",
"span": {
"type": "Span",
"start": 1020,
"end": 1020
}
}
],
"content": "message-attr-callee-selector = {message.attr() ->\n *[key] Value\n}\n",
"span": {
"type": "Span",
"start": 976,
"end": 1044
}
},
{
"content": "ERROR Term values may not be used as selectors.",
"type": "Comment",
"span": {
"type": "Span",
"start": 1044,
"end": 1093
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0017",
"arguments": [],
"message": "Terms cannot be used as selectors",
"span": {
"type": "Span",
"start": 1126,
"end": 1126
}
}
],
"content": "term-callee-selector = {-term() ->\n *[key] Value\n}\n",
"span": {
"type": "Span",
"start": 1094,
"end": 1147
}
},
{
"content": "ERROR Variables cannot be parameterized.",
"type": "Comment",
"span": {
"type": "Span",
"start": 1147,
"end": 1189
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
"}"
],
"message": "Expected token: \"}\"",
"span": {
"type": "Span",
"start": 1227,
"end": 1227
}
}
],
"content": "variable-callee-selector = {$variable() ->\n *[key] Value\n}\n",
"span": {
"type": "Span",
"start": 1190,
"end": 1251
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 1251
}
}

View File

@@ -0,0 +1,20 @@
# Standalone Comment
# Message Comment
foo = Foo
# Term Comment
# with a blank last line.
#
-term = Term
# Another standalone
#
# with indent
## Group Comment
### Resource Comment
# Errors
#error
##error
###error

View File

@@ -0,0 +1,219 @@
{
"type": "Resource",
"body": [
{
"content": "Standalone Comment",
"type": "Comment",
"span": {
"type": "Span",
"start": 0,
"end": 20
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "foo",
"span": {
"type": "Span",
"start": 40,
"end": 43
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "Foo",
"span": {
"type": "Span",
"start": 46,
"end": 49
}
}
],
"span": {
"type": "Span",
"start": 46,
"end": 49
}
},
"attributes": [],
"comment": {
"content": "Message Comment",
"type": "Comment",
"span": {
"type": "Span",
"start": 22,
"end": 39
}
},
"span": {
"type": "Span",
"start": 22,
"end": 49
}
},
{
"type": "Term",
"id": {
"type": "Identifier",
"name": "term",
"span": {
"type": "Span",
"start": 95,
"end": 99
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "Term",
"span": {
"type": "Span",
"start": 102,
"end": 106
}
}
],
"span": {
"type": "Span",
"start": 102,
"end": 106
}
},
"attributes": [],
"comment": {
"content": "Term Comment\nwith a blank last line.\n",
"type": "Comment",
"span": {
"type": "Span",
"start": 51,
"end": 93
}
},
"span": {
"type": "Span",
"start": 51,
"end": 106
}
},
{
"content": "Another standalone\n\n with indent",
"type": "Comment",
"span": {
"type": "Span",
"start": 108,
"end": 150
}
},
{
"content": "Group Comment",
"type": "GroupComment",
"span": {
"type": "Span",
"start": 151,
"end": 167
}
},
{
"content": "Resource Comment",
"type": "ResourceComment",
"span": {
"type": "Span",
"start": 168,
"end": 188
}
},
{
"content": "Errors",
"type": "Comment",
"span": {
"type": "Span",
"start": 190,
"end": 198
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
" "
],
"message": "Expected token: \" \"",
"span": {
"type": "Span",
"start": 200,
"end": 200
}
}
],
"content": "#error\n",
"span": {
"type": "Span",
"start": 199,
"end": 206
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
" "
],
"message": "Expected token: \" \"",
"span": {
"type": "Span",
"start": 208,
"end": 208
}
}
],
"content": "##error\n",
"span": {
"type": "Span",
"start": 206,
"end": 214
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
" "
],
"message": "Expected token: \" \"",
"span": {
"type": "Span",
"start": 217,
"end": 217
}
}
],
"content": "###error\n",
"span": {
"type": "Span",
"start": 214,
"end": 223
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 223
}
}

View File

@@ -0,0 +1 @@
### This entire file uses CR as EOL.

View File

@@ -0,0 +1,19 @@
{
"type": "Resource",
"body": [
{
"type": "ResourceComment",
"content": "This entire file uses CR as EOL.\r\rerr01 = Value 01\rerr02 = Value 02\r\rerr03 =\r\r Value 03\r Continued\r\r .title = Title\r\rerr04 = { \"str\r\rerr05 = { $sel -> }\r",
"span": {
"type": "Span",
"start": 0,
"end": 166
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 166
}
}

View File

@@ -0,0 +1,14 @@
key01 = Value 01
key02 =
Value 02
Continued
.title = Title
# ERROR Unclosed StringLiteral
err03 = { "str
# ERROR Missing newline after ->.
err04 = { $sel -> }

View File

@@ -0,0 +1,76 @@
{
"type": "Resource",
"body": [
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "key01"
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "Value 01"
}
]
},
"attributes": [],
"comment": null
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "key02"
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "Value 02\nContinued"
}
]
},
"attributes": [
{
"type": "Attribute",
"id": {
"type": "Identifier",
"name": "title"
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "Title"
}
]
}
}
],
"comment": null
},
{
"type": "Comment",
"content": "ERROR Unclosed StringLiteral"
},
{
"type": "Junk",
"annotations": [],
"content": "err03 = { \"str\r\n\r\n"
},
{
"type": "Comment",
"content": "ERROR Missing newline after ->."
},
{
"type": "Junk",
"annotations": [],
"content": "err04 = { $sel -> }\r\n"
}
]
}

View File

@@ -0,0 +1,3 @@
### NOTE: Disable final newline insertion when editing this file.
# No EOL

View File

@@ -0,0 +1,28 @@
{
"type": "Resource",
"body": [
{
"content": "NOTE: Disable final newline insertion when editing this file.",
"type": "ResourceComment",
"span": {
"type": "Span",
"start": 0,
"end": 65
}
},
{
"content": "No EOL",
"type": "Comment",
"span": {
"type": "Span",
"start": 67,
"end": 75
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 75
}
}

View File

@@ -0,0 +1,9 @@
{
"type": "Resource",
"body": [],
"span": {
"type": "Span",
"start": 0,
"end": 0
}
}

View File

@@ -0,0 +1,3 @@
### NOTE: Disable final newline insertion when editing this file.
message-id

View File

@@ -0,0 +1,43 @@
{
"type": "Resource",
"body": [
{
"content": "NOTE: Disable final newline insertion when editing this file.",
"type": "ResourceComment",
"span": {
"type": "Span",
"start": 0,
"end": 65
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
"="
],
"message": "Expected token: \"=\"",
"span": {
"type": "Span",
"start": 77,
"end": 77
}
}
],
"content": "message-id",
"span": {
"type": "Span",
"start": 67,
"end": 77
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 77
}
}

View File

@@ -0,0 +1,3 @@
### NOTE: Disable final newline insertion when editing this file.
message-id =

View File

@@ -0,0 +1,43 @@
{
"type": "Resource",
"body": [
{
"content": "NOTE: Disable final newline insertion when editing this file.",
"type": "ResourceComment",
"span": {
"type": "Span",
"start": 0,
"end": 65
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0005",
"arguments": [
"message-id"
],
"message": "Expected message \"message-id\" to have a value or attributes",
"span": {
"type": "Span",
"start": 79,
"end": 79
}
}
],
"content": "message-id =",
"span": {
"type": "Span",
"start": 67,
"end": 79
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 79
}
}

View File

@@ -0,0 +1,3 @@
### NOTE: Disable final newline insertion when editing this file.
000

View File

@@ -0,0 +1,41 @@
{
"type": "Resource",
"body": [
{
"content": "NOTE: Disable final newline insertion when editing this file.",
"type": "ResourceComment",
"span": {
"type": "Span",
"start": 0,
"end": 65
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0002",
"arguments": [],
"message": "Expected an entry start",
"span": {
"type": "Span",
"start": 67,
"end": 67
}
}
],
"content": "000",
"span": {
"type": "Span",
"start": 67,
"end": 70
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 70
}
}

View File

@@ -0,0 +1,3 @@
### NOTE: Disable final newline insertion when editing this file.
no-eol = No EOL

View File

@@ -0,0 +1,57 @@
{
"type": "Resource",
"body": [
{
"content": "NOTE: Disable final newline insertion when editing this file.",
"type": "ResourceComment",
"span": {
"type": "Span",
"start": 0,
"end": 65
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "no-eol",
"span": {
"type": "Span",
"start": 67,
"end": 73
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "No EOL",
"span": {
"type": "Span",
"start": 76,
"end": 82
}
}
],
"span": {
"type": "Span",
"start": 76,
"end": 82
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 67,
"end": 82
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 82
}
}

View File

@@ -0,0 +1,37 @@
## Literal text
text-backslash-one = Value with \ a backslash
text-backslash-two = Value with \\ two backslashes
text-backslash-brace = Value with \{placeable}
text-backslash-u = \u0041
text-backslash-backslash-u = \\u0041
## String literals
quote-in-string = {"\""}
backslash-in-string = {"\\"}
# ERROR Mismatched quote
mismatched-quote = {"\\""}
# ERROR Unknown escape
unknown-escape = {"\x"}
# ERROR Multiline literal
invalid-multiline-literal = {"
"}
## Unicode escapes
string-unicode-4digits = {"\u0041"}
escape-unicode-4digits = {"\\u0041"}
string-unicode-6digits = {"\U01F602"}
escape-unicode-6digits = {"\\U01F602"}
# OK The trailing "00" is part of the literal value.
string-too-many-4digits = {"\u004100"}
# OK The trailing "00" is part of the literal value.
string-too-many-6digits = {"\U01F60200"}
# ERROR Too few hex digits after \u.
string-too-few-4digits = {"\u41"}
# ERROR Too few hex digits after \U.
string-too-few-6digits = {"\U1F602"}
## Literal braces
brace-open = An opening {"{"} brace.
brace-close = A closing {"}"} brace.

View File

@@ -0,0 +1,937 @@
{
"type": "Resource",
"body": [
{
"content": "Literal text",
"type": "GroupComment",
"span": {
"type": "Span",
"start": 0,
"end": 15
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "text-backslash-one",
"span": {
"type": "Span",
"start": 16,
"end": 34
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "Value with \\ a backslash",
"span": {
"type": "Span",
"start": 37,
"end": 61
}
}
],
"span": {
"type": "Span",
"start": 37,
"end": 61
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 16,
"end": 61
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "text-backslash-two",
"span": {
"type": "Span",
"start": 62,
"end": 80
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "Value with \\\\ two backslashes",
"span": {
"type": "Span",
"start": 83,
"end": 112
}
}
],
"span": {
"type": "Span",
"start": 83,
"end": 112
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 62,
"end": 112
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "text-backslash-brace",
"span": {
"type": "Span",
"start": 113,
"end": 133
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "Value with \\",
"span": {
"type": "Span",
"start": 136,
"end": 148
}
},
{
"type": "Placeable",
"expression": {
"type": "MessageReference",
"id": {
"type": "Identifier",
"name": "placeable",
"span": {
"type": "Span",
"start": 149,
"end": 158
}
},
"attribute": null,
"span": {
"type": "Span",
"start": 149,
"end": 158
}
},
"span": {
"type": "Span",
"start": 148,
"end": 159
}
}
],
"span": {
"type": "Span",
"start": 136,
"end": 159
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 113,
"end": 159
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "text-backslash-u",
"span": {
"type": "Span",
"start": 160,
"end": 176
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "\\u0041",
"span": {
"type": "Span",
"start": 179,
"end": 185
}
}
],
"span": {
"type": "Span",
"start": 179,
"end": 185
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 160,
"end": 185
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "text-backslash-backslash-u",
"span": {
"type": "Span",
"start": 186,
"end": 212
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "\\\\u0041",
"span": {
"type": "Span",
"start": 215,
"end": 222
}
}
],
"span": {
"type": "Span",
"start": 215,
"end": 222
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 186,
"end": 222
}
},
{
"content": "String literals",
"type": "GroupComment",
"span": {
"type": "Span",
"start": 224,
"end": 242
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "quote-in-string",
"span": {
"type": "Span",
"start": 243,
"end": 258
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "\\\"",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 262,
"end": 266
}
},
"span": {
"type": "Span",
"start": 261,
"end": 267
}
}
],
"span": {
"type": "Span",
"start": 261,
"end": 267
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 243,
"end": 267
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "backslash-in-string",
"span": {
"type": "Span",
"start": 268,
"end": 287
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "\\\\",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 291,
"end": 295
}
},
"span": {
"type": "Span",
"start": 290,
"end": 296
}
}
],
"span": {
"type": "Span",
"start": 290,
"end": 296
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 268,
"end": 296
}
},
{
"content": "ERROR Mismatched quote",
"type": "Comment",
"span": {
"type": "Span",
"start": 297,
"end": 321
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
"}"
],
"message": "Expected token: \"}\"",
"span": {
"type": "Span",
"start": 346,
"end": 346
}
}
],
"content": "mismatched-quote = {\"\\\\\"\"}\n",
"span": {
"type": "Span",
"start": 322,
"end": 349
}
},
{
"content": "ERROR Unknown escape",
"type": "Comment",
"span": {
"type": "Span",
"start": 349,
"end": 371
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0025",
"arguments": [
"x"
],
"message": "Unknown escape sequence: \\x.",
"span": {
"type": "Span",
"start": 392,
"end": 392
}
}
],
"content": "unknown-escape = {\"\\x\"}\n",
"span": {
"type": "Span",
"start": 372,
"end": 396
}
},
{
"content": "ERROR Multiline literal",
"type": "Comment",
"span": {
"type": "Span",
"start": 396,
"end": 421
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0020",
"arguments": [],
"message": "Unterminated string expression",
"span": {
"type": "Span",
"start": 452,
"end": 452
}
}
],
"content": "invalid-multiline-literal = {\"\n \"}\n\n",
"span": {
"type": "Span",
"start": 422,
"end": 458
}
},
{
"content": "Unicode escapes",
"type": "GroupComment",
"span": {
"type": "Span",
"start": 458,
"end": 476
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "string-unicode-4digits",
"span": {
"type": "Span",
"start": 477,
"end": 499
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "\\u0041",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 503,
"end": 511
}
},
"span": {
"type": "Span",
"start": 502,
"end": 512
}
}
],
"span": {
"type": "Span",
"start": 502,
"end": 512
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 477,
"end": 512
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "escape-unicode-4digits",
"span": {
"type": "Span",
"start": 513,
"end": 535
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "\\\\u0041",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 539,
"end": 548
}
},
"span": {
"type": "Span",
"start": 538,
"end": 549
}
}
],
"span": {
"type": "Span",
"start": 538,
"end": 549
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 513,
"end": 549
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "string-unicode-6digits",
"span": {
"type": "Span",
"start": 550,
"end": 572
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "\\U01F602",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 576,
"end": 586
}
},
"span": {
"type": "Span",
"start": 575,
"end": 587
}
}
],
"span": {
"type": "Span",
"start": 575,
"end": 587
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 550,
"end": 587
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "escape-unicode-6digits",
"span": {
"type": "Span",
"start": 588,
"end": 610
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "\\\\U01F602",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 614,
"end": 625
}
},
"span": {
"type": "Span",
"start": 613,
"end": 626
}
}
],
"span": {
"type": "Span",
"start": 613,
"end": 626
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 588,
"end": 626
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "string-too-many-4digits",
"span": {
"type": "Span",
"start": 681,
"end": 704
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "\\u004100",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 708,
"end": 718
}
},
"span": {
"type": "Span",
"start": 707,
"end": 719
}
}
],
"span": {
"type": "Span",
"start": 707,
"end": 719
}
},
"attributes": [],
"comment": {
"content": "OK The trailing \"00\" is part of the literal value.",
"type": "Comment",
"span": {
"type": "Span",
"start": 628,
"end": 680
}
},
"span": {
"type": "Span",
"start": 628,
"end": 719
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "string-too-many-6digits",
"span": {
"type": "Span",
"start": 773,
"end": 796
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "\\U01F60200",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 800,
"end": 812
}
},
"span": {
"type": "Span",
"start": 799,
"end": 813
}
}
],
"span": {
"type": "Span",
"start": 799,
"end": 813
}
},
"attributes": [],
"comment": {
"content": "OK The trailing \"00\" is part of the literal value.",
"type": "Comment",
"span": {
"type": "Span",
"start": 720,
"end": 772
}
},
"span": {
"type": "Span",
"start": 720,
"end": 813
}
},
{
"content": "ERROR Too few hex digits after \\u.",
"type": "Comment",
"span": {
"type": "Span",
"start": 815,
"end": 851
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0026",
"arguments": [
"\\u41\""
],
"message": "Invalid Unicode escape sequence: \\u41\".",
"span": {
"type": "Span",
"start": 883,
"end": 883
}
}
],
"content": "string-too-few-4digits = {\"\\u41\"}\n",
"span": {
"type": "Span",
"start": 852,
"end": 886
}
},
{
"content": "ERROR Too few hex digits after \\U.",
"type": "Comment",
"span": {
"type": "Span",
"start": 886,
"end": 922
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0026",
"arguments": [
"\\U1F602\""
],
"message": "Invalid Unicode escape sequence: \\U1F602\".",
"span": {
"type": "Span",
"start": 957,
"end": 957
}
}
],
"content": "string-too-few-6digits = {\"\\U1F602\"}\n\n",
"span": {
"type": "Span",
"start": 923,
"end": 961
}
},
{
"content": "Literal braces",
"type": "GroupComment",
"span": {
"type": "Span",
"start": 961,
"end": 978
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "brace-open",
"span": {
"type": "Span",
"start": 979,
"end": 989
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "An opening ",
"span": {
"type": "Span",
"start": 992,
"end": 1003
}
},
{
"type": "Placeable",
"expression": {
"value": "{",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 1004,
"end": 1007
}
},
"span": {
"type": "Span",
"start": 1003,
"end": 1008
}
},
{
"type": "TextElement",
"value": " brace.",
"span": {
"type": "Span",
"start": 1008,
"end": 1015
}
}
],
"span": {
"type": "Span",
"start": 992,
"end": 1015
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 979,
"end": 1015
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "brace-close",
"span": {
"type": "Span",
"start": 1016,
"end": 1027
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "TextElement",
"value": "A closing ",
"span": {
"type": "Span",
"start": 1030,
"end": 1040
}
},
{
"type": "Placeable",
"expression": {
"value": "}",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 1041,
"end": 1044
}
},
"span": {
"type": "Span",
"start": 1040,
"end": 1045
}
},
{
"type": "TextElement",
"value": " brace.",
"span": {
"type": "Span",
"start": 1045,
"end": 1052
}
}
],
"span": {
"type": "Span",
"start": 1030,
"end": 1052
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 1016,
"end": 1052
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 1053
}
}

View File

@@ -0,0 +1,21 @@
## Two adjacent Junks.
err01 = {1x}
err02 = {2x}
# A single Junk.
err03 = {1x
2
# A single Junk.
ą=Invalid identifier
ć=Another one
# The COMMENT ends this junk.
err04 = {
# COMMENT
# The COMMENT ends this junk.
# The closing brace is a separate Junk.
err04 = {
# COMMENT
}

View File

@@ -0,0 +1,233 @@
{
"type": "Resource",
"body": [
{
"content": "Two adjacent Junks.",
"type": "GroupComment",
"span": {
"type": "Span",
"start": 0,
"end": 22
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
"}"
],
"message": "Expected token: \"}\"",
"span": {
"type": "Span",
"start": 33,
"end": 33
}
}
],
"content": "err01 = {1x}\n",
"span": {
"type": "Span",
"start": 23,
"end": 36
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
"}"
],
"message": "Expected token: \"}\"",
"span": {
"type": "Span",
"start": 46,
"end": 46
}
}
],
"content": "err02 = {2x}\n\n",
"span": {
"type": "Span",
"start": 36,
"end": 50
}
},
{
"content": "A single Junk.",
"type": "Comment",
"span": {
"type": "Span",
"start": 50,
"end": 66
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0003",
"arguments": [
"}"
],
"message": "Expected token: \"}\"",
"span": {
"type": "Span",
"start": 77,
"end": 77
}
}
],
"content": "err03 = {1x\n2\n\n",
"span": {
"type": "Span",
"start": 67,
"end": 82
}
},
{
"content": "A single Junk.",
"type": "Comment",
"span": {
"type": "Span",
"start": 82,
"end": 98
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0002",
"arguments": [],
"message": "Expected an entry start",
"span": {
"type": "Span",
"start": 99,
"end": 99
}
}
],
"content": "ą=Invalid identifier\nć=Another one\n\n",
"span": {
"type": "Span",
"start": 99,
"end": 135
}
},
{
"content": "The COMMENT ends this junk.",
"type": "Comment",
"span": {
"type": "Span",
"start": 135,
"end": 164
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0028",
"arguments": [],
"message": "Expected an inline expression",
"span": {
"type": "Span",
"start": 175,
"end": 175
}
}
],
"content": "err04 = {\n",
"span": {
"type": "Span",
"start": 165,
"end": 175
}
},
{
"content": "COMMENT",
"type": "Comment",
"span": {
"type": "Span",
"start": 175,
"end": 184
}
},
{
"content": "The COMMENT ends this junk.\nThe closing brace is a separate Junk.",
"type": "Comment",
"span": {
"type": "Span",
"start": 186,
"end": 255
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0028",
"arguments": [],
"message": "Expected an inline expression",
"span": {
"type": "Span",
"start": 266,
"end": 266
}
}
],
"content": "err04 = {\n",
"span": {
"type": "Span",
"start": 256,
"end": 266
}
},
{
"content": "COMMENT",
"type": "Comment",
"span": {
"type": "Span",
"start": 266,
"end": 275
}
},
{
"type": "Junk",
"annotations": [
{
"type": "Annotation",
"code": "E0002",
"arguments": [],
"message": "Expected an entry start",
"span": {
"type": "Span",
"start": 276,
"end": 276
}
}
],
"content": "}\n",
"span": {
"type": "Span",
"start": 276,
"end": 278
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 278
}
}

View File

@@ -0,0 +1,76 @@
key01 = .Value
key02 = …Value
key03 = {"."}Value
key04 =
{"."}Value
key05 = Value
{"."}Continued
key06 = .Value
{"."}Continued
# MESSAGE (value = "Value", attributes = [])
# JUNK (attr .Continued" must have a value)
key07 = Value
.Continued
# JUNK (attr .Value must have a value)
key08 =
.Value
# JUNK (attr .Value must have a value)
key09 =
.Value
Continued
key10 =
.Value = which is an attribute
Continued
key11 =
{"."}Value = which looks like an attribute
Continued
key12 =
.accesskey =
A
key13 =
.attribute = .Value
key14 =
.attribute =
{"."}Value
key15 =
{ 1 ->
[one] .Value
*[other]
{"."}Value
}
# JUNK (variant must have a value)
key16 =
{ 1 ->
*[one]
.Value
}
# JUNK (unclosed placeable)
key17 =
{ 1 ->
*[one] Value
.Continued
}
# JUNK (attr .Value must have a value)
key18 =
.Value
key19 =
.attribute = Value
Continued
key20 =
{"."}Value

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
string-expression = {"abc"}
number-expression = {123}
number-expression = {-3.14}

View File

@@ -0,0 +1,148 @@
{
"type": "Resource",
"body": [
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "string-expression",
"span": {
"type": "Span",
"start": 0,
"end": 17
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "abc",
"type": "StringLiteral",
"span": {
"type": "Span",
"start": 21,
"end": 26
}
},
"span": {
"type": "Span",
"start": 20,
"end": 27
}
}
],
"span": {
"type": "Span",
"start": 20,
"end": 27
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 0,
"end": 27
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "number-expression",
"span": {
"type": "Span",
"start": 28,
"end": 45
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "123",
"type": "NumberLiteral",
"span": {
"type": "Span",
"start": 49,
"end": 52
}
},
"span": {
"type": "Span",
"start": 48,
"end": 53
}
}
],
"span": {
"type": "Span",
"start": 48,
"end": 53
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 28,
"end": 53
}
},
{
"type": "Message",
"id": {
"type": "Identifier",
"name": "number-expression",
"span": {
"type": "Span",
"start": 54,
"end": 71
}
},
"value": {
"type": "Pattern",
"elements": [
{
"type": "Placeable",
"expression": {
"value": "-3.14",
"type": "NumberLiteral",
"span": {
"type": "Span",
"start": 75,
"end": 80
}
},
"span": {
"type": "Span",
"start": 74,
"end": 81
}
}
],
"span": {
"type": "Span",
"start": 74,
"end": 81
}
},
"attributes": [],
"comment": null,
"span": {
"type": "Span",
"start": 54,
"end": 81
}
}
],
"span": {
"type": "Span",
"start": 0,
"end": 82
}
}

Some files were not shown because too many files have changed in this diff Show More