docs: private static void labelClassNodeHTML(final ClassInfo ci, final String shape, final String boxBgColor,
              final boolean showFields, final boolean showMethods, final boolean useSimpleNames,
              final ScanSpec scanSpec, final StringBuilder buf) {
          buf.append("[shape=").append(shape).append(",style=filled,fillcolor=\"#").append(boxBgColor)
                  .append("\",label=");
          buf.append('<');
          buf.append("<table border='0' cellborder='0' cellspacing='1'>");
  
          // Class modifiers
          buf.append("<tr><td><font point-size='12'>").append(ci.getModifiersStr()).append(' ')
                  .append(ci.isEnum() ? "enum"
                          : ci.isAnnotation() ? "@interface" : ci.isInterface() ? "interface" : "class")
                  .append("</font></td></tr>");
  
          if (ci.getName().contains(".")) {
              buf.append("<tr><td><font point-size='14'><b>");
              htmlEncode(ci.getPackageName() + ".", buf);
              buf.append("</b></font></td></tr>");
          }
  
          // Class name
          buf.append("<tr><td><font point-size='20'><b>");
          htmlEncode(ci.getSimpleName(), buf);
          buf.append("</b></font></td></tr>");
  
          // Create a color that matches the box background color, but is darker
          final float darkness = 0.8f;
          final int r = (int) (Integer.parseInt(boxBgColor.substring(0, 2), 16) * darkness);
          final int g = (int) (Integer.parseInt(boxBgColor.substring(2, 4), 16) * darkness);
          final int b = (int) (Integer.parseInt(boxBgColor.substring(4, 6), 16) * darkness);
          final String darkerColor = String.format("#%s%s%s%s%s%s", Integer.toString(r >> 4, 16),
                  Integer.toString(r & 0xf, 16), Integer.toString(g >> 4, 16), Integer.toString(g & 0xf, 16),
                  Integer.toString(b >> 4, 16), Integer.toString(b & 0xf, 16));
  
          // Class annotations
          final AnnotationInfoList annotationInfo = ci.annotationInfo;
          if (annotationInfo != null && !annotationInfo.isEmpty()) {
              buf.append("<tr><td colspan='3' bgcolor='").append(darkerColor)
                      .append("'><font point-size='12'><b>ANNOTATIONS</b></font></td></tr>");
              final AnnotationInfoList annotationInfoSorted = new AnnotationInfoList(annotationInfo);
              CollectionUtils.sortIfNotEmpty(annotationInfoSorted);
              for (final AnnotationInfo ai : annotationInfoSorted) {
                  final String annotationName = ai.getName();
                  if (!annotationName.startsWith("java.lang.annotation.")) {
                      buf.append("<tr>");
                      buf.append("<td align='center' valign='top'>");
                      htmlEncode(ai.toString(), buf);
                      buf.append("</td></tr>");
                  }
              }
          }
  
          // Fields
          final FieldInfoList fieldInfo = ci.fieldInfo;
          if (showFields && fieldInfo != null && !fieldInfo.isEmpty()) {
              final FieldInfoList fieldInfoSorted = new FieldInfoList(fieldInfo);
              CollectionUtils.sortIfNotEmpty(fieldInfoSorted);
              for (int i = fieldInfoSorted.size() - 1; i >= 0; --i) {
                  // Remove serialVersionUID field
                  if (fieldInfoSorted.get(i).getName().equals("serialVersionUID")) {
                      fieldInfoSorted.remove(i);
                  }
              }
              if (!fieldInfoSorted.isEmpty()) {
                  buf.append("<tr><td colspan='3' bgcolor='").append(darkerColor)
                          .append("'><font point-size='12'><b>")
                          .append(scanSpec.ignoreFieldVisibility ? "" : "PUBLIC ")
                          .append("FIELDS</b></font></td></tr>");
                  buf.append("<tr><td cellpadding='0'>");
                  buf.append("<table border='0' cellborder='0'>");
                  for (final FieldInfo fi : fieldInfoSorted) {
                      buf.append("<tr>");
                      buf.append("<td align='right' valign='top'>");
  
                      // Field Annotations
                      final AnnotationInfoList fieldAnnotationInfo = fi.annotationInfo;
                      if (fieldAnnotationInfo != null) {
                          for (final AnnotationInfo ai : fieldAnnotationInfo) {
                              if (buf.charAt(buf.length() - 1) != ' ') {
                                  buf.append(' ');
                              }
                              htmlEncode(ai.toString(), buf);
                          }
                      }
  
                      // Field modifiers
                      if (scanSpec.ignoreFieldVisibility) {
                          if (buf.charAt(buf.length() - 1) != ' ') {
                              buf.append(' ');
                          }
                          buf.append(fi.getModifierStr());
                      }
  
                      // Field type
                      if (buf.charAt(buf.length() - 1) != ' ') {
                          buf.append(' ');
                      }
                      final TypeSignature typeSig = fi.getTypeSignatureOrTypeDescriptor();
                      htmlEncode(useSimpleNames ? typeSig.toStringWithSimpleNames() : typeSig.toString(), buf);
                      buf.append("</td>");
  
                      // Field name
                      buf.append("<td align='left' valign='top'><b>");
                      final String fieldName = fi.getName();
                      htmlEncode(fieldName, buf);
                      buf.append("</b></td></tr>");
                  }
                  buf.append("</table>");
                  buf.append("</td></tr>");
              }
          }
  
          // Methods
          final MethodInfoList methodInfo = ci.methodInfo;
          if (showMethods && methodInfo != null) {
              final MethodInfoList methodInfoSorted = new MethodInfoList(methodInfo);
              CollectionUtils.sortIfNotEmpty(methodInfoSorted);
              for (int i = methodInfoSorted.size() - 1; i >= 0; --i) {
                  // Don't list static initializer blocks or methods of Object
                  final MethodInfo mi = methodInfoSorted.get(i);
                  final String name = mi.getName();
                  final int numParam = mi.getParameterInfo().length;
                  if (name.equals("<clinit>") || name.equals("hashCode") && numParam == 0
                          || name.equals("toString") && numParam == 0 || name.equals("equals") && numParam == 1
                                  && mi.getTypeDescriptor().toString().equals("boolean (java.lang.Object)")) {
                      methodInfoSorted.remove(i);
                  }
              }
              if (!methodInfoSorted.isEmpty()) {
                  buf.append("<tr><td cellpadding='0'>");
                  buf.append("<table border='0' cellborder='0'>");
                  buf.append("<tr><td colspan='3' bgcolor='").append(darkerColor)
                          .append("'><font point-size='12'><b>")
                          .append(scanSpec.ignoreMethodVisibility ? "" : "PUBLIC ")
                          .append("METHODS</b></font></td></tr>");
                  for (final MethodInfo mi : methodInfoSorted) {
                      buf.append("<tr>");
  
                      // Method annotations
                      // TODO: wrap this cell if the contents get too long
                      buf.append("<td align='right' valign='top'>");
                      final AnnotationInfoList methodAnnotationInfo = mi.annotationInfo;
                      if (methodAnnotationInfo != null) {
                          for (final AnnotationInfo ai : methodAnnotationInfo) {
                              if (buf.charAt(buf.length() - 1) != ' ') {
                                  buf.append(' ');
                              }
                              htmlEncode(ai.toString(), buf);
                          }
                      }
  
                      // Method modifiers
                      if (scanSpec.ignoreMethodVisibility) {
                          if (buf.charAt(buf.length() - 1) != ' ') {
                              buf.append(' ');
                          }
                          buf.append(mi.getModifiersStr());
                      }
  
                      // Method return type
                      if (buf.charAt(buf.length() - 1) != ' ') {
                          buf.append(' ');
                      }
                      if (!mi.getName().equals("<init>")) {
                          // Don't list return type for constructors
                          final TypeSignature resultTypeSig = mi.getTypeSignatureOrTypeDescriptor().getResultType();
                          htmlEncode(
                                  useSimpleNames ? resultTypeSig.toStringWithSimpleNames() : resultTypeSig.toString(),
                                  buf);
                      } else {
                          buf.append("<b>&lt;constructor&gt;</b>");
                      }
                      buf.append("</td>");
  
                      // Method name
                      buf.append("<td align='left' valign='top'>");
                      buf.append("<b>");
                      if (mi.getName().equals("<init>")) {
                          // Show class name for constructors
                          htmlEncode(ci.getSimpleName(), buf);
                      } else {
                          htmlEncode(mi.getName(), buf);
                      }
                      buf.append("</b>&nbsp;");
                      buf.append("</td>");
  
                      // Method parameters
                      buf.append("<td align='left' valign='top'>");
                      buf.append('(');
                      final MethodParameterInfo[] paramInfo = mi.getParameterInfo();
                      if (paramInfo.length != 0) {
                          for (int i = 0, wrapPos = 0; i < paramInfo.length; i++) {
                              if (i > 0) {
                                  buf.append(", ");
                                  wrapPos += 2;
                              }
                              if (wrapPos > PARAM_WRAP_WIDTH) {
                                  buf.append("</td></tr><tr><td></td><td></td><td align='left' valign='top'>");
                                  wrapPos = 0;
                              }
  
                              // Param annotation
                              final AnnotationInfo[] paramAnnotationInfo = paramInfo[i].annotationInfo;
                              if (paramAnnotationInfo != null) {
                                  for (final AnnotationInfo ai : paramAnnotationInfo) {
                                      final String ais = ai.toString();
                                      if (!ais.isEmpty()) {
                                          if (buf.charAt(buf.length() - 1) != ' ') {
                                              buf.append(' ');
                                          }
                                          htmlEncode(ais, buf);
                                          wrapPos += 1 + ais.length();
                                          if (wrapPos > PARAM_WRAP_WIDTH) {
                                              buf.append("</td></tr><tr><td></td><td></td>"
                                                      + "<td align='left' valign='top'>");
                                              wrapPos = 0;
                                          }
                                      }
                                  }
                              }
  
                              // Param type
                              final TypeSignature paramTypeSig = paramInfo[i].getTypeSignatureOrTypeDescriptor();
                              final String paramTypeStr = useSimpleNames ? paramTypeSig.toStringWithSimpleNames()
                                      : paramTypeSig.toString();
                              htmlEncode(paramTypeStr, buf);
                              wrapPos += paramTypeStr.length();
  
                              // Param name
                              final String paramName = paramInfo[i].getName();
                              if (paramName != null) {
                                  buf.append(" <B>");
                                  htmlEncode(paramName, buf);
                                  wrapPos += 1 + paramName.length();
                                  buf.append("</B>");
                              }
                          }
                      }
                      buf.append(')');
                      buf.append("</td></tr>");
                  }
                  buf.append("</table>");
                  buf.append("</td></tr>");
              }
          }
          buf.append("</table>");
          buf.append(">]");
      }