docs: @Override
    protected HtmlToken produce() {
      HtmlToken token = readToken();
      if (token == null) { return null; }
  
      switch (token.type) {
  
        // Keep track of whether we're inside a tag or not.
        case TAGBEGIN:
          state = State.IN_TAG;
          break;
        case TAGEND:
          if (state == State.SAW_EQ && HtmlTokenType.TAGEND == token.type) {
            // Distinguish <input type=checkbox checked=> from
            // <input type=checkbox checked>
            pushbackToken(token);
            state = State.IN_TAG;
            return HtmlToken.instance(
                token.start, token.start, HtmlTokenType.ATTRVALUE);
          }
  
          state = State.OUTSIDE_TAG;
          break;
  
        // Drop ignorable tokens by zeroing out the one received and recursing
        case IGNORABLE:
          return produce();
  
        // collapse adjacent text nodes if we're outside a tag, or otherwise,
        // Recognize attribute names and values.
        default:
          switch (state) {
            case OUTSIDE_TAG:
              if (HtmlTokenType.TEXT == token.type
                  || HtmlTokenType.UNESCAPED == token.type) {
                token = collapseSubsequent(token);
              }
              break;
            case IN_TAG:
              if (HtmlTokenType.TEXT == token.type
                  && !token.tokenInContextMatches(input, "=")) {
                // Reclassify as attribute name
                token = HtmlInputSplitter.reclassify(
                    token, HtmlTokenType.ATTRNAME);
                state = State.SAW_NAME;
              }
              break;
            case SAW_NAME:
              if (HtmlTokenType.TEXT == token.type) {
                if (token.tokenInContextMatches(input, "=")) {
                  state = State.SAW_EQ;
                  // Skip the '=' token
                  return produce();
                } else {
                  // Reclassify as attribute name
                  token = HtmlInputSplitter.reclassify(
                      token, HtmlTokenType.ATTRNAME);
                }
              } else {
                state = State.IN_TAG;
              }
              break;
            case SAW_EQ:
              if (HtmlTokenType.TEXT == token.type
                  || HtmlTokenType.QSTRING == token.type) {
                if (HtmlTokenType.TEXT == token.type) {
                  // Collapse adjacent text nodes to properly handle
                  //   <a onclick=this.clicked=true>
                  //   <a title=foo bar>
                  token = collapseAttributeName(token);
                }
                // Reclassify as value
                token = HtmlInputSplitter.reclassify(
                    token, HtmlTokenType.ATTRVALUE);
                state = State.IN_TAG;
              }
              break;
          }
          break;
      }
  
      return token;
    }