// CS 509, Spring 2004
// Example solution for assignment 1.

#include "check.h"
#include <cassert>
#include <cctype>


class tag {

  public:

    enum tag_type { open, close };

    // Create a tag of the given type with the given text.

       tag(tag_type tt, const std::string & text) 
	 : text(text), open_tag(tt == open) { }

    // Return true iff this is a close tag.

       bool is_close_tag() const {
	 return not open_tag;
	 }

    // Return true iff this is an open tag.

       bool is_open_tag() const {
	 return open_tag;
	 }

    // Return true iff this is a end-of-file tag.

       bool is_end_tag() const {
	 return is_close_tag() and text.empty();
	 }

    // Return true iff this tag is a pair with the given tag.

       bool match(const tag & t) const {
	 return (t.open_tag ^ open_tag) and (t.text == text);
       }

  private:

    std::string text;
    bool open_tag;
  };


// Deja vu, c++ style.

   static tag read_next_tag(std::istream &, std::string &);
   static std::string trim(const std::string &);


static std::string
check_balanced_tags(std::istream & is, const tag & open_tag) {

  // Make sure all the text in the given istream up to closing tag matching the
  // given tag contains balanced tags.  Return an error message if it doesn't;
  // otherwise return the empty string.

  assert(open_tag.is_open_tag());

  std::string emsg;

  while (true) {
    tag t = read_next_tag(is, emsg);
    if (!emsg.empty())
      break;

    if (t.is_open_tag()) {
      emsg = check_balanced_tags(is, t);
      if (!emsg.empty())
	break;
      }
    else {
      if (not open_tag.match(t))
	if (t.is_end_tag())
	  emsg = "missing close tag";
	else
	  emsg = "mismatch between open and close tags";
      break;
      }
    }    

  return emsg;
  }


std::string
check_balanced_tags(std::istream & is) {
  
  // Make sure all the text in the given istream contains balanced tags.
  // Return an error message if it doesn't; otherwise return the empty string.

  std::string emsg;

  while (true) {
    tag t = read_next_tag(is, emsg);
    if (not emsg.empty())
      break;

    if (t.is_end_tag())
      break;

    if (t.is_close_tag()) {
      emsg = "close tag without earlier open tag";
      break;
      }

    emsg = check_balanced_tags(is, t);
    if (not emsg.empty())
      break;
    }

  return emsg;
  }


static tag
check_tag_text(const std::string & text, std::string & emsg) {

  // Check the given text to make sure it's valid tag text and return the
  // associated tag.  If there's an error the return value is undefined and
  // emsg contains an informative error message; if there's no error, emsg is
  // unchanged.

  std::string txt = trim(text);
  if (txt.empty()) {
    emsg = "tag contains no text";
    return tag(tag::close, "");
    }

  const bool close_tag = txt[0] == '/';
  if (close_tag) {
    txt[0] = ' ';
    txt = trim(txt);
    }

  if (txt.empty()) {
    emsg = "tag contains no text";
    return tag(tag::close, "");
    }

  for (std::string::size_type i = 0; i < txt.size(); i++)
    if (not isalnum(txt[i])) {
      emsg = "non-alphanumeric character in tag text";
      break;
      }
    else 
      txt[i] = tolower(txt[i]);

  return tag(close_tag ? tag::close : tag::open, txt);
  }


static tag 
read_next_tag(std::istream & is, std::string & emsg) {

  // Read and return the next tag in the given input stream.  If something goes
  // wrong, store an informative error message in the given string; the return
  // value is undefined; otherwise the given string is unchanged.

  std::string str;

  if (not getline(is, str, '<').good()) {
    if (not is.eof())
      emsg = "stream error during input";
    return tag(tag::close, "");
    }

  if (not getline(is, str, '>').good()) {
    if (is.eof())
      emsg = "unterminated tag";
    else
      emsg = "stream error during input";
    return tag(tag::close, "");
    }
    
  return check_tag_text(str, emsg);
  }


static std::string
trim(const std::string & str) {

  // Return the given string shorn of space characters front and back.

  std::string::size_type 
    b = 0, 
    e = str.size();

  while ((b < e) and isspace(str.at(b)))
    b++;
  
  while ((b < e) and isspace(str.at(e - 1))) 
    e--;

  return str.substr(b, e - b);
  }


#ifdef TESTING

// g++ -g -o testing-check -DTESTING -ansi -pedantic -Wall check.cc && ./testing-check

#include <sstream>

int
main() {
  assert(trim("") == "");
  assert(trim("	 ") == "");
  assert(trim("a") == "a");
  assert(trim("  	a") == "a");
  assert(trim("a  	") == "a");
  assert(trim("  	a  	") == "a");

  std::string emsg;
  std::istringstream iss("< a >");
  {tag t = read_next_tag(iss, emsg);
   assert(emsg.empty() and ("a" == t.text) and (t.is_open_tag)); }
 
  iss.str(" < / bl1h > ");
  {tag t = read_next_tag(iss, emsg);
   assert(emsg.empty() and ("bl1h" == t.text) and (t.is_close_tag())); }

  iss.str("< / blah >");
  {tag t = read_next_tag(iss, emsg);
   assert(emsg.empty() and ("blah" == t.text) and (t.is_close_tag())); }

  iss.str(" / blah >");
  {tag t = read_next_tag(iss, emsg);
   assert(emsg.empty() and ("" == t.text) and (t.is_close_tag())); }

  iss.str(" < / blah ");
  {tag t = read_next_tag(iss, emsg);
   assert(not emsg.empty()); }

  emsg.clear();
  iss.clear();
  iss.str(" < / bl!h > ");
  {tag t = read_next_tag(iss, emsg);
   assert(not emsg.empty()); }
  }

#endif


// $Log: check.cc,v $
// Revision 1.2  2004/02/13 18:00:10  rclayton
// Redefine tags.
//
// Revision 1.1  2004/01/25 23:15:16  rclayton
// Initial revision
//


syntax highlighted by Code2HTML, v. 0.9