/** Called to push a new lexical scope onto the stack. */
  private boolean lexicalClimb(PageCompilingContext pc, Node node) {
    if (node.attr(ANNOTATION).length() > 1) {

      // Setup a new lexical scope (symbol table changes on each scope encountered).
      if (REPEAT_WIDGET.equalsIgnoreCase(node.attr(ANNOTATION_KEY))
          || CHOOSE_WIDGET.equalsIgnoreCase(node.attr(ANNOTATION_KEY))) {

        String[] keyAndContent = {node.attr(ANNOTATION_KEY), node.attr(ANNOTATION_CONTENT)};
        pc.lexicalScopes.push(new MvelEvaluatorCompiler(parseRepeatScope(pc, keyAndContent, node)));
        return true;
      }

      // Setup a new lexical scope for compiling against embedded pages (closures).
      final PageBook.Page embed = pageBook.forName(node.attr(ANNOTATION_KEY));
      if (null != embed) {
        final Class<?> embedClass = embed.pageClass();
        MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(embedClass);
        checkEmbedAgainst(
            pc, compiler, Parsing.toBindMap(node.attr(ANNOTATION_CONTENT)), embedClass, node);

        pc.lexicalScopes.push(compiler);
        return true;
      }
    }

    return false;
  }
  private void checkFormFields(PageCompilingContext pc, Node element) {
    if (null == pc.form) return;

    String action = pc.form.attr("action");

    // Only look at contextual uris (i.e. hosted by us).
    // TODO - relative, not starting with '/'
    if (null == action || (!action.startsWith("/"))) return;

    final PageBook.Page page = pageBook.get(action);

    // Only look at pages we actually have registered.
    if (null == page) {
      pc.warnings.add(
          CompileError.in(element.outerHtml())
              .near(line(element))
              .causedBy(CompileErrors.UNRESOLVABLE_FORM_ACTION));

      return;
    }

    // If we're inside a form do a throw-away compile against the target page.
    if ("input".equals(element.nodeName()) || "textarea".equals(element.nodeName())) {
      String name = element.attr("name");

      // Skip submits and buttons.
      if (skippable(element.attr("type"))) return;

      // TODO Skip empty?
      if (null == name) {
        pc.warnings.add(
            CompileError.in(element.outerHtml())
                .near(line(element))
                .causedBy(CompileErrors.FORM_MISSING_NAME));

        return;
      }

      // Compile expression path.
      try {
        new MvelEvaluatorCompiler(page.pageClass()).compile(name);

      } catch (ExpressionCompileException e) {
        // TODO Very hacky, needed to strip out xmlns attribution.
        pc.warnings.add(
            CompileError.in(element.outerHtml())
                .near(element.siblingIndex()) // TODO - line number
                .causedBy(CompileErrors.UNRESOLVABLE_FORM_BINDING, e));
      }
    }
  }
  private void checkUriConsistency(PageCompilingContext pc, Node element) {
    String uriAttrib = element.attr("action");
    if (null == uriAttrib) uriAttrib = element.attr("src");
    if (null == uriAttrib) uriAttrib = element.attr("href");

    if (null != uriAttrib) {

      // Verify that such a uri exists in the page book,
      // only if it is contextual--ignore abs & relative URIs.
      if (uriAttrib.startsWith("/"))
        if (null == pageBook.nonCompilingGet(uriAttrib))
          pc.warnings.add(
              CompileError.in(element.outerHtml())
                  .near(element.siblingIndex()) // TODO - line number
                  .causedBy(CompileErrors.UNRESOLVABLE_FORM_ACTION, uriAttrib));
    }
  }