private void html() {
    if (!finalPass) return;

    LOG.debug("parse: html");

    String text = scan.getToken();
    if (text.length() == 0) {
      return;
    }

    // If we detect it is all whitespace, we need to keep it for later
    // If it is not whitespace, we need to flush any whitespace we do have
    boolean contentIsWhitespace = !Pattern.compile("\\S").matcher(text).find();
    if (!contentIsWhitespace && currentlyBufferingWhitespace) {
      flushBufferedWhiteSpace();
    } else {
      currentlyBufferingWhitespace = contentIsWhitespace;
    }
    // We need to know if the last content output was not whitespace, for tag safety checks
    previousContentWasNonWhitespace = !contentIsWhitespace;

    if (currentlyBufferingWhitespace) {
      whitespaceBuffer.append(text);
    } else {
      appendHtmlPart(text);
    }
  }
  private void page() {

    LOG.debug("parse: page");

    if (finalPass) {
      out.println();
      if (pluginAnnotation != null) {
        out.println(pluginAnnotation);
      }
      out.print("class ");
      out.print(className);
      out.println(" extends GroovyPage {");

      out.println(
          "public String getGroovyPageFileName() { \"" + pageName.replaceAll("\\\\", "/") + "\" }");
      out.println("public Object run() {");
      /*
      out.println("def params = binding.params");
      out.println("def request = binding.request");
      out.println("def flash = binding.flash");
      out.println("def response = binding.response");
      */
      out.println("Writer " + GroovyPage.OUT + " = getOut()");
      out.println("Writer " + GroovyPage.EXPRESSION_OUT + " = getExpressionOut()");
      // out.println("JspTagLib jspTag");
      if (sitemeshPreprocessMode) {
        out.println("registerSitemeshPreprocessMode()");
      }
    }

    loop:
    for (; ; ) {
      if (doNextScan) {
        state = scan.nextToken();
      } else {
        doNextScan = true;
      }

      // Flush any buffered whitespace if there's not a possibility of more whitespace
      // or a new tag which will handle flushing as necessary
      if ((state != GSTART_TAG) && (state != HTML)) {
        flushBufferedWhiteSpace();
        previousContentWasNonWhitespace = false; // well, we don't know
      }

      switch (state) {
        case EOF:
          break loop;
        case HTML:
          html();
          break;
        case JEXPR:
          scriptletExpr();
          break;
        case JSCRIPT:
          script(false);
          break;
        case JDIRECT:
          direct();
          break;
        case JDECLAR:
          declare(false);
          break;
        case GEXPR:
          expr();
          break;
        case GSCRIPT:
          script(true);
          break;
        case GDIRECT:
          direct();
          break;
        case GDECLAR:
          declare(true);
          break;
        case GSTART_TAG:
          startTag();
          break;
        case GEND_EMPTY_TAG:
        case GEND_TAG:
          endTag();
          break;
      }
    }

    if (finalPass) {
      if (!tagMetaStack.isEmpty()) {
        throw new GrailsTagException(
            "Grails tags were not closed! [" + tagMetaStack + "] in GSP " + pageName + "",
            pageName,
            getCurrentOutputLineNumber());
      }

      out.println("}");

      out.println("public static final Map " + CONSTANT_NAME_JSP_TAGS + " = new HashMap()");
      if (jspTags != null && jspTags.size() > 0) {
        out.println("static {");
        for (Map.Entry<String, String> entry : jspTags.entrySet()) {
          out.print("\t" + CONSTANT_NAME_JSP_TAGS + ".put('");
          out.print(escapeGroovy(entry.getKey()));
          out.print("','");
          out.print(escapeGroovy(entry.getValue()));
          out.println("')");
        }
        out.println("}");
      }

      out.println("protected void init() {");
      out.println("\tthis.jspTags = " + CONSTANT_NAME_JSP_TAGS);
      out.println("}");

      out.println(
          "public static final String "
              + CONSTANT_NAME_CONTENT_TYPE
              + " = '"
              + escapeGroovy(contentType)
              + "'");

      out.println(
          "public static final long " + CONSTANT_NAME_LAST_MODIFIED + " = " + lastModified + "L");

      out.println(
          "public static final String "
              + CONSTANT_NAME_EXPRESSION_CODEC
              + " = '"
              + escapeGroovy(expressionCodecDirectiveValue)
              + "'");
      out.println(
          "public static final String "
              + CONSTANT_NAME_STATIC_CODEC
              + " = '"
              + escapeGroovy(staticCodecDirectiveValue)
              + "'");
      out.println(
          "public static final String "
              + CONSTANT_NAME_OUT_CODEC
              + " = '"
              + escapeGroovy(outCodecDirectiveValue)
              + "'");
      out.println(
          "public static final String "
              + CONSTANT_NAME_TAGLIB_CODEC
              + " = '"
              + escapeGroovy(taglibCodecDirectiveValue)
              + "'");

      out.println("}");

      if (shouldAddLineNumbers()) {
        addLineNumbers();
      }
    } else {
      for (int i = 0; i < DEFAULT_IMPORTS.length; i++) {
        out.print("import ");
        out.println(DEFAULT_IMPORTS[i]);
      }
    }
  }
  @SuppressWarnings({"unchecked", "rawtypes"})
  private void startTag() {
    if (!finalPass) return;

    tagIndex++;

    String text;
    StringBuilder buf = new StringBuilder(scan.getToken());
    String ns = scan.getNamespace();

    boolean emptyTag = false;

    state = scan.nextToken();
    while (state != HTML && state != GEND_TAG && state != GEND_EMPTY_TAG && state != EOF) {
      if (state == GTAG_EXPR) {
        buf.append("${");
        buf.append(scan.getToken().trim());
        buf.append("}");
      } else {
        buf.append(scan.getToken());
      }
      state = scan.nextToken();
    }
    if (state == GEND_EMPTY_TAG) {
      emptyTag = true;
    }

    doNextScan = false;

    text = buf.toString();

    String tagName;
    Map attrs = new LinkedHashMap();

    Matcher m = Pattern.compile("\\s").matcher(text);

    if (m.find()) { // ignores carriage returns and new lines
      tagName = text.substring(0, m.start());
      if (state != EOF) {
        String attrTokens = text.substring(m.start(), text.length());
        populateMapWithAttributes(attrs, attrTokens);
      }
    } else {
      tagName = text;
    }

    if (state == EOF) {
      throw new GrailsTagException(
          "Unexpected end of file encountered parsing Tag ["
              + tagName
              + "] for "
              + className
              + ". Are you missing a closing brace '}'?",
          pageName,
          getCurrentOutputLineNumber());
    }

    flushTagBuffering();

    TagMeta tm = new TagMeta();
    tm.name = tagName;
    tm.namespace = ns;
    tm.hasAttributes = !attrs.isEmpty();
    tm.lineNumber = getCurrentOutputLineNumber();
    tm.emptyTag = emptyTag;
    tm.tagIndex = tagIndex;
    tagMetaStack.push(tm);

    if (GroovyPage.DEFAULT_NAMESPACE.equals(ns) && tagRegistry.isSyntaxTag(tagName)) {
      if (tagContext == null) {
        tagContext = new HashMap<Object, Object>();
        tagContext.put(GroovyPage.OUT, out);
        tagContext.put(GroovyPageParser.class, this);
      }
      GroovySyntaxTag tag = (GroovySyntaxTag) tagRegistry.newTag(tagName);
      tag.init(tagContext);
      tag.setAttributes(attrs);

      if (tag.isKeepPrecedingWhiteSpace() && currentlyBufferingWhitespace) {
        flushBufferedWhiteSpace();
      } else if (!tag.isAllowPrecedingContent() && previousContentWasNonWhitespace) {
        throw new GrailsTagException(
            "Tag ["
                + tag.getName()
                + "] cannot have non-whitespace characters directly preceding it.",
            pageName,
            getCurrentOutputLineNumber());
      } else {
        // If tag does not specify buffering of WS, we swallow it here
        clearBufferedWhiteSpace();
      }

      tag.doStartTag();

      tm.instance = tag;
    } else {
      // Custom taglibs have to always flush the whitespace, there's no
      // "allowPrecedingWhitespace" property on tags yet
      flushBufferedWhiteSpace();

      if (attrs.size() > 0) {
        FastStringWriter buffer = new FastStringWriter();
        buffer.print("[");
        for (Iterator<?> i = attrs.keySet().iterator(); i.hasNext(); ) {
          String name = (String) i.next();
          String cleanedName = name;
          if (name.startsWith("\"") && name.endsWith("\"")) {
            cleanedName = "'" + name.substring(1, name.length() - 1) + "'";
          }
          buffer.print(cleanedName);
          buffer.print(':');

          buffer.print(getExpressionText(attrs.get(name).toString()));
          if (i.hasNext()) {
            buffer.print(',');
          } else {
            buffer.print("]");
          }
        }
        attrsVarsMapDefinition.put(tagIndex, buffer.toString());
        buffer.close();
      }

      if (!emptyTag) {
        tm.bufferMode = true;
      }
    }
  }