private static Stream<MarkupNode> parseLine(@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 || (!prev.content().isEmpty() && ContainerUtil.and(markupNodes, mn -> mn instanceof EndRegion))) {
      markupNodes.add(0, prev);
    }
    else {
      markupNodes.add(prev);
    }
    return markupNodes.stream();
  }
private static Stream<MarkupNode> parseLine(@NotNull PlainText text)