/**
   * Visits a {@link PrintNode}, wrapping it with an HtmlPrintNode node if it occurs in {@link
   * HtmlState#TAG} or {@link HtmlState#PCDATA}. This allows the code generator to handle those
   * print statements separately and know the state in which they occurred. If the {@link PrintNode}
   * occurs in {@link HtmlState#ATTR_VALUE}, then the print node becomes a part of the current
   * attribute's value.
   */
  @Override
  protected void visitPrintNode(PrintNode node) {
    checkForValidSoyNodeLocation(node);

    HtmlPrintNode htmlPrintNode;
    switch (getState()) {
      case ATTR_VALUE:
        // A PrintNode in an attribute value, add it to the current attribute values, which will get
        // added to the attribute node once the attribute value ends.
        currentAttributeValues.add(node);
        node.getParent().removeChild(node);
        break;
      case TAG:
        // A PrintNode in the tag context - this is a print of something that has kind="attributes",
        // keep track of the context using an HtmlPrintNode so that the code generator knows what.
        // to do with it.
        htmlPrintNode =
            new HtmlPrintNode(
                idGen.genId(), node, HtmlPrintNode.Context.HTML_TAG, node.getSourceLocation());
        node.getParent().replaceChild(node, htmlPrintNode);
        break;
      case PCDATA:
        // A PrintNode in the pcdata context - this is a print of something that is the child of
        // an HTML element. This could be html, text, css, etc., just need to keep track of the
        // the context and the code generator will do the right thing with each type.
        htmlPrintNode =
            new HtmlPrintNode(
                idGen.genId(), node, HtmlPrintNode.Context.HTML_PCDATA, node.getSourceLocation());
        node.getParent().replaceChild(node, htmlPrintNode);
        break;
      default:
        break;
    }
  }
  /**
   * Handles a character within {@link HtmlState#TAG_NAME}, where the name of a tag must be present.
   *
   * @param node The node that the current character belongs to.
   * @param c The current character being examined.
   */
  private void handleHtmlTagName(RawTextNode node, char c) {
    if (CharMatcher.WHITESPACE.matches(c) || c == '>') {
      currentTag = consumeText(false);

      // No tag name, saw something like <> or <  >.
      if (currentTag.length() <= 0) {
        SourceLocation sl = deriveSourceLocation(node);
        errorReporter.report(sl, MISSING_TAG_NAME);
      }

      // Currently, closing tags and open tags are handled through the states. If this is not a
      // closing tag, then an open tag needs to be started.
      if (!currentTag.startsWith("/")) {
        SourceLocation sl = deriveSourceLocation(node);
        transformMapping.put(node, new HtmlOpenTagStartNode(idGen.genId(), currentTag, sl));
      }

      if (c == '>') {
        // Handle close tags and tags that only have a tag name (e.g. <div>).
        handleHtmlTag(node, c);
      } else {
        // Get ready to capture attributes.
        currentAttributeValues = new ArrayList<>();
        setState(HtmlState.TAG);
      }
    } else {
      currentText.append(c);
    }
  }
  /**
   * Creates a new {@link HtmlAttributeNode} and maps it to node, taking all the attribute values
   * (text, conditionals, print statements) and adding them to the new attribute node.
   *
   * @param node The node that the mapped node comes from.
   */
  private void createAttribute(RawTextNode node) {
    SourceLocation sl = deriveSourceLocation(node);
    HtmlAttributeNode htmlAttributeNode =
        new HtmlAttributeNode(idGen.genId(), currentAttributeName, sl);
    htmlAttributeNode.addChildren(currentAttributeValues);
    transformMapping.put(node, htmlAttributeNode);

    currentAttributeValues = new ArrayList<>();
  }
  /**
   * Creates a new {@link HtmlTextNode} and maps it to node.
   *
   * @param node The node that the mapped node comes from.
   */
  private void createTextNode(RawTextNode node) {
    // Consume text, removing unnecessary whitespace
    String currentString = consumeText(true);

    if (currentString.length() > 0) {
      SourceLocation sl = deriveSourceLocation(node);
      transformMapping.put(node, new HtmlTextNode(idGen.genId(), currentString, sl));
    }
  }
  /**
   * Creates a {@link RawTextNode} for the current part of an attribute value and adds it to the
   * pending attribute value array.
   *
   * @param node The node that the mapped node comes from.
   */
  private void createAttributeValueNode(RawTextNode node) {
    String currentString = consumeText(false);

    // Check to see if the currentText is empty. This may occur when we have something like
    // disabled="" or disabled="{$foo}" after the print tag is finished.
    if (currentString.length() > 0) {
      SourceLocation sl = deriveSourceLocation(node);
      currentAttributeValues.add(new RawTextNode(idGen.genId(), currentString, sl));
    }
  }
  /**
   * Handles a character within {@link HtmlState#TAG}, where either an attribute declaration or the
   * end of a tag may appear.
   *
   * @param node The node that the current character belongs to.
   * @param c The current character being examined.
   */
  private void handleHtmlTag(RawTextNode node, char c) {
    if (c == '>') {
      // Found the end of the tag - create the appropriate open tag or close tag node, depending
      // on which we are ending.
      SourceLocation sl = deriveSourceLocation(node);

      if (currentTag.startsWith("/")) {
        transformMapping.put(
            node, new HtmlCloseTagNode(idGen.genId(), currentTag.substring(1), sl));
      } else {
        transformMapping.put(node, new HtmlOpenTagEndNode(idGen.genId(), currentTag, sl));
      }

      setState(HtmlState.PCDATA);
    } else if (CharMatcher.WHITESPACE.matches(c)) {
      // Skip whitespace characters.
    } else {
      setState(HtmlState.ATTRIBUTE_NAME);
      currentText.append(c);
    }
  }