mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[javadoc] Snippet markup parsing and visiting (IDEA-314507)
GitOrigin-RevId: 61cc6fd201222573e430261d8138bd7b85a74b10
This commit is contained in:
committed by
intellij-monorepo-bot
parent
702d6eedda
commit
2e3c106b5d
@@ -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<String, Set<String>> 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<PlainText> 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");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user