diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java
index 68640a926..f0a794b3f 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java
@@ -25,6 +25,7 @@
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.PeekingIterator;
import com.google.googlejavaformat.java.javadoc.Token.BeginJavadoc;
import com.google.googlejavaformat.java.javadoc.Token.BlockquoteCloseTag;
@@ -103,14 +104,38 @@ private static String stripJavadocBeginAndEnd(String input) {
return input.substring("/**".length(), input.length() - "*/".length());
}
+ /**
+ * An element of the nested contexts we might be in. For example, if we are inside {@code
+ *
{@code ...}} then the stack of nested contexts would be {@code PRE} plus {@code
+ * CODE_CONTEXT}.
+ */
+ enum NestingContext {
+ /** {@code ...
}. */
+ HTML_PRE_CONTEXT,
+
+ /** {@code ...}. */
+ HTML_CODE_CONTEXT,
+
+ /** {@code }. */
+ TABLE,
+
+ /** {@code {@snippet ...}}. */
+ SNIPPET_CONTEXT,
+
+ /** Nested braces within one of the other contexts. */
+ BRACE_CONTEXT,
+
+ /**
+ * An inline tag such as {@code {@link ...}} or {@code {@code ...}}, but not {@code {@snippet
+ * ...}}.
+ */
+ INLINE_TAG_CONTEXT
+ }
+
private final CharStream input;
private final boolean classicJavadoc;
private final MarkdownPositions markdownPositions;
- private final NestingStack braceStack = new NestingStack();
- private final NestingStack preStack = new NestingStack();
- private final NestingStack codeStack = new NestingStack();
- private final NestingStack tableStack = new NestingStack();
- private boolean outerInlineTagIsSnippet;
+ private final NestingStack contextStack = new NestingStack<>();
private boolean somethingSinceNewline;
private JavadocLexer(
@@ -200,56 +225,61 @@ private Function consumeToken() throws LexException {
somethingSinceNewline = true;
if (input.tryConsumeRegex(SNIPPET_TAG_OPEN_PATTERN)) {
- if (braceStack.isEmpty()) {
- braceStack.push();
- outerInlineTagIsSnippet = true;
+ // {@snippet ...}
+ if (contextStack.containsAny(BRACE_CONTEXTS)) {
+ contextStack.push(NestingContext.BRACE_CONTEXT);
+ return Literal::new;
+ } else {
+ contextStack.push(NestingContext.SNIPPET_CONTEXT);
return SnippetBegin::new;
}
- braceStack.push();
- return Literal::new;
} else if (input.tryConsumeRegex(INLINE_TAG_OPEN_PATTERN)) {
- braceStack.push();
+ // {@foo ...}. We recognize this even in something like {@code {@foo ...}}, but it doesn't
+ // make any difference.
+ contextStack.push(NestingContext.INLINE_TAG_CONTEXT);
return Literal::new;
} else if (input.tryConsume("{")) {
- braceStack.incrementIfPositive();
+ // A left brace that is not the start of {@foo}. We record the brace, for cases like
+ // `{@code foo{bar}}`, where the second right brace is the end of the tag.
+ if (contextStack.containsAny(BRACE_CONTEXTS)) {
+ contextStack.push(NestingContext.BRACE_CONTEXT);
+ }
return Literal::new;
} else if (input.tryConsume("}")) {
- if (outerInlineTagIsSnippet && braceStack.total() == 1) {
- braceStack.popIfNotEmpty();
- outerInlineTagIsSnippet = false;
+ var popped = contextStack.popIfIn(BRACE_CONTEXTS);
+ if (popped == NestingContext.SNIPPET_CONTEXT) {
return SnippetEnd::new;
}
- braceStack.popIfNotEmpty();
return Literal::new;
}
// Inside an inline tag, don't do any HTML interpretation.
- if (!braceStack.isEmpty()) {
+ if (contextStack.containsAny(TAG_CONTEXTS)) {
verify(input.tryConsumeRegex(literalPattern()));
return Literal::new;
}
if (input.tryConsumeRegex(PRE_OPEN_PATTERN)) {
- preStack.push();
+ contextStack.push(NestingContext.HTML_PRE_CONTEXT);
return preserveExistingFormatting ? Literal::new : PreOpenTag::new;
} else if (input.tryConsumeRegex(PRE_CLOSE_PATTERN)) {
- preStack.popIfNotEmpty();
+ contextStack.popUntil(NestingContext.HTML_PRE_CONTEXT);
return preserveExistingFormatting() ? Literal::new : PreCloseTag::new;
}
if (input.tryConsumeRegex(CODE_OPEN_PATTERN)) {
- codeStack.push();
+ contextStack.push(NestingContext.HTML_CODE_CONTEXT);
return preserveExistingFormatting ? Literal::new : CodeOpenTag::new;
} else if (input.tryConsumeRegex(CODE_CLOSE_PATTERN)) {
- codeStack.popIfNotEmpty();
+ contextStack.popUntil(NestingContext.HTML_CODE_CONTEXT);
return preserveExistingFormatting() ? Literal::new : CodeCloseTag::new;
}
if (input.tryConsumeRegex(TABLE_OPEN_PATTERN)) {
- tableStack.push();
+ contextStack.push(NestingContext.TABLE);
return preserveExistingFormatting ? Literal::new : TableOpenTag::new;
} else if (input.tryConsumeRegex(TABLE_CLOSE_PATTERN)) {
- tableStack.popIfNotEmpty();
+ contextStack.popUntil(NestingContext.TABLE);
return preserveExistingFormatting() ? Literal::new : TableCloseTag::new;
}
@@ -293,17 +323,11 @@ private Function consumeToken() throws LexException {
}
private boolean preserveExistingFormatting() {
- return !preStack.isEmpty()
- || !tableStack.isEmpty()
- || !codeStack.isEmpty()
- || outerInlineTagIsSnippet;
+ return contextStack.containsAny(PRESERVE_FORMATTING_CONTEXTS);
}
private void checkMatchingTags() throws LexException {
- if (!braceStack.isEmpty()
- || !preStack.isEmpty()
- || !tableStack.isEmpty()
- || !codeStack.isEmpty()) {
+ if (!contextStack.isEmpty()) {
throw new LexException();
}
}
@@ -535,6 +559,31 @@ private static void deindentPreCodeBlock(
}
}
+ /** Contexts that imply that we should not do HTML interpretation. */
+ private static final ImmutableSet TAG_CONTEXTS =
+ ImmutableSet.of(NestingContext.SNIPPET_CONTEXT, NestingContext.INLINE_TAG_CONTEXT);
+
+ /**
+ * Contexts that are opened by a left brace and closed by a matching right brace. These are the
+ * ones where a nested left brace should open a nested context.
+ */
+ private static final ImmutableSet BRACE_CONTEXTS =
+ ImmutableSet.of(
+ NestingContext.SNIPPET_CONTEXT,
+ NestingContext.INLINE_TAG_CONTEXT,
+ NestingContext.BRACE_CONTEXT);
+
+ /**
+ * Contexts that preserve formatting, including line breaks and leading whitespace, within the
+ * context.
+ */
+ private static final ImmutableSet PRESERVE_FORMATTING_CONTEXTS =
+ ImmutableSet.of(
+ NestingContext.HTML_PRE_CONTEXT,
+ NestingContext.TABLE,
+ NestingContext.HTML_CODE_CONTEXT,
+ NestingContext.SNIPPET_CONTEXT);
+
private static final CharMatcher NEWLINE = CharMatcher.is('\n');
private static boolean hasMultipleNewlines(String s) {
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
index 019e129fc..078fd44b6 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
@@ -65,9 +65,9 @@ final class JavadocWriter {
private boolean continuingListItemOfInnermostList;
private boolean continuingFooterTag;
- private final NestingStack continuingListItemStack = new NestingStack();
- private final NestingStack continuingListStack = new NestingStack();
- private final NestingStack postWriteModifiedContinuingListStack = new NestingStack();
+ private final NestingStack.Int continuingListItemStack = new NestingStack.Int();
+ private final NestingStack.Int continuingListStack = new NestingStack.Int();
+ private final NestingStack.Int postWriteModifiedContinuingListStack = new NestingStack.Int();
private int remainingOnLine;
private boolean atStartOfLine;
private RequestedWhitespace requestedWhitespace = NONE;
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java
index c029428df..29197b8b5 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java
@@ -15,41 +15,51 @@
package com.google.googlejavaformat.java.javadoc;
import java.util.ArrayDeque;
+import java.util.Collection;
import java.util.Deque;
+import org.jspecify.annotations.Nullable;
/**
- * Stack for tracking the level of nesting. In the simplest case, each entry is just the integer 1,
- * and the stack is effectively a counter. In more complex cases, the entries may depend on context.
- * For example, if the stack is keeping track of Javadoc lists, the entries represent indentation
- * levels, and those depend on whether the list is an HTML list or a Markdown list.
+ * Stack for tracking the level of nesting. In the simplest case, we have a stack of {@link Integer}
+ * where each entry is just the integer 1, and the stack is effectively a counter. In more complex
+ * cases, the entries may depend on context. For example, if the stack is keeping track of Javadoc
+ * lists, the entries represent indentation levels, and those depend on whether the list is an HTML
+ * list or a Markdown list.
+ *
+ * @param The type of the elements in the stack.
*/
-final class NestingStack {
- private int total;
- private final Deque stack = new ArrayDeque<>();
+final class NestingStack {
+ private final Deque stack = new ArrayDeque<>();
- int total() {
- return total;
+ void push(E value) {
+ stack.push(value);
}
- void push() {
- push(1);
+ @Nullable E popIfIn(Collection values) {
+ if (isEmpty() || !values.contains(stack.peek())) {
+ return null;
+ }
+ return stack.pop();
}
- void push(int value) {
- stack.push(value);
- total += value;
+ /**
+ * If the stack contains the given element, pop it and everything above it. Otherwise, do nothing.
+ */
+ void popUntil(E value) {
+ if (stack.contains(value)) {
+ E popped;
+ do {
+ popped = stack.pop();
+ } while (!popped.equals(value));
+ }
}
- void incrementIfPositive() {
- if (total > 0) {
- push();
- }
+ boolean contains(E value) {
+ return stack.contains(value);
}
- void popIfNotEmpty() {
- if (!isEmpty()) {
- total -= stack.pop();
- }
+ boolean containsAny(Collection values) {
+ return stack.stream().anyMatch(values::contains);
}
boolean isEmpty() {
@@ -57,7 +67,39 @@ boolean isEmpty() {
}
void reset() {
- total = 0;
stack.clear();
}
+
+ static final class Int {
+ private final Deque stack = new ArrayDeque<>();
+ private int total;
+
+ int total() {
+ return total;
+ }
+
+ void push(int value) {
+ stack.push(value);
+ total += value;
+ }
+
+ void push() {
+ push(1);
+ }
+
+ void popIfNotEmpty() {
+ if (!stack.isEmpty()) {
+ total -= stack.pop();
+ }
+ }
+
+ boolean isEmpty() {
+ return stack.isEmpty();
+ }
+
+ void reset() {
+ stack.clear();
+ total = 0;
+ }
+ }
}