Stream<MarkupNode> (@NotNull PlainText text) { String content = text.content(); Matcher matcher = MARKUP_SPEC.matcher(content); if (!matcher.find()) return Stream.of(text); int offset = text.range().getStartOffset(); String markupSpec = matcher.group(1).stripTrailing(); boolean hasColon = markupSpec.endsWith(":"); if (hasColon) { markupSpec = markupSpec.substring(0, markupSpec.length() - 1); } int start = matcher.start(0); PlainText prev = new PlainText(TextRange.create(offset, offset + start), text.content.substring(0, start) + "\n"); start = matcher.start(1); List<MarkupNode> markupNodes = new ArrayList<>(); while (!markupSpec.isEmpty()) { Matcher tagMatcher = MARKUP_TAG.matcher(markupSpec); if (!tagMatcher.find()) { markupNodes.add(new ErrorMarkup(TextRange.create(0, markupSpec.length()).shiftRight(start + offset), JavaBundle.message("javadoc.snippet.error.markup.tag.expected"))); break; } if (tagMatcher.start() != 0) { markupNodes.add(new ErrorMarkup(TextRange.create(0, tagMatcher.start()).shiftRight(start + offset), JavaBundle.message("javadoc.snippet.error.markup.tag.expected"))); break; } String tagName = tagMatcher.group(1); Set<String> allowedAttrs = ALLOWED_ATTRS.get(tagName); Map<String, Attribute> attrs = new HashMap<>(); int attrPos = tagMatcher.end(); int attrEnd = attrPos; while (attrPos < markupSpec.length()) { Matcher attrMatcher = ATTRIBUTE.matcher(markupSpec); boolean found = attrMatcher.find(attrPos); if (!found || attrMatcher.start() != attrPos) break; String key = attrMatcher.group(1); attrEnd = attrMatcher.end(2); if (attrEnd == -1) { attrEnd = attrMatcher.end(1); } TextRange attrRange = TextRange.create(attrMatcher.start(), attrEnd).shiftRight(start + offset); if (!allowedAttrs.contains(key)) { markupNodes.add(new ErrorMarkup(attrRange, JavaBundle.message("javadoc.snippet.error.unsupported.attribute", tagName, key))); } else if (attrs.containsKey(key)) { markupNodes.add(new ErrorMarkup(attrRange, JavaBundle.message("javadoc.snippet.error.duplicate.attribute", tagName, key))); } else { String value = attrMatcher.group(4); if (value == null) value = attrMatcher.group(5); if (value == null) value = attrMatcher.group(6); if (value == null) value = ""; attrs.put(key, new Attribute(attrRange, key, value)); } attrPos = attrMatcher.end(); } while (attrEnd > 0 && Character.isWhitespace(markupSpec.charAt(attrEnd - 1))) { attrEnd--; } TextRange range = TextRange.create(0, attrEnd).shiftRight(start + offset); validateAndAddTag(markupNodes, tagName, attrs, range); start += attrPos; markupSpec = markupSpec.substring(attrPos); } if (prev.content().isBlank() && !prev.content().isEmpty()) { prev = new PlainText(TextRange.from(text.range().getEndOffset(), 0), ""); } if (hasColon) { markupNodes.add(0, prev); } else { markupNodes.add(prev); } return markupNodes.stream(); }