diff --git a/src/main/java/org/example/ebnfFormatter/render/OriginalGap.java b/src/main/java/org/example/ebnfFormatter/render/OriginalGap.java new file mode 100644 index 0000000..bf131a6 --- /dev/null +++ b/src/main/java/org/example/ebnfFormatter/render/OriginalGap.java @@ -0,0 +1,4 @@ +package org.example.ebnfFormatter.render; + +record OriginalGap(String text, boolean hasComment) { +} diff --git a/src/main/java/org/example/ebnfFormatter/render/OriginalGaps.java b/src/main/java/org/example/ebnfFormatter/render/OriginalGaps.java new file mode 100644 index 0000000..f305d27 --- /dev/null +++ b/src/main/java/org/example/ebnfFormatter/render/OriginalGaps.java @@ -0,0 +1,69 @@ +package org.example.ebnfFormatter.render; + +import com.github.javaparser.JavaToken; +import com.github.javaparser.TokenRange; +import com.github.javaparser.ast.Node; + +import java.util.Optional; + +final class OriginalGaps { + private OriginalGaps() { + } + + static Optional between(Object left, Object right) { + Optional leftRange = tokenRange(left); + Optional rightRange = tokenRange(right); + if (leftRange.isEmpty() || rightRange.isEmpty()) { + return Optional.empty(); + } + + JavaToken end = rightRange.get().getBegin(); + Optional current = leftRange.get().getEnd().getNextToken(); + StringBuilder text = new StringBuilder(); + boolean hasComment = false; + + while (current.isPresent() && current.get() != end) { + JavaToken token = current.get(); + if (!token.getCategory().isWhitespaceOrComment()) { + return Optional.empty(); + } + hasComment = hasComment || token.getCategory().isComment(); + text.append(token.getText()); + current = token.getNextToken(); + } + + return current.isPresent() ? Optional.of(new OriginalGap(text.toString(), hasComment)) : Optional.empty(); + } + + static Optional after(Object value) { + Optional range = tokenRange(value); + if (range.isEmpty()) { + return Optional.empty(); + } + + Optional current = range.get().getEnd().getNextToken(); + StringBuilder text = new StringBuilder(); + boolean hasComment = false; + + while (current.isPresent() && current.get().getCategory().isWhitespaceOrComment()) { + JavaToken token = current.get(); + hasComment = hasComment || token.getCategory().isComment(); + text.append(token.getText()); + current = token.getNextToken(); + } + + return Optional.of(new OriginalGap(text.toString(), hasComment)); + } + + private static Optional tokenRange(Object value) { + if (value instanceof Node node) { + return node.getTokenRange(); + } + + if (value instanceof Optional optional) { + return optional.flatMap(OriginalGaps::tokenRange); + } + + return Optional.empty(); + } +} diff --git a/src/main/java/org/example/ebnfFormatter/render/RenderContext.java b/src/main/java/org/example/ebnfFormatter/render/RenderContext.java index 16bfff9..f17d692 100644 --- a/src/main/java/org/example/ebnfFormatter/render/RenderContext.java +++ b/src/main/java/org/example/ebnfFormatter/render/RenderContext.java @@ -4,42 +4,84 @@ public final class RenderContext { private static final String INDENT_UNIT = " "; private final StringBuilder out = new StringBuilder(); + private final StringBuilder pendingWhitespace = new StringBuilder(); private int indentLevel = 0; private boolean lineStart = true; + private int column = 0; public void appendText(String text) { if (text == null || text.isEmpty()) { return; } - for (int i = 0; i < text.length(); i++) { - char ch = text.charAt(i); + flushPendingWhitespace(); + appendIndentedText(text); + } - if (lineStart) { - appendIndentIfNeeded(); - } + public void appendRawText(String text) { + if (text == null || text.isEmpty()) { + return; + } - out.append(ch); + flushPendingWhitespace(); + appendRawCommitted(text); + } - if (ch == '\n') { - lineStart = true; - } else { - lineStart = false; - } + void appendSourceWhitespace(String text) { + if (text == null || text.isEmpty()) { + return; } + + flushPendingWhitespace(); + appendWhitespace(text); } - public void space() { + void appendSourceTokenText(String text) { + if (text == null || text.isEmpty()) { + return; + } + + flushPendingWhitespace(); if (lineStart) { appendIndentIfNeeded(); } - out.append(' '); - lineStart = false; + appendRawCommitted(text); + } + + public void replacePendingWhitespaceWithRaw(String text) { + if (startsOnPendingLine(text)) { + flushPendingWhitespace(); + appendIndentedText(text); + return; + } + + pendingWhitespace.setLength(0); + appendRawCommitted(text); + } + + public void appendPendingWhitespace(String text) { + if (text == null || text.isEmpty()) { + return; + } + + pendingWhitespace.append(text); + } + + public void flushPendingWhitespace() { + if (pendingWhitespace.isEmpty()) { + return; + } + + appendWhitespace(pendingWhitespace.toString()); + pendingWhitespace.setLength(0); + } + + public void space() { + appendPendingWhitespace(" "); } public void newline() { - out.append('\n'); - lineStart = true; + appendPendingWhitespace("\n"); } public void indent() { @@ -53,14 +95,109 @@ public void dedent() { indentLevel--; } + public boolean isLineStart() { + if (pendingWhitespace.isEmpty()) { + return lineStart; + } + + char last = pendingWhitespace.charAt(pendingWhitespace.length() - 1); + return last == '\n' || last == '\r'; + } + + int sourceStartColumn() { + flushPendingWhitespace(); + if (lineStart) { + return currentIndentColumns(); + } + return column; + } + + int currentIndentColumns() { + return indentLevel * INDENT_UNIT.length(); + } + public String result() { + flushPendingWhitespace(); return out.toString(); } + private void appendIndentedText(String text) { + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + + if (lineStart) { + appendIndentIfNeeded(); + } + + out.append(ch); + + if (ch == '\n') { + lineStart = true; + column = 0; + } else { + lineStart = false; + column++; + } + } + } + + private void appendWhitespace(String text) { + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + if (ch == '\n' || ch == '\r') { + out.append(ch); + lineStart = true; + column = 0; + continue; + } + + if (lineStart) { + appendIndentIfNeeded(); + } + out.append(ch); + lineStart = false; + column++; + } + } + + private void appendRawCommitted(String text) { + if (text == null || text.isEmpty()) { + return; + } + + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + out.append(ch); + if (ch == '\n' || ch == '\r') { + lineStart = true; + column = 0; + } else { + lineStart = false; + column++; + } + } + } + + private boolean startsOnPendingLine(String text) { + return !pendingWhitespace.isEmpty() + && isLineStart() + && startsWithoutIndent(text); + } + + private boolean startsWithoutIndent(String text) { + return text != null + && !text.isEmpty() + && text.charAt(0) != ' ' + && text.charAt(0) != '\t' + && text.charAt(0) != '\n' + && text.charAt(0) != '\r'; + } + private void appendIndentIfNeeded() { for (int i = 0; i < indentLevel; i++) { out.append(INDENT_UNIT); + column += INDENT_UNIT.length(); } lineStart = false; } -} \ No newline at end of file +} diff --git a/src/main/java/org/example/ebnfFormatter/render/SourceCursor.java b/src/main/java/org/example/ebnfFormatter/render/SourceCursor.java new file mode 100644 index 0000000..92ed8e3 --- /dev/null +++ b/src/main/java/org/example/ebnfFormatter/render/SourceCursor.java @@ -0,0 +1,198 @@ +package org.example.ebnfFormatter.render; + +import com.github.javaparser.JavaToken; +import com.github.javaparser.TokenRange; +import com.github.javaparser.ast.Node; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +final class SourceCursor { + private final JavaToken begin; + private final JavaToken end; + private JavaToken lastConsumed; + + private SourceCursor(TokenRange range) { + this.begin = range.getBegin(); + this.end = range.getEnd(); + } + + static Optional create(Object value) { + return tokenRange(value).map(SourceCursor::new); + } + + void consumeLiteral(String text, RenderContext context) { + String sourceText = removeWhitespace(text); + if (sourceText.isEmpty()) { + context.appendPendingWhitespace(text); + return; + } + + Optional> tokens = nextSourceTokens(sourceText); + if (tokens.isEmpty()) { + context.flushPendingWhitespace(); + return; + } + + List matched = tokens.get(); + applyGapBefore(matched.getFirst(), context); + lastConsumed = matched.getLast(); + } + + void consumeValue(Object value, RenderContext context) { + Optional range = tokenRange(value); + if (range.isEmpty() || !contains(range.get().getBegin()) || !contains(range.get().getEnd())) { + context.flushPendingWhitespace(); + return; + } + + if (applyGapBefore(range.get().getBegin(), context)) { + lastConsumed = range.get().getEnd(); + return; + } + + context.flushPendingWhitespace(); + } + + private Optional> nextSourceTokens(String text) { + Optional current = nextToken(); + while (current.isPresent() && current.get().getCategory().isWhitespaceOrComment()) { + current = current.get().getNextToken(); + } + + StringBuilder matchedText = new StringBuilder(); + List matchedTokens = new ArrayList<>(); + + while (current.isPresent()) { + JavaToken token = current.get(); + if (!contains(token) || token.getCategory().isWhitespaceOrComment()) { + return Optional.empty(); + } + + matchedText.append(token.getText()); + if (!text.startsWith(matchedText.toString())) { + return Optional.empty(); + } + + matchedTokens.add(token); + if (matchedText.toString().equals(text)) { + return Optional.of(matchedTokens); + } + + current = token.getNextToken(); + } + + return Optional.empty(); + } + + private boolean applyGapBefore(JavaToken target, RenderContext context) { + Optional gap = gapBefore(target); + if (gap.isEmpty()) { + return false; + } + + if (gap.get().hasComment()) { + context.replacePendingWhitespaceWithRaw(gap.get().text()); + } else { + context.flushPendingWhitespace(); + } + return true; + } + + void finish(RenderContext context) { + Optional gap = remainingGap(); + if (gap.isPresent() && gap.get().hasComment()) { + context.replacePendingWhitespaceWithRaw(gap.get().text()); + } + } + + private Optional gapBefore(JavaToken target) { + Optional current = lastConsumed == null + ? Optional.of(begin) + : lastConsumed.getNextToken(); + StringBuilder text = new StringBuilder(); + boolean hasComment = false; + + while (current.isPresent() && current.get() != target) { + JavaToken token = current.get(); + if (!contains(token) || !token.getCategory().isWhitespaceOrComment()) { + return Optional.empty(); + } + hasComment = hasComment || token.getCategory().isComment(); + text.append(token.getText()); + current = token.getNextToken(); + } + + return current.isPresent() ? Optional.of(new OriginalGap(text.toString(), hasComment)) : Optional.empty(); + } + + private Optional remainingGap() { + Optional current = nextToken(); + StringBuilder text = new StringBuilder(); + boolean hasComment = false; + + while (current.isPresent()) { + JavaToken token = current.get(); + if (!contains(token) || !token.getCategory().isWhitespaceOrComment()) { + return Optional.empty(); + } + hasComment = hasComment || token.getCategory().isComment(); + text.append(token.getText()); + if (token == end) { + return Optional.of(new OriginalGap(text.toString(), hasComment)); + } + current = token.getNextToken(); + } + + return Optional.empty(); + } + + private Optional nextToken() { + if (lastConsumed == null) { + return Optional.of(begin); + } + if (lastConsumed == end) { + return Optional.empty(); + } + return lastConsumed.getNextToken(); + } + + private boolean contains(JavaToken token) { + Optional current = Optional.of(begin); + while (current.isPresent()) { + JavaToken currentToken = current.get(); + if (currentToken == token) { + return true; + } + if (currentToken == end) { + return false; + } + current = currentToken.getNextToken(); + } + return false; + } + + private static String removeWhitespace(String text) { + StringBuilder result = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + if (!Character.isWhitespace(ch)) { + result.append(ch); + } + } + return result.toString(); + } + + private static Optional tokenRange(Object value) { + if (value instanceof Node node) { + return node.getTokenRange(); + } + + if (value instanceof Optional optional) { + return optional.flatMap(SourceCursor::tokenRange); + } + + return Optional.empty(); + } +} diff --git a/src/main/java/org/example/ebnfFormatter/render/SourceRenderState.java b/src/main/java/org/example/ebnfFormatter/render/SourceRenderState.java new file mode 100644 index 0000000..1854439 --- /dev/null +++ b/src/main/java/org/example/ebnfFormatter/render/SourceRenderState.java @@ -0,0 +1,45 @@ +package org.example.ebnfFormatter.render; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Optional; + +final class SourceRenderState { + private final Deque> cursors = new ArrayDeque<>(); + + void enter(Object sourceValue) { + cursors.push(SourceCursor.create(sourceValue)); + } + + void exit(RenderContext context) { + Optional cursor = cursors.pop(); + cursor.ifPresent(sourceCursor -> sourceCursor.finish(context)); + } + + void beforeLiteral(String text, RenderContext context) { + Optional cursor = currentCursor(); + if (cursor.isEmpty()) { + context.flushPendingWhitespace(); + return; + } + + cursor.get().consumeLiteral(text, context); + } + + void beforeValue(Object value, RenderContext context) { + Optional cursor = currentCursor(); + if (cursor.isEmpty()) { + context.flushPendingWhitespace(); + return; + } + + cursor.get().consumeValue(value, context); + } + + private Optional currentCursor() { + if (cursors.isEmpty()) { + return Optional.empty(); + } + return cursors.peek(); + } +} diff --git a/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java b/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java index 682b0bd..d42121a 100644 --- a/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java +++ b/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java @@ -6,6 +6,7 @@ import com.github.javaparser.ast.Node; import com.github.javaparser.printer.Stringable; import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter; +import org.example.ebnfFormatter.match.AppliedRule; import org.example.ebnfFormatter.match.AppliedRuleValue; import org.example.ebnfFormatter.match.Bindings; import org.example.ebnfFormatter.match.BoundValue; @@ -13,7 +14,6 @@ import org.example.ebnfFormatter.model.format.*; import java.lang.reflect.Array; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -26,39 +26,87 @@ public String render(FormatAst format, Bindings bindings) { public String render(FormatAst format, Bindings bindings, NestedRuleRenderer nestedRuleRenderer) { RenderContext context = new RenderContext(); - renderInto(format, bindings, nestedRuleRenderer, context); + SourceRenderState sourceState = new SourceRenderState(); + renderInto(format, bindings, nestedRuleRenderer, context, sourceState); return context.result(); } + public String render(AppliedRule appliedRule, NestedRuleRenderer nestedRuleRenderer) { + RenderContext context = new RenderContext(); + SourceRenderState sourceState = new SourceRenderState(); + renderAppliedRule(appliedRule, nestedRuleRenderer, context, sourceState); + return context.result(); + } + + private void renderAppliedRule( + AppliedRule appliedRule, + NestedRuleRenderer nestedRuleRenderer, + RenderContext context, + SourceRenderState sourceState + ) { + sourceState.enter(appliedRule.sourceValue()); + try { + renderInto( + appliedRule.rule().format(), + appliedRule.bindings(), + nestedRuleRenderer, + context, + sourceState + ); + } finally { + sourceState.exit(context); + } + } + private void renderInto( FormatAst format, Bindings bindings, NestedRuleRenderer nestedRuleRenderer, - RenderContext context + RenderContext context, + SourceRenderState sourceState ) { switch (format) { - case FormatText text -> context.appendText(text.text()); + case FormatText text -> renderText(text.text(), context, sourceState); case FormatPlaceholder placeholder -> - renderPlaceholder(placeholder.name(), bindings, nestedRuleRenderer, context); + renderPlaceholder(placeholder.name(), bindings, nestedRuleRenderer, context, sourceState); case FormatDirective directive -> renderDirective(directive, context); case FormatSeq seq -> { for (FormatAst item : seq.items()) { - renderInto(item, bindings, nestedRuleRenderer, context); + renderInto(item, bindings, nestedRuleRenderer, context, sourceState); } } - case FormatGroup group -> renderInto(group.body(), bindings, nestedRuleRenderer, context); + case FormatGroup group -> renderInto(group.body(), bindings, nestedRuleRenderer, context, sourceState); case FormatIfPresent ifPresent -> { if (!bindings.findValues(ifPresent.name()).isEmpty()) { - renderInto(ifPresent.body(), bindings, nestedRuleRenderer, context); + renderInto(ifPresent.body(), bindings, nestedRuleRenderer, context, sourceState); } } - case FormatJoin join -> renderJoin(join, bindings, nestedRuleRenderer, context); + case FormatJoin join -> renderJoin(join, bindings, nestedRuleRenderer, context, sourceState); + } + } + + private void renderText(String text, RenderContext context, SourceRenderState sourceState) { + int index = 0; + while (index < text.length()) { + boolean whitespace = Character.isWhitespace(text.charAt(index)); + int start = index; + while (index < text.length() && Character.isWhitespace(text.charAt(index)) == whitespace) { + index++; + } + + String part = text.substring(start, index); + if (whitespace) { + context.appendPendingWhitespace(part); + } else { + sourceState.beforeLiteral(part, context); + context.appendText(part); + } } } @@ -75,11 +123,12 @@ private void renderPlaceholder( String placeholderName, Bindings bindings, NestedRuleRenderer nestedRuleRenderer, - RenderContext context + RenderContext context, + SourceRenderState sourceState ) { List values = bindings.getRequiredValues(placeholderName); for (BoundValue value : values) { - renderBoundValue(placeholderName, value, nestedRuleRenderer, context); + renderBoundValue(placeholderName, value, nestedRuleRenderer, context, sourceState); } } @@ -87,7 +136,8 @@ private void renderJoin( FormatJoin join, Bindings bindings, NestedRuleRenderer nestedRuleRenderer, - RenderContext context + RenderContext context, + SourceRenderState sourceState ) { List items = bindings.findValues(join.placeholderName()); boolean hasEmptySeparator = isEmptyFormat(join.separator()); @@ -95,105 +145,79 @@ private void renderJoin( for (int i = 0; i < items.size(); i++) { BoundValue item = items.get(i); if (i > 0) { - if (hasEmptySeparator) { - appendOriginalGapBetween(items.get(i - 1), item, context); - } else { - renderInto(join.separator(), bindings, nestedRuleRenderer, context); - } + appendJoinSeparator( + join, + bindings, + nestedRuleRenderer, + context, + sourceState, + items.get(i - 1), + item, + hasEmptySeparator + ); } - renderBoundValue(join.placeholderName(), item, nestedRuleRenderer, context); + renderBoundValue(join.placeholderName(), item, nestedRuleRenderer, context, sourceState); } if (hasEmptySeparator && !items.isEmpty()) { - appendOriginalGapAfter(items.getLast(), context); + appendEmptyJoinGapAfter(items.getLast(), context); } } - private boolean isEmptyFormat(FormatAst format) { - return switch (format) { - case FormatText text -> text.text().isEmpty(); - case FormatSeq seq -> seq.items().stream().allMatch(this::isEmptyFormat); - case FormatGroup group -> isEmptyFormat(group.body()); - default -> false; - }; - } - - private void appendOriginalGapBetween(BoundValue left, BoundValue right, RenderContext context) { - originalGapBetween(left.legacyValue(), right.legacyValue()).ifPresent(context::appendText); - } - - private void appendOriginalGapAfter(BoundValue value, RenderContext context) { - originalGapAfter(value.legacyValue()).ifPresent(context::appendText); - } - - private Optional originalGapBetween(Object left, Object right) { - Optional leftRange = tokenRange(left); - Optional rightRange = tokenRange(right); - if (leftRange.isEmpty() || rightRange.isEmpty()) { - return Optional.empty(); - } - - JavaToken end = rightRange.get().getBegin(); - Optional current = leftRange.get().getEnd().getNextToken(); - StringBuilder text = new StringBuilder(); - - while (current.isPresent() && current.get() != end) { - JavaToken token = current.get(); - if (!token.getCategory().isWhitespaceOrComment()) { - return Optional.empty(); - } - text.append(token.getText()); - current = token.getNextToken(); + private void appendJoinSeparator( + FormatJoin join, + Bindings bindings, + NestedRuleRenderer nestedRuleRenderer, + RenderContext context, + SourceRenderState sourceState, + BoundValue left, + BoundValue right, + boolean hasEmptySeparator + ) { + if (hasEmptySeparator) { + appendEmptyJoinGapBetween(left, right, context); + return; } - return current.isPresent() ? Optional.of(text.toString()) : Optional.empty(); + renderInto(join.separator(), bindings, nestedRuleRenderer, context, sourceState); } - private Optional originalGapAfter(Object value) { - Optional range = tokenRange(value); - if (range.isEmpty()) { - return Optional.empty(); - } - - Optional current = range.get().getEnd().getNextToken(); - StringBuilder text = new StringBuilder(); - - while (current.isPresent() && current.get().getCategory().isWhitespaceOrComment()) { - JavaToken token = current.get(); - text.append(token.getText()); - current = token.getNextToken(); + private void appendEmptyJoinGapBetween(BoundValue left, BoundValue right, RenderContext context) { + Optional originalGap = OriginalGaps.between(left.legacyValue(), right.legacyValue()); + if (originalGap.isPresent() && !originalGap.get().hasComment()) { + context.appendRawText(originalGap.get().text()); } - - return Optional.of(text.toString()); } - private Optional tokenRange(Object value) { - if (value instanceof Node node) { - return node.getTokenRange(); - } - - if (value instanceof Optional optional) { - return optional.flatMap(this::tokenRange); + private void appendEmptyJoinGapAfter(BoundValue item, RenderContext context) { + Optional originalGap = OriginalGaps.after(item.legacyValue()); + if (originalGap.isPresent() && !originalGap.get().hasComment()) { + context.appendRawText(originalGap.get().text()); } + } - return Optional.empty(); + private boolean isEmptyFormat(FormatAst format) { + return switch (format) { + case FormatText text -> text.text().isEmpty(); + case FormatSeq seq -> seq.items().stream().allMatch(this::isEmptyFormat); + case FormatGroup group -> isEmptyFormat(group.body()); + default -> false; + }; } private void renderBoundValue( String placeholderName, BoundValue boundValue, NestedRuleRenderer nestedRuleRenderer, - RenderContext context + RenderContext context, + SourceRenderState sourceState ) { + sourceState.beforeValue(boundValue.legacyValue(), context); + switch (boundValue) { case AppliedRuleValue appliedRuleValue -> - renderInto( - appliedRuleValue.appliedRule().rule().format(), - appliedRuleValue.appliedRule().bindings(), - nestedRuleRenderer, - context - ); - case RawValue rawValue -> renderRawValue(placeholderName, rawValue.value(), nestedRuleRenderer, context); + renderAppliedRule(appliedRuleValue.appliedRule(), nestedRuleRenderer, context, sourceState); + case RawValue rawValue -> renderRawValue(placeholderName, rawValue.value(), nestedRuleRenderer, context, sourceState); } } @@ -201,7 +225,8 @@ private void renderRawValue( String placeholderName, Object value, NestedRuleRenderer nestedRuleRenderer, - RenderContext context + RenderContext context, + SourceRenderState sourceState ) { if (value == null) { return; @@ -214,7 +239,7 @@ private void renderRawValue( } if (value instanceof Optional optional) { - optional.ifPresent(v -> renderRawValue(placeholderName, v, nestedRuleRenderer, context)); + optional.ifPresent(v -> renderRawValue(placeholderName, v, nestedRuleRenderer, context, sourceState)); return; } @@ -227,7 +252,8 @@ private void renderRawValue( Iterator it = iterable.iterator(); while (it.hasNext()) { Object item = it.next(); - renderRawValue(placeholderName, item, nestedRuleRenderer, context); + sourceState.beforeValue(item, context); + renderRawValue(placeholderName, item, nestedRuleRenderer, context, sourceState); } return; } @@ -236,7 +262,9 @@ private void renderRawValue( if (type.isArray()) { int length = Array.getLength(value); for (int i = 0; i < length; i++) { - renderRawValue(placeholderName, Array.get(value, i), nestedRuleRenderer, context); + Object item = Array.get(value, i); + sourceState.beforeValue(item, context); + renderRawValue(placeholderName, item, nestedRuleRenderer, context, sourceState); } return; } @@ -298,7 +326,106 @@ private void renderNode(Node node, NestedRuleRenderer nestedRuleRenderer, Render return; } - context.appendText(sourceText(node)); + appendSourceNode(node, context); + } + + private void appendSourceNode(Node node, RenderContext context) { + Optional tokenRange = node.getTokenRange(); + if (tokenRange.isEmpty()) { + context.appendText(sourceText(node)); + return; + } + + TokenRange range = tokenRange.get(); + int sourceIndent = sourceIndent(range.getBegin()); + int renderedSourceIndent = context.sourceStartColumn(); + int renderedInlineIndent = Math.max(0, renderedSourceIndent - context.currentIndentColumns()); + int sourceIndentToStrip = Math.max(0, sourceIndent - renderedInlineIndent); + int[] indentToStrip = {0}; + JavaToken token = range.getBegin(); + + while (true) { + appendSourceToken(token, sourceIndentToStrip, indentToStrip, context); + if (token == range.getEnd()) { + return; + } + + Optional nextToken = token.getNextToken(); + if (nextToken.isEmpty()) { + return; + } + token = nextToken.get(); + } + } + + private void appendSourceToken( + JavaToken token, + int sourceIndentToStrip, + int[] indentToStrip, + RenderContext context + ) { + if (token.getCategory().isWhitespace()) { + context.appendSourceWhitespace(stripSourceIndent(token.getText(), sourceIndentToStrip, indentToStrip)); + return; + } + + indentToStrip[0] = 0; + context.appendSourceTokenText(token.getText()); + } + + private int sourceIndent(JavaToken token) { + return token.getRange() + .map(range -> Math.max(0, range.begin.column - 1)) + .orElse(0); + } + + private String stripSourceIndent(String text, int sourceIndentToStrip, int[] indentToStrip) { + if (sourceIndentToStrip <= 0 || text.isEmpty()) { + return text; + } + + StringBuilder result = new StringBuilder(text.length()); + int index = 0; + while (index < text.length()) { + char ch = text.charAt(index); + + if (indentToStrip[0] > 0) { + if (ch == ' ') { + indentToStrip[0]--; + index++; + continue; + } + if (ch == '\t') { + indentToStrip[0] = Math.max(0, indentToStrip[0] - 4); + index++; + continue; + } + indentToStrip[0] = 0; + } + + if (ch == '\r') { + result.append(ch); + index++; + if (index < text.length() && text.charAt(index) == '\n') { + result.append('\n'); + index++; + } + indentToStrip[0] = sourceIndentToStrip; + continue; + } + + if (ch == '\n') { + result.append(ch); + index++; + indentToStrip[0] = sourceIndentToStrip; + continue; + } + + result.append(ch); + index++; + } + + return result.toString(); } private String sourceText(Node node) { @@ -322,33 +449,4 @@ private String stripQuantifierSuffix(String name) { }; } - private List toList(Object value) { - if (value == null) { - return List.of(); - } - - if (value instanceof List list) { - return list; - } - - if (value instanceof Iterable iterable) { - List result = new ArrayList<>(); - for (Object item : iterable) { - result.add(item); - } - return result; - } - - Class type = value.getClass(); - if (type.isArray()) { - int length = Array.getLength(value); - List result = new ArrayList<>(length); - for (int i = 0; i < length; i++) { - result.add(Array.get(value, i)); - } - return result; - } - - return List.of(value); - } } diff --git a/src/main/java/org/example/ebnfFormatter/runtime/FormatterEngine.java b/src/main/java/org/example/ebnfFormatter/runtime/FormatterEngine.java index 06cd440..ff8d309 100644 --- a/src/main/java/org/example/ebnfFormatter/runtime/FormatterEngine.java +++ b/src/main/java/org/example/ebnfFormatter/runtime/FormatterEngine.java @@ -35,8 +35,7 @@ public Optional tryRender(String ruleName, Object value) { MatchResult match = patternMatcher.match(rule, value); if (match.matched()) { return Optional.of(templateRenderer.render( - rule.format(), - match.bindings(), + match.appliedRule(), this::tryRenderNested )); } diff --git a/src/test/java/org/example/ebnfFormatter/runtime/ALotOfEndToEndTest.java b/src/test/java/org/example/ebnfFormatter/runtime/ALotOfEndToEndTest.java index c78e00f..903e3a9 100644 --- a/src/test/java/org/example/ebnfFormatter/runtime/ALotOfEndToEndTest.java +++ b/src/test/java/org/example/ebnfFormatter/runtime/ALotOfEndToEndTest.java @@ -747,9 +747,9 @@ class Deep { int run(int a, int b) { if (a > b) { while (i < a) { - if (i == b) - return i;tick();++i; - } + if (i == b) + return i;tick();++i; + } return a; } else if (a == b) { diff --git a/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java b/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java index e157408..bb9bb6b 100644 --- a/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java +++ b/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java @@ -162,9 +162,7 @@ public abstract int sum(Input } """; - String expected = """ - public abstract int sum(Input - input)"""; + String expected = "public abstract int sum(Input\n input)"; String formatted = formatFirstNode(methodDeclarationRules, code, MethodDeclaration.class, "MethodDeclaration"); assertThat(formatted).isEqualTo(expected); diff --git a/src/test/java/org/example/ebnfFormatter/runtime/LikeReadmeNodeFormattingE2ETest.java b/src/test/java/org/example/ebnfFormatter/runtime/LikeReadmeNodeFormattingE2ETest.java index d4c5654..66fe547 100644 --- a/src/test/java/org/example/ebnfFormatter/runtime/LikeReadmeNodeFormattingE2ETest.java +++ b/src/test/java/org/example/ebnfFormatter/runtime/LikeReadmeNodeFormattingE2ETest.java @@ -68,7 +68,7 @@ public abstract int sum(Input\s } """; - String expected = "public abstract int sum(Input \n input)"; + String expected = "public abstract int sum(Input \n input)"; MethodDeclaration node = StaticJavaParser.parse(source) .findFirst(MethodDeclaration.class)