diff --git a/java/java-impl/src/com/intellij/codeInsight/javadoc/SnippetMarkup.java b/java/java-impl/src/com/intellij/codeInsight/javadoc/SnippetMarkup.java new file mode 100644 index 000000000000..eaae22966b3b --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/javadoc/SnippetMarkup.java @@ -0,0 +1,357 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.javadoc; + +import com.intellij.java.JavaBundle; +import com.intellij.openapi.util.NlsContexts; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; +import com.intellij.psi.javadoc.PsiSnippetDocTagBody; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.Stack; +import one.util.streamex.StreamEx; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public class SnippetMarkup { + private final @NotNull List<@NotNull MarkupNode> myNodes; + + // \\S+ = language-dependent comment start token, like "//" or "#" + private static final Pattern MARKUP_TAG = Pattern.compile("\\S+\\s*@(start|end|highlight|replace|link)\\s*(\\S.+?)?\\s*(:?)\\s*$"); + + private static final Pattern ATTRIBUTE = Pattern.compile("(\\w+)\\s*(=\\s*(['\"]?)(\\S+)\\3)?"); + + private static final Map> ALLOWED_ATTRS = + Map.of( + "start", Set.of("region"), + "end", Set.of("region"), + "highlight", Set.of("substring", "regex", "region", "type"), + "replace", Set.of("substring", "regex", "region", "replacement"), + "link", Set.of("substring", "regex", "region", "target", "type") + ); + + private SnippetMarkup(@NotNull List<@NotNull MarkupNode> nodes) { + myNodes = nodes; + } + + /** + * Type of the highlighting + * @see Highlight + */ + public enum HighlightType { + BOLD, ITALIC, HIGHLIGHTED + } + + /** + * Type of the link + * @see Link + */ + public enum LinkType { + LINK, LINKPLAIN + } + + /** + * Element of snippet text + */ + public sealed interface MarkupNode { + /** + * @return range in the parent PSI element that corresponds to this element + */ + @NotNull TextRange range(); + } + + /** + * A markup tag applicable to a particular part of original text + */ + public sealed interface LocationMarkupNode extends MarkupNode { + /** + * @return substring text to which this node is applicable + */ + @Nullable String substring(); + + /** + * @return regular expression, so this node is applicable to the matching substrings + */ + @Nullable String regex(); + + /** + * @return if present, defines a region name to which this tag is applicable. If null, then the tag is applicable to the next + * {@link PlainText} node. + */ + @Nullable String region(); + } + + /** + * A node that represents a chunk of plain text, ending with a linebreak + * + * @param content text content (excluding the final linebreak) + */ + public record PlainText(@NotNull TextRange range, @NotNull String content) implements MarkupNode { + public PlainText { + if (range.getLength() != content.length()) { + throw new IllegalArgumentException(); + } + } + + /** + * @param start start offset of substring + * @param end end offset of substring + * @return PlainText object that corresponds to the substring of this object + */ + @NotNull PlainText substring(@SuppressWarnings("SameParameterValue") int start, int end) { + int offset = range().getStartOffset(); + return new PlainText(TextRange.create(offset + start, offset + end), content.substring(start, end)); + } + } + + private record Attribute(@NotNull TextRange range, @NotNull String key, @Nullable String value) implements MarkupNode { + } + + /** + * Represents a {@code @start} tag + * + * @param region name of the region + */ + public record StartRegion(@NotNull TextRange range, @NotNull String region) implements MarkupNode { + } + + /** + * Represents an {@code @end} tag + * @param region name of the region to close. If null then the most recently open region is closed by this tag + */ + public record EndRegion(@NotNull TextRange range, @Nullable String region) implements MarkupNode { + } + + /** + * Represents a malformed markup chunk + * + * @param message localized error message + */ + public record ErrorMarkup(@NotNull TextRange range, @NlsContexts.ParsingError @NotNull String message) implements MarkupNode { + } + + /** + * Represents a {@code @highlight} tag + * + * @param type highlighting type + */ + public record Highlight(@NotNull TextRange range, @Nullable String substring, @Nullable String regex, @Nullable String region, + @NotNull HighlightType type) implements LocationMarkupNode { + } + + /** + * Represents a {@code @replace} tag + * + * @param replacement replacement text + */ + public record Replace(@NotNull TextRange range, @Nullable String substring, @Nullable String regex, @Nullable String region, + @NotNull String replacement) implements LocationMarkupNode { + } + + /** + * Represents a {@code @link} tag + * + * @param target link target + * @param linkType link type + */ + public record Link(@NotNull TextRange range, @Nullable String substring, @Nullable String regex, @Nullable String region, @NotNull String target, + @NotNull LinkType linkType) implements LocationMarkupNode { + } + + public static @NotNull SnippetMarkup parse(@NotNull String text) { + return parse(preparse(text)); + } + + public static @NotNull SnippetMarkup parse(@NotNull PsiSnippetDocTagBody body) { + return parse(preparse(body)); + } + + private static @NotNull List<@NotNull PlainText> preparse(@NotNull String text) { + List output = new ArrayList<>(); + int pos = 0; + while (true) { + int nextPos = text.indexOf('\n', pos); + if (nextPos == -1) { + output.add(new PlainText(TextRange.create(pos, text.length()), text.substring(pos))); + break; + } + else { + output.add(new PlainText(TextRange.create(pos, nextPos), text.substring(pos, nextPos))); + pos = nextPos + 1; + } + } + return output; + } + + private static @NotNull List<@NotNull PlainText> preparse(@NotNull PsiSnippetDocTagBody body) { + List<PlainText> output = new ArrayList<>(); + for (PsiElement element : body.getContent()) { + output.add(new PlainText(element.getTextRangeInParent(), element.getText())); + } + return output; + } + + private static @NotNull SnippetMarkup parse(@NotNull List<@NotNull PlainText> preparsed) { + return new SnippetMarkup(preparsed.stream().flatMap(SnippetMarkup::parseLine).toList()); + } + + private static Stream<MarkupNode> parseLine(@NotNull PlainText text) { + String content = text.content(); + Matcher matcher = MARKUP_TAG.matcher(content); + if (!matcher.find()) return Stream.of(text); + int start = matcher.start(); + PlainText prev = text.substring(0, start); + String tagName = matcher.group(1); + List<MarkupNode> attrs = parseAttributes(text.range().getStartOffset() + matcher.start(2), matcher.group(2), + ALLOWED_ATTRS.get(tagName)); + List<ErrorMarkup> errors = new ArrayList<>(ContainerUtil.filterIsInstance(attrs, ErrorMarkup.class)); + Map<String, String> attrValues = StreamEx.of(attrs).select(Attribute.class).toMap(Attribute::key, Attribute::value); + TextRange range = TextRange.create(matcher.start(1), matcher.end()).shiftRight(text.range().getStartOffset()); + boolean hasColon = !matcher.group(3).isEmpty(); + MarkupNode node = switch (tagName) { + case "start" -> { + String region = attrValues.get("region"); + if (region == null) { + yield new ErrorMarkup(range, JavaBundle.message("javadoc.snippet.error.missing.required.attribute", "@start", "region")); + } + yield new StartRegion(range, region); + } + case "end" -> new EndRegion(range, attrValues.get("region")); + case "highlight" -> { + Attribute typeAttr = + (Attribute)ContainerUtil.find(attrs, n -> n instanceof Attribute attr && attr.key().equals("type")); + HighlightType type = null; + if (typeAttr != null) { + type = ContainerUtil.find(HighlightType.values(), + ht -> ht.name().toLowerCase(Locale.ROOT).equals(typeAttr.value())); + if (type == null) { + errors.add(new ErrorMarkup(typeAttr.range(), JavaBundle.message("javadoc.snippet.error.unknown.highlight.type", typeAttr.value()))); + } + } + yield new Highlight(range, attrValues.get("substring"), attrValues.get("regex"), attrValues.get("region"), + Objects.requireNonNullElse(type, HighlightType.HIGHLIGHTED)); + } + case "replace" -> { + String replacement = attrValues.get("replacement"); + if (replacement == null) { + errors.add(new ErrorMarkup(range, JavaBundle.message("javadoc.snippet.error.missing.required.attribute", "@replace", "replacement"))); + replacement = ""; + } + yield new Replace(range, attrValues.get("substring"), attrValues.get("regex"), attrValues.get("region"), replacement); + } + case "link" -> { + String target = attrValues.get("target"); + if (target == null) { + errors.add(new ErrorMarkup(range, JavaBundle.message("javadoc.snippet.error.missing.required.attribute", "@link", "target"))); + target = ""; + } + Attribute typeAttr = + (Attribute)ContainerUtil.find(attrs, n -> n instanceof Attribute attr && attr.key().equals("type")); + LinkType type = null; + if (typeAttr != null) { + type = ContainerUtil.find(LinkType.values(), + ht -> ht.name().toLowerCase(Locale.ROOT).equals(typeAttr.value())); + if (type == null) { + errors.add(new ErrorMarkup(typeAttr.range(), JavaBundle.message("javadoc.snippet.error.unknown.link.type", typeAttr.value()))); + } + } + yield new Link(range, attrValues.get("substring"), attrValues.get("regex"), attrValues.get("region"), + target, Objects.requireNonNullElse(type, LinkType.LINK)); + } + default -> throw new AssertionError("Unexpected tag: " + tagName); + }; + return prev.content().isBlank() ? StreamEx.of(node).append(errors) : + hasColon ? StreamEx.of(prev, node).append(errors) : + StreamEx.of(node).append(errors).append(prev); + } + + private static List<MarkupNode> parseAttributes(int offset, @Nullable String attrs, Set<String> allowedAttrs) { + if (attrs == null) { + return List.of(); + } + Matcher matcher = ATTRIBUTE.matcher(attrs); + int pos = 0; + List<MarkupNode> result = new ArrayList<>(); + Set<String> used = new HashSet<>(); + while (true) { + boolean found = matcher.find(pos); + if (!found) { + if (pos < attrs.length()) { + result.add(new ErrorMarkup(TextRange.create(offset + pos, offset + attrs.length()), + JavaBundle.message("javadoc.snippet.error.malformed.attribute"))); + } + break; + } + if (!attrs.substring(pos, matcher.start()).isBlank()) { + result.add(new ErrorMarkup(TextRange.create(offset + pos, offset + matcher.start()), + JavaBundle.message("javadoc.snippet.error.malformed.attribute"))); + break; + } + String key = matcher.group(1); + TextRange range = TextRange.create(offset + matcher.start(), offset + matcher.end()); + if (!allowedAttrs.contains(key)) { + result.add(new ErrorMarkup(range, JavaBundle.message("javadoc.snippet.error.unsupported.attribute", key))); + } + else if (!used.add(key)) { + result.add(new ErrorMarkup(range, JavaBundle.message("javadoc.snippet.error.duplicate.attribute", key))); + } + else { + result.add(new Attribute(range, key, Objects.requireNonNullElse(matcher.group(4), ""))); + } + pos = matcher.end(); + } + return result; + } + + public interface SnippetVisitor { + void visitPlainText(@NotNull PlainText plainText, @NotNull List<@NotNull LocationMarkupNode> activeNodes); + + void visitError(@NotNull ErrorMarkup errorMarkup); + } + + public void visitSnippet(@Nullable String region, SnippetVisitor visitor) { + Stack<String> regions = new Stack<>(); + Map<String, List<LocationMarkupNode>> active = new LinkedHashMap<>(); + for (int i = 0; i < myNodes.size(); i++) { + MarkupNode node = myNodes.get(i); + if (node instanceof StartRegion start) { + regions.push(start.region()); + } else if (node instanceof EndRegion end) { + if (end.region() == null) { + active.remove(regions.pop()); + } else { + regions.remove(end.region()); + active.remove(end.region()); + } + } else if (node instanceof LocationMarkupNode loc) { + String regionName = loc.region(); + if (regionName != null) { + regions.push(regionName); + } + active.computeIfAbsent(regionName, k -> new ArrayList<>()).add(loc); + } + else if (node instanceof PlainText plainText) { + if (region == null || regions.contains(region)) { + visitor.visitPlainText(plainText, StreamEx.ofValues(active).toFlatList(Function.identity())); + } + active.remove(null); + } + else if (node instanceof ErrorMarkup errorMarkup) { + if (region == null || regions.contains(region)) { + visitor.visitError(errorMarkup); + } + } + } + } + + @Override + public String toString() { + return StringUtil.join(myNodes, "\n"); + } +} diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/SnippetMarkupTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/SnippetMarkupTest.java new file mode 100644 index 000000000000..a674f78d95c3 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/SnippetMarkupTest.java @@ -0,0 +1,230 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.codeInsight.javadoc; + +import com.intellij.codeInsight.javadoc.SnippetMarkup; +import com.intellij.codeInsight.javadoc.SnippetMarkup.ErrorMarkup; +import com.intellij.codeInsight.javadoc.SnippetMarkup.LocationMarkupNode; +import com.intellij.codeInsight.javadoc.SnippetMarkup.PlainText; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class SnippetMarkupTest { + @Test + public void plainText() { + testParsing( + """ + Hello!!! + World!!!""", """ + PlainText[range=(0,8), content=Hello!!!] + PlainText[range=(9,17), content=World!!!]"""); + } + + @Test + public void someMarkup() { + testParsing( + """ + public class X { + // @start region=test + void main() { + System.out.println("Hello"); // @replace substring=Hello replacement=... + // @highlight substring=Hello : + System.out.println("Hello"); + // @highlight region: + System.out.println("Hello1"); + System.out.println("Hello2"); // @link regex=H\\w+ target=Hello type=linkplain + // @end + } + // @end + } + """, """ + PlainText[range=(0,16), content=public class X {] + StartRegion[range=(23,40), region=test] + PlainText[range=(41,56), content= void main() {] + Replace[range=(94,133), substring=Hello, regex=null, region=null, replacement=...] + PlainText[range=(57,90), content= System.out.println("Hello"); ] + Highlight[range=(142,169), substring=Hello, regex=null, region=null, type=HIGHLIGHTED] + PlainText[range=(170,202), content= System.out.println("Hello");] + Highlight[range=(211,228), substring=null, regex=null, region=, type=HIGHLIGHTED] + PlainText[range=(229,262), content= System.out.println("Hello1");] + Link[range=(301,344), substring=null, regex=H\\w+, region=null, target=Hello, linkType=LINKPLAIN] + PlainText[range=(263,297), content= System.out.println("Hello2"); ] + EndRegion[range=(353,356), region=null] + PlainText[range=(357,360), content= }] + EndRegion[range=(367,370), region=null] + PlainText[range=(371,372), content=}] + PlainText[range=(373,373), content=]"""); + } + + @Test + public void errors() { + testParsing( + """ + public class X { + // @start + void main() { + System.out.println("Hello"); // @replace substring="Hello" substring2='GoodBye' + // @highlight type=underline + System.out.println("Hello"); // @link type=pfff + } + // @end test + } + """, """ + PlainText[range=(0,16), content=public class X {] + ErrorMarkup[range=(23,28), message=@start: missing 'region' attribute] + PlainText[range=(29,44), content= void main() {] + Replace[range=(82,128), substring=Hello, regex=null, region=null, replacement=] + ErrorMarkup[range=(108,128), message=Unsupported attribute: 'substring2'] + ErrorMarkup[range=(82,128), message=@replace: missing 'replacement' attribute] + PlainText[range=(45,78), content= System.out.println("Hello"); ] + Highlight[range=(137,161), substring=null, regex=null, region=null, type=HIGHLIGHTED] + ErrorMarkup[range=(147,161), message=Unknown type 'underline'; only 'bold', 'italic', and 'highlighted' are supported] + Link[range=(199,213), substring=null, regex=null, region=null, target=, linkType=LINK] + ErrorMarkup[range=(199,213), message=@link: missing 'target' attribute] + ErrorMarkup[range=(204,213), message=Unknown type 'pfff'; only 'link' and 'linkplain' are supported] + PlainText[range=(162,195), content= System.out.println("Hello"); ] + PlainText[range=(214,217), content= }] + EndRegion[range=(224,232), region=null] + ErrorMarkup[range=(228,232), message=Unsupported attribute: 'test'] + PlainText[range=(233,234), content=}] + PlainText[range=(235,235), content=]"""); + } + + @Test + public void visitorSimple() { + String text = """ + public class Hello { + // @start region=main + public static void main(String[] args) { + code: // @replace regex='code:' replacement="..." + // @highlight substring="Hello" + System.out.println("Hello"); + // @highlight region substring="idx" + for(int idx=0; idx<10; idx++) { + System.out.println(idx); + } + // @end + } + // @end + } + """; + testParsing(text, """ + PlainText[range=(0,20), content=public class Hello {] + StartRegion[range=(27,44), region=main] + PlainText[range=(45,87), content= public static void main(String[] args) {] + Replace[range=(102,141), substring=null, regex=code:, region=null, replacement=...] + PlainText[range=(88,98), content= code: ] + Highlight[range=(150,177), substring=Hello, regex=null, region=null, type=HIGHLIGHTED] + PlainText[range=(178,210), content= System.out.println("Hello");] + Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED] + PlainText[range=(252,287), content= for(int idx=0; idx<10; idx++) {] + PlainText[range=(288,318), content= System.out.println(idx);] + PlainText[range=(319,324), content= }] + EndRegion[range=(333,336), region=null] + PlainText[range=(337,340), content= }] + EndRegion[range=(347,350), region=null] + PlainText[range=(351,352), content=}] + PlainText[range=(353,353), content=]"""); + testVisitor(text, null, """ + public class Hello { + public static void main(String[] args) { + code: // [Replace[range=(102,141), substring=null, regex=code:, region=null, replacement=...]] + System.out.println("Hello"); // [Highlight[range=(150,177), substring=Hello, regex=null, region=null, type=HIGHLIGHTED]] + for(int idx=0; idx<10; idx++) { // [Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + System.out.println(idx); // [Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + } // [Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + } + } + + """); + testVisitor(text, "main", """ + public static void main(String[] args) { + code: // [Replace[range=(102,141), substring=null, regex=code:, region=null, replacement=...]] + System.out.println("Hello"); // [Highlight[range=(150,177), substring=Hello, regex=null, region=null, type=HIGHLIGHTED]] + for(int idx=0; idx<10; idx++) { // [Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + System.out.println(idx); // [Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + } // [Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + } + """); + } + + @Test + public void visitorNested() { + String text = """ + // @highlight region="r1" substring="xxx" type=bold + r1 start xxx + // @highlight region="r2" substring="yyy" type=italic + r2 start yyy + // @highlight substring="zzz" type=highlighted: + one line with zzz highlighting + r1+r2 xxx + // @end region="r1" + r2 continues yyy + // @end region="r2" + """; + testParsing(text, """ + Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD] + PlainText[range=(52,64), content=r1 start xxx] + Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC] + PlainText[range=(119,131), content=r2 start yyy] + Highlight[range=(136,179), substring=zzz, regex=null, region=null, type=HIGHLIGHTED] + PlainText[range=(180,210), content=one line with zzz highlighting] + PlainText[range=(211,220), content=r1+r2 xxx] + EndRegion[range=(225,240), region=r1] + PlainText[range=(241,257), content=r2 continues yyy] + EndRegion[range=(262,277), region=r2] + PlainText[range=(278,278), content=]"""); + testVisitor(text, null, """ + r1 start xxx // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD]] + r2 start yyy // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] + one line with zzz highlighting // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC], Highlight[range=(136,179), substring=zzz, regex=null, region=null, type=HIGHLIGHTED]] + r1+r2 xxx // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] + r2 continues yyy // [Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] + + """); + testVisitor(text, "r1", """ + r1 start xxx // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD]] + r2 start yyy // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] + one line with zzz highlighting // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC], Highlight[range=(136,179), substring=zzz, regex=null, region=null, type=HIGHLIGHTED]] + r1+r2 xxx // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] + """); + testVisitor(text, "r2", """ + r2 start yyy // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] + one line with zzz highlighting // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC], Highlight[range=(136,179), substring=zzz, regex=null, region=null, type=HIGHLIGHTED]] + r1+r2 xxx // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] + r2 continues yyy // [Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] + """); + } + + private static void testParsing(@NotNull String input, @NotNull String expected) { + assertEquals(expected, SnippetMarkup.parse(input).toString()); + } + + private static void testVisitor(@NotNull String input, @Nullable String region, @NotNull String expected) { + var visitor = new SnippetMarkup.SnippetVisitor() { + StringBuilder sb = new StringBuilder(); + + @Override + public void visitPlainText(@NotNull PlainText plainText, + @NotNull List<@NotNull LocationMarkupNode> activeNodes) { + sb.append(plainText.content()); + if (!activeNodes.isEmpty()) { + sb.append(" // ").append(activeNodes); + } + sb.append("\n"); + } + + @Override + public void visitError(@NotNull ErrorMarkup errorMarkup) { + sb.append("Error: ").append(errorMarkup.message()).append("\n"); + } + }; + + SnippetMarkup.parse(input).visitSnippet(region, visitor); + assertEquals(expected, visitor.sb.toString()); + } +} diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties index 14dbc134c234..1014b6b855e5 100644 --- a/java/openapi/resources/messages/JavaBundle.properties +++ b/java/openapi/resources/messages/JavaBundle.properties @@ -1819,3 +1819,9 @@ fix.move.to.source.root=Move to source root warning.java.file.outside.source.root=Java file is located outside of the module source root, so it won't be compiled inspection.message.snippet.file.not.found=Snippet file is not found javadoc.snippet.not.found=Snippet not found: +javadoc.snippet.error.unknown.highlight.type=Unknown type ''{0}''; only ''bold'', ''italic'', and ''highlighted'' are supported +javadoc.snippet.error.unknown.link.type=Unknown type ''{0}''; only ''link'' and ''linkplain'' are supported +javadoc.snippet.error.malformed.attribute=Malformed attribute +javadoc.snippet.error.unsupported.attribute=Unsupported attribute: ''{0}'' +javadoc.snippet.error.duplicate.attribute=Duplicate attribute: ''{0}'' +javadoc.snippet.error.missing.required.attribute={0}: missing ''{1}'' attribute