Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
* <pre>{@code ...}</pre>} then the stack of nested contexts would be {@code PRE} plus {@code
* CODE_CONTEXT}.
*/
enum NestingContext {
/** {@code <pre>...</pre>}. */
HTML_PRE_CONTEXT,

/** {@code <code>...</code>}. */
HTML_CODE_CONTEXT,

/** {@code <table>...</table>}. */
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<NestingContext> contextStack = new NestingStack<>();
private boolean somethingSinceNewline;

private JavadocLexer(
Expand Down Expand Up @@ -200,56 +225,61 @@ private Function<String, Token> 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;
}

Expand Down Expand Up @@ -293,17 +323,11 @@ private Function<String, Token> 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();
}
}
Expand Down Expand Up @@ -535,6 +559,31 @@ private static void deindentPreCodeBlock(
}
}

/** Contexts that imply that we should not do HTML interpretation. */
private static final ImmutableSet<NestingContext> 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<NestingContext> 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<NestingContext> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,91 @@
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 <E> The type of the elements in the stack.
*/
final class NestingStack {
private int total;
private final Deque<Integer> stack = new ArrayDeque<>();
final class NestingStack<E> {
private final Deque<E> stack = new ArrayDeque<>();

int total() {
return total;
void push(E value) {
stack.push(value);
}

void push() {
push(1);
@Nullable E popIfIn(Collection<E> 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<E> values) {
return stack.stream().anyMatch(values::contains);
}

boolean isEmpty() {
return stack.isEmpty();
}

void reset() {
total = 0;
stack.clear();
}

static final class Int {
private final Deque<Integer> 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;
}
}
}
Loading