static String htmldecode(final String input) {
  if (input == null) ret null;
  
  final int MIN_ESCAPE = 2;
  final int MAX_ESCAPE = 6;

  StringWriter writer = null;
  int len = input.length();
  int i = 1;
  int st = 0;
  while (true) {
      // look for '&'
      while (i < len && input.charAt(i-1) != '&')
          i++;
      if (i >= len)
          break;

      // found '&', look for ';'
      int j = i;
      while (j < len && j < i + MAX_ESCAPE + 1 && input.charAt(j) != ';')
          j++;
      if (j == len || j < i + MIN_ESCAPE || j == i + MAX_ESCAPE + 1) {
          i++;
          continue;
      }

      // found escape 
      if (input.charAt(i) == '#') {
          // numeric escape
          int k = i + 1;
          int radix = 10;

          final char firstChar = input.charAt(k);
          if (firstChar == 'x' || firstChar == 'X') {
              k++;
              radix = 16;
          }

          try {
              int entityValue = Integer.parseInt(input.substring(k, j), radix);

              if (writer == null) 
                  writer = new StringWriter(input.length());
              writer.append(input.substring(st, i - 1));

              if (entityValue > 0xFFFF) {
                  final char[] chrs = Character.toChars(entityValue);
                  writer.write(chrs[0]);
                  writer.write(chrs[1]);
              } else {
                  writer.write(entityValue);
              }

          } catch (NumberFormatException ex) { 
              i++;
              continue;
          }
      }
      else {
          // named escape
          CharSequence value = htmldecode_lookupMap.get(input.substring(i, j));
          if (value == null) {
              i++;
              continue;
          }

          if (writer == null) 
              writer = new StringWriter(input.length());
          writer.append(input.substring(st, i - 1));

          writer.append(value);
      }

      // skip escape
      st = j + 1;
      i = st;
  }

  if (writer != null) {
      writer.append(input.substring(st, len));
      return writer.toString();
  }
  return input;
}

static simplyCached HashMap<String, CharSequence> htmldecode_lookupMap() {
  var map = new HashMap<String, CharSequence>();
  for (CharSequence[] seq : htmldecode_escapes()) 
    map.put(seq[1].toString(), seq[0]);
  ret map;
}