private void addTextToken(int end) {
    // No active block to add to.

    if (_blockStart < 0) return;

    if (_blockStart <= end) {
      TemplateToken token =
          _factory.createTextToken(_templateData, _blockStart, end, _templateLocation);

      _tokens.add(token);
    }

    _blockStart = -1;
  }
  private void addOpenToken(String tagName, String jwcId, String type, ILocation location) {
    OpenToken token = _factory.createOpenToken(tagName, jwcId, type, location);
    _tokens.add(token);

    if (_attributes.isEmpty()) return;

    Iterator i = _attributes.entrySet().iterator();
    while (i.hasNext()) {
      Map.Entry entry = (Map.Entry) i.next();

      String key = (String) entry.getKey();

      if (key.equalsIgnoreCase(JWCID_ATTRIBUTE_NAME)) continue;

      String value = (String) entry.getValue();

      addAttributeToToken(token, key, value);
    }
  }
  private void processComponentStart(
      String tagName,
      String jwcId,
      boolean emptyTag,
      int startLine,
      int cursorStart,
      ILocation startLocation)
      throws TemplateParseException {
    if (jwcId.equalsIgnoreCase(CONTENT_ID)) {
      processContentTag(tagName, startLine, cursorStart, emptyTag);

      return;
    }

    boolean isRemoveId = jwcId.equalsIgnoreCase(REMOVE_ID);

    if (_ignoring && !isRemoveId)
      templateParseProblem(
          Tapestry.format(
              "TemplateParser.component-may-not-be-ignored", tagName, Integer.toString(startLine)),
          startLocation,
          startLine,
          cursorStart);

    String type = null;
    boolean allowBody = false;

    if (_patternMatcher.matches(jwcId, _implicitIdPattern)) {
      MatchResult match = _patternMatcher.getMatch();

      jwcId = match.group(IMPLICIT_ID_PATTERN_ID_GROUP);
      type = match.group(IMPLICIT_ID_PATTERN_TYPE_GROUP);

      String libraryId = match.group(IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP);
      String simpleType = match.group(IMPLICIT_ID_PATTERN_SIMPLE_TYPE_GROUP);

      // If (and this is typical) no actual component id was specified,
      // then generate one on the fly.
      // The allocated id for anonymous components is
      // based on the simple (unprefixed) type, but starts
      // with a leading dollar sign to ensure no conflicts
      // with user defined component ids (which don't allow dollar signs
      // in the id).

      if (jwcId == null) jwcId = _idAllocator.allocateId("$" + simpleType);

      try {
        allowBody = _delegate.getAllowBody(libraryId, simpleType, startLocation);
      } catch (ApplicationRuntimeException e) {
        // give subclasses a chance to handle and rethrow
        templateParseProblem(e, startLine, cursorStart);
      }

    } else {
      if (!isRemoveId) {
        if (!_patternMatcher.matches(jwcId, _simpleIdPattern))
          templateParseProblem(
              Tapestry.format(
                  "TemplateParser.component-id-invalid",
                  tagName,
                  Integer.toString(startLine),
                  jwcId),
              startLocation,
              startLine,
              cursorStart);

        if (!_delegate.getKnownComponent(jwcId))
          templateParseProblem(
              Tapestry.format(
                  "TemplateParser.unknown-component-id",
                  tagName,
                  Integer.toString(startLine),
                  jwcId),
              startLocation,
              startLine,
              cursorStart);

        try {
          allowBody = _delegate.getAllowBody(jwcId, startLocation);
        } catch (ApplicationRuntimeException e) {
          // give subclasses a chance to handle and rethrow
          templateParseProblem(e, startLine, cursorStart);
        }
      }
    }

    // Ignore the body if we're removing the entire tag,
    // of if the corresponding component doesn't allow
    // a body.

    boolean ignoreBody = !emptyTag && (isRemoveId || !allowBody);

    if (_ignoring && ignoreBody)
      templateParseProblem(
          Tapestry.format("TemplateParser.nested-ignore", tagName, Integer.toString(startLine)),
          new Location(_resourceLocation, startLine),
          startLine,
          cursorStart);

    if (!emptyTag) pushNewTag(tagName, startLine, isRemoveId, ignoreBody);

    // End any open block.

    addTextToken(cursorStart - 1);

    if (!isRemoveId) {
      addOpenToken(tagName, jwcId, type, startLocation);

      if (emptyTag) _tokens.add(_factory.createCloseToken(tagName, getCurrentLocation()));
    }

    advance();
  }
  private void startTag() throws TemplateParseException {
    int cursorStart = _cursor;
    int length = _templateData.length;
    String tagName = null;
    boolean endOfTag = false;
    boolean emptyTag = false;
    int startLine = _line;
    ILocation startLocation = new Location(_resourceLocation, startLine);

    tagBeginEvent(startLine, _cursor);

    advance();

    // Collect the element type

    while (_cursor < length) {
      char ch = _templateData[_cursor];

      if (ch == '/' || ch == '>' || Character.isWhitespace(ch)) {
        tagName = new String(_templateData, cursorStart + 1, _cursor - cursorStart - 1);

        break;
      }

      advance();
    }

    String attributeName = null;
    int attributeNameStart = -1;
    int attributeValueStart = -1;
    int state = WAIT_FOR_ATTRIBUTE_NAME;
    char quoteChar = 0;

    _attributes.clear();

    // Collect each attribute

    while (!endOfTag) {
      if (_cursor >= length) {
        String key =
            (tagName == null)
                ? "TemplateParser.unclosed-unknown-tag"
                : "TemplateParser.unclosed-tag";

        templateParseProblem(
            Tapestry.format(key, tagName, Integer.toString(startLine)),
            startLocation,
            startLine,
            cursorStart);
      }

      char ch = _templateData[_cursor];

      switch (state) {
        case WAIT_FOR_ATTRIBUTE_NAME:

          // Ignore whitespace before the next attribute name, while
          // looking for the end of the current tag.

          if (ch == '/') {
            emptyTag = true;
            advance();
            break;
          }

          if (ch == '>') {
            endOfTag = true;
            break;
          }

          if (Character.isWhitespace(ch)) {
            advance();
            break;
          }

          // Found non-whitespace, assume its the attribute name.
          // Note: could use a check here for non-alpha.

          attributeNameStart = _cursor;
          state = COLLECT_ATTRIBUTE_NAME;
          advance();
          break;

        case COLLECT_ATTRIBUTE_NAME:

          // Looking for end of attribute name.

          if (ch == '=' || ch == '/' || ch == '>' || Character.isWhitespace(ch)) {
            attributeName =
                new String(_templateData, attributeNameStart, _cursor - attributeNameStart);

            state = ADVANCE_PAST_EQUALS;
            break;
          }

          // Part of the attribute name

          advance();
          break;

        case ADVANCE_PAST_EQUALS:

          // Looking for the '=' sign.  May hit the end of the tag, or (for bare attributes),
          // the next attribute name.

          if (ch == '/' || ch == '>') {
            // A bare attribute, which is not interesting to
            // us.

            state = WAIT_FOR_ATTRIBUTE_NAME;
            break;
          }

          if (Character.isWhitespace(ch)) {
            advance();
            break;
          }

          if (ch == '=') {
            state = WAIT_FOR_ATTRIBUTE_VALUE;
            quoteChar = 0;
            attributeValueStart = -1;
            advance();
            break;
          }

          // Otherwise, an HTML style "bare" attribute (such as <select multiple>).
          // We aren't interested in those (we're just looking for the id or jwcid attribute).

          state = WAIT_FOR_ATTRIBUTE_NAME;
          break;

        case WAIT_FOR_ATTRIBUTE_VALUE:
          if (ch == '/' || ch == '>')
            templateParseProblem(
                Tapestry.format(
                    "TemplateParser.missing-attribute-value",
                    tagName,
                    Integer.toString(_line),
                    attributeName),
                getCurrentLocation(),
                _line,
                _cursor);

          // Ignore whitespace between '=' and the attribute value.  Also, look
          // for initial quote.

          if (Character.isWhitespace(ch)) {
            advance();
            break;
          }

          if (ch == '\'' || ch == '"') {
            quoteChar = ch;

            state = COLLECT_QUOTED_VALUE;
            advance();
            attributeValueStart = _cursor;
            attributeBeginEvent(attributeName, _line, attributeValueStart);
            break;
          }

          // Not whitespace or quote, must be start of unquoted attribute.

          state = COLLECT_UNQUOTED_VALUE;
          attributeValueStart = _cursor;
          attributeBeginEvent(attributeName, _line, attributeValueStart);
          break;

        case COLLECT_QUOTED_VALUE:

          // Start collecting the quoted attribute value.  Stop at the matching quote character,
          // unless bare, in which case, stop at the next whitespace.

          if (ch == quoteChar) {
            String attributeValue =
                new String(_templateData, attributeValueStart, _cursor - attributeValueStart);

            attributeEndEvent(_cursor);

            if (_attributes.containsKey(attributeName))
              templateParseProblem(
                  Tapestry.format(
                      "TemplateParser.duplicate-tag-attribute",
                      tagName,
                      Integer.toString(_line),
                      attributeName),
                  getCurrentLocation(),
                  _line,
                  _cursor);

            _attributes.put(attributeName, attributeValue);

            // Advance over the quote.
            advance();
            state = WAIT_FOR_ATTRIBUTE_NAME;
            break;
          }

          advance();
          break;

        case COLLECT_UNQUOTED_VALUE:

          // An unquoted attribute value ends with whitespace
          // or the end of the enclosing tag.

          if (ch == '/' || ch == '>' || Character.isWhitespace(ch)) {
            String attributeValue =
                new String(_templateData, attributeValueStart, _cursor - attributeValueStart);

            attributeEndEvent(_cursor);

            if (_attributes.containsKey(attributeName))
              templateParseProblem(
                  Tapestry.format(
                      "TemplateParser.duplicate-tag-attribute",
                      tagName,
                      Integer.toString(_line),
                      attributeName),
                  getCurrentLocation(),
                  _line,
                  _cursor);

            _attributes.put(attributeName, attributeValue);

            state = WAIT_FOR_ATTRIBUTE_NAME;
            break;
          }

          advance();
          break;
      }
    }

    tagEndEvent(_cursor);

    // Check for invisible localizations

    String localizationKey = findValueCaselessly(LOCALIZATION_KEY_ATTRIBUTE_NAME, _attributes);
    String jwcId = findValueCaselessly(JWCID_ATTRIBUTE_NAME, _attributes);

    if (localizationKey != null && tagName.equalsIgnoreCase("span") && jwcId == null) {
      if (_ignoring)
        templateParseProblem(
            Tapestry.format(
                "TemplateParser.component-may-not-be-ignored",
                tagName,
                Integer.toString(startLine)),
            startLocation,
            startLine,
            cursorStart);

      // If the tag isn't empty, then create a Tag instance to ignore the
      // body of the tag.

      if (!emptyTag) {
        Tag tag = new Tag(tagName, startLine);

        tag._component = false;
        tag._removeTag = true;
        tag._ignoringBody = true;
        tag._mustBalance = true;

        _stack.add(tag);

        // Start ignoring content until the close tag.

        _ignoring = true;
      } else {
        // Cursor is at the closing carat, advance over it and any whitespace.
        advance();
        advanceOverWhitespace();
      }

      // End any open block.

      addTextToken(cursorStart - 1);

      boolean raw = checkBoolean(RAW_ATTRIBUTE_NAME, _attributes);

      Map attributes =
          filter(_attributes, new String[] {LOCALIZATION_KEY_ATTRIBUTE_NAME, RAW_ATTRIBUTE_NAME});

      TemplateToken token =
          _factory.createLocalizationToken(
              tagName, localizationKey, raw, attributes, startLocation);

      _tokens.add(token);

      return;
    }

    if (jwcId != null) {
      processComponentStart(tagName, jwcId, emptyTag, startLine, cursorStart, startLocation);
      return;
    }

    // A static tag (not a tag without a jwcid attribute).
    // We need to record this so that we can match close tags later.

    if (!emptyTag) {
      Tag tag = new Tag(tagName, startLine);
      _stack.add(tag);
    }

    // If there wasn't an active block, then start one.

    if (_blockStart < 0 && !_ignoring) _blockStart = cursorStart;

    advance();
  }
  /**
   * Invoked to handle a closing tag, i.e., &lt;/foo&gt;. When a tag closes, it will match against a
   * tag on the open tag start. Preferably the top tag on the stack (if everything is well
   * balanced), but this is HTML, not XML, so many tags won't balance.
   *
   * <p>Once the matching tag is located, the question is ... is the tag dynamic or static? If
   * static, then the current text block is extended to include this close tag. If dynamic, then the
   * current text block is ended (before the '&lt;' that starts the tag) and a close token is added.
   *
   * <p>In either case, the matching static element and anything above it is removed, and the cursor
   * is left on the character following the '&gt;'.
   */
  private void closeTag() throws TemplateParseException {
    int cursorStart = _cursor;
    int length = _templateData.length;
    int startLine = _line;

    ILocation startLocation = getCurrentLocation();

    _cursor += CLOSE_TAG.length;

    int tagStart = _cursor;

    while (true) {
      if (_cursor >= length)
        templateParseProblem(
            Tapestry.format("TemplateParser.incomplete-close-tag", Integer.toString(startLine)),
            startLocation,
            startLine,
            cursorStart);

      char ch = _templateData[_cursor];

      if (ch == '>') break;

      advance();
    }

    String tagName = new String(_templateData, tagStart, _cursor - tagStart);

    int stackPos = _stack.size() - 1;
    Tag tag = null;

    while (stackPos >= 0) {
      tag = (Tag) _stack.get(stackPos);

      if (tag.match(tagName)) break;

      if (tag._mustBalance)
        templateParseProblem(
            Tapestry.format(
                "TemplateParser.improperly-nested-close-tag",
                new Object[] {
                  tagName, Integer.toString(startLine), tag._tagName, Integer.toString(tag._line)
                }),
            startLocation,
            startLine,
            cursorStart);

      stackPos--;
    }

    // test begin add is page condiction
    if (stackPos < 0) {
      if (_delegate.getComponent().getSpecification().isPageSpecification()) {

        templateParseProblem(
            Tapestry.format(
                "TemplateParser.unmatched-close-tag", tagName, Integer.toString(startLine)),
            startLocation,
            startLine,
            cursorStart);
      }
    }
    // test end

    // Special case for the content tag

    // test begin
    if (tag != null) {
      if (tag._content) {
        addTextToken(cursorStart - 1);

        // Advance the cursor right to the end.

        _cursor = length;
        _stack.clear();
        return;
      }

      // When a component closes, add a CLOSE tag.
      if (tag._component) {
        addTextToken(cursorStart - 1);

        _tokens.add(_factory.createCloseToken(tagName, getCurrentLocation()));
      } else {
        // The close of a static tag.  Unless removing the tag
        // entirely, make sure the block tag is part of a text block.

        if (_blockStart < 0 && !tag._removeTag && !_ignoring) _blockStart = cursorStart;
      }

      // Remove all elements at stackPos or above.

      // test begin add && i >= 0
      for (int i = _stack.size() - 1; i >= stackPos && i >= 0; i--) _stack.remove(i);
      // test end

      // Advance cursor past '>'

      advance();

      // If editting out the tag (i.e., $remove$) then kill any whitespace.
      // For components that simply don't contain a body, removeTag will
      // be false.

      if (tag._removeTag) advanceOverWhitespace();

      // If we were ignoring the body of the tag, then clear the ignoring
      // flag, since we're out of the body.

      if (tag._ignoringBody) _ignoring = false;
    }
    // test end
  }