|
25 | 25 |
|
26 | 26 | import com.google.common.base.CharMatcher; |
27 | 27 | import com.google.common.collect.ImmutableList; |
| 28 | +import com.google.common.collect.ImmutableSet; |
28 | 29 | import com.google.common.collect.PeekingIterator; |
29 | 30 | import com.google.googlejavaformat.java.javadoc.Token.BeginJavadoc; |
30 | 31 | import com.google.googlejavaformat.java.javadoc.Token.BlockquoteCloseTag; |
@@ -103,14 +104,35 @@ private static String stripJavadocBeginAndEnd(String input) { |
103 | 104 | return input.substring("/**".length(), input.length() - "*/".length()); |
104 | 105 | } |
105 | 106 |
|
| 107 | + /** |
| 108 | + * An element of the nested contexts we might be in. For example, if we are inside {@code |
| 109 | + * <pre>{@code ...}</pre>} then the stack of nested contexts would be {@code PRE} plus {@code |
| 110 | + * CODE_CONTEXT}. |
| 111 | + */ |
| 112 | + enum NestingContext { |
| 113 | + /** {@code <pre>...</pre>}. */ |
| 114 | + PRE, |
| 115 | + |
| 116 | + /** {@code {@code ...}}. */ |
| 117 | + CODE_CONTEXT, |
| 118 | + |
| 119 | + /** {@code <table>...</table>}. */ |
| 120 | + TABLE, |
| 121 | + |
| 122 | + /** {@code {@snippet ...}}. */ |
| 123 | + SNIPPET_CONTEXT, |
| 124 | + |
| 125 | + /** Nested braces within one of the other contexts. */ |
| 126 | + BRACE_CONTEXT, |
| 127 | + |
| 128 | + /** {@code an inline tag such as {@link ...}} */ |
| 129 | + TAG_CONTEXT |
| 130 | + } |
| 131 | + |
106 | 132 | private final CharStream input; |
107 | 133 | private final boolean classicJavadoc; |
108 | 134 | private final MarkdownPositions markdownPositions; |
109 | | - private final NestingStack braceStack = new NestingStack(); |
110 | | - private final NestingStack preStack = new NestingStack(); |
111 | | - private final NestingStack codeStack = new NestingStack(); |
112 | | - private final NestingStack tableStack = new NestingStack(); |
113 | | - private boolean outerInlineTagIsSnippet; |
| 135 | + private final NestingStack<NestingContext> contextStack = new NestingStack<>(); |
114 | 136 | private boolean somethingSinceNewline; |
115 | 137 |
|
116 | 138 | private JavadocLexer( |
@@ -200,56 +222,60 @@ private Function<String, Token> consumeToken() throws LexException { |
200 | 222 | somethingSinceNewline = true; |
201 | 223 |
|
202 | 224 | if (input.tryConsumeRegex(SNIPPET_TAG_OPEN_PATTERN)) { |
203 | | - if (braceStack.isEmpty()) { |
204 | | - braceStack.push(); |
205 | | - outerInlineTagIsSnippet = true; |
206 | | - return SnippetBegin::new; |
207 | | - } |
208 | | - braceStack.push(); |
209 | | - return Literal::new; |
| 225 | + // {@snippet ...} |
| 226 | + boolean outermost = contextStack.isEmpty(); |
| 227 | + contextStack.push(NestingContext.SNIPPET_CONTEXT); |
| 228 | + return outermost ? SnippetBegin::new : Literal::new; |
210 | 229 | } else if (input.tryConsumeRegex(INLINE_TAG_OPEN_PATTERN)) { |
211 | | - braceStack.push(); |
| 230 | + // {@foo ...}. We recognize this even in something like {@code {@foo ...}}, but it doesn't |
| 231 | + // make any difference. |
| 232 | + contextStack.push(NestingContext.TAG_CONTEXT); |
212 | 233 | return Literal::new; |
213 | 234 | } else if (input.tryConsume("{")) { |
214 | | - braceStack.incrementIfPositive(); |
| 235 | + // A left brace that is not the start of {@foo}. If we are inside another context, we'll |
| 236 | + // record the brace, for cases like {@code foo{bar}}, where the second brace is the end of the |
| 237 | + // tag. |
| 238 | + if (contextStack.containsAny(BRACE_CONTEXTS)) { |
| 239 | + contextStack.push(NestingContext.BRACE_CONTEXT); |
| 240 | + } |
215 | 241 | return Literal::new; |
216 | 242 | } else if (input.tryConsume("}")) { |
217 | | - if (outerInlineTagIsSnippet && braceStack.total() == 1) { |
218 | | - braceStack.popIfNotEmpty(); |
219 | | - outerInlineTagIsSnippet = false; |
| 243 | + var popped = contextStack.popIfNotEmpty(); |
| 244 | + if (contextStack.isEmpty() && popped == NestingContext.SNIPPET_CONTEXT) { |
220 | 245 | return SnippetEnd::new; |
221 | 246 | } |
222 | | - braceStack.popIfNotEmpty(); |
223 | 247 | return Literal::new; |
224 | 248 | } |
225 | 249 |
|
226 | 250 | // Inside an inline tag, don't do any HTML interpretation. |
227 | | - if (!braceStack.isEmpty()) { |
| 251 | + if (contextStack.containsAny(TAG_CONTEXTS)) { |
228 | 252 | verify(input.tryConsumeRegex(literalPattern())); |
229 | 253 | return Literal::new; |
230 | 254 | } |
231 | 255 |
|
232 | 256 | if (input.tryConsumeRegex(PRE_OPEN_PATTERN)) { |
233 | | - preStack.push(); |
| 257 | + contextStack.push(NestingContext.PRE); |
234 | 258 | return preserveExistingFormatting ? Literal::new : PreOpenTag::new; |
235 | 259 | } else if (input.tryConsumeRegex(PRE_CLOSE_PATTERN)) { |
236 | | - preStack.popIfNotEmpty(); |
| 260 | + contextStack.popUntil(NestingContext.PRE); |
237 | 261 | return preserveExistingFormatting() ? Literal::new : PreCloseTag::new; |
238 | 262 | } |
239 | 263 |
|
240 | 264 | if (input.tryConsumeRegex(CODE_OPEN_PATTERN)) { |
241 | | - codeStack.push(); |
| 265 | + // <code> |
| 266 | + contextStack.push(NestingContext.CODE_CONTEXT); |
242 | 267 | return preserveExistingFormatting ? Literal::new : CodeOpenTag::new; |
243 | 268 | } else if (input.tryConsumeRegex(CODE_CLOSE_PATTERN)) { |
244 | | - codeStack.popIfNotEmpty(); |
| 269 | + // </code> |
| 270 | + contextStack.popUntil(NestingContext.CODE_CONTEXT); |
245 | 271 | return preserveExistingFormatting() ? Literal::new : CodeCloseTag::new; |
246 | 272 | } |
247 | 273 |
|
248 | 274 | if (input.tryConsumeRegex(TABLE_OPEN_PATTERN)) { |
249 | | - tableStack.push(); |
| 275 | + contextStack.push(NestingContext.TABLE); |
250 | 276 | return preserveExistingFormatting ? Literal::new : TableOpenTag::new; |
251 | 277 | } else if (input.tryConsumeRegex(TABLE_CLOSE_PATTERN)) { |
252 | | - tableStack.popIfNotEmpty(); |
| 278 | + contextStack.popUntil(NestingContext.TABLE); |
253 | 279 | return preserveExistingFormatting() ? Literal::new : TableCloseTag::new; |
254 | 280 | } |
255 | 281 |
|
@@ -293,17 +319,11 @@ private Function<String, Token> consumeToken() throws LexException { |
293 | 319 | } |
294 | 320 |
|
295 | 321 | private boolean preserveExistingFormatting() { |
296 | | - return !preStack.isEmpty() |
297 | | - || !tableStack.isEmpty() |
298 | | - || !codeStack.isEmpty() |
299 | | - || outerInlineTagIsSnippet; |
| 322 | + return contextStack.containsAny(PRESERVE_FORMATTING_CONTEXTS); |
300 | 323 | } |
301 | 324 |
|
302 | 325 | private void checkMatchingTags() throws LexException { |
303 | | - if (!braceStack.isEmpty() |
304 | | - || !preStack.isEmpty() |
305 | | - || !tableStack.isEmpty() |
306 | | - || !codeStack.isEmpty()) { |
| 326 | + if (!contextStack.isEmpty()) { |
307 | 327 | throw new LexException(); |
308 | 328 | } |
309 | 329 | } |
@@ -535,6 +555,31 @@ private static void deindentPreCodeBlock( |
535 | 555 | } |
536 | 556 | } |
537 | 557 |
|
| 558 | + /** Contexts that imply that we should not do HTML interpretation. */ |
| 559 | + private static final ImmutableSet<NestingContext> TAG_CONTEXTS = |
| 560 | + ImmutableSet.of(NestingContext.SNIPPET_CONTEXT, NestingContext.TAG_CONTEXT); |
| 561 | + |
| 562 | + /** |
| 563 | + * Contexts that are opened by a left brace and closed by a matching right brace. These are the |
| 564 | + * ones where a nested left brace should open a nested context. |
| 565 | + */ |
| 566 | + private static final ImmutableSet<NestingContext> BRACE_CONTEXTS = |
| 567 | + ImmutableSet.of( |
| 568 | + NestingContext.CODE_CONTEXT, |
| 569 | + NestingContext.SNIPPET_CONTEXT, |
| 570 | + NestingContext.BRACE_CONTEXT); |
| 571 | + |
| 572 | + /** |
| 573 | + * Contexts that preserve formatting, including line breaks and leading whitespace, within the |
| 574 | + * context. |
| 575 | + */ |
| 576 | + private static final ImmutableSet<NestingContext> PRESERVE_FORMATTING_CONTEXTS = |
| 577 | + ImmutableSet.of( |
| 578 | + NestingContext.PRE, |
| 579 | + NestingContext.TABLE, |
| 580 | + NestingContext.CODE_CONTEXT, |
| 581 | + NestingContext.SNIPPET_CONTEXT); |
| 582 | + |
538 | 583 | private static final CharMatcher NEWLINE = CharMatcher.is('\n'); |
539 | 584 |
|
540 | 585 | private static boolean hasMultipleNewlines(String s) { |
|
0 commit comments