The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/* ----------------------------------------------------------------------------
 * TL-HtmlFilter.xs
 * ----------------------------------------------------------------------------
 * Mastering programmed by YAMASHINA Hio
 *
 * Copyright 2005 YAMASHINA Hio
 * ----------------------------------------------------------------------------
 * $Id: HtmlFilter.xs 4923 2007-11-22 08:03:58Z hio $
 * ------------------------------------------------------------------------- */

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include <string.h>

#ifdef _MSC_VER /* Microsoft Visual C++ */
#define strncasecmp(s1,s2,n) _strnicmp(s1,s2,n)
#endif
#ifndef PERL_MAGIC_taint
#define PERL_MAGIC_taint 't'
#endif

static const int FILT_INTEREST       = 0;
static const int FILT_TRACK          = 1;
static const int FILT_FILTER_TEXT    = 2;
static const int FILT_FILTER_COMMENT = 3;
static const int FILT_CONTEXT        = 4;
static const int FILT_HTML           = 5;
static const int FILT_OUTPUT         = 6;

static const int ELEM_NAME   = 0;
static const int ELEM_ATTRS  = 1;
static const int ELEM_ATTR_H = 2;
static const int ELEM_TAIL   = 3;

static void
_void__call_method__sv(SV* this_sv, const char* method, SV* arg1) {
	dSP;

	ENTER;
	SAVETMPS;

	PUSHMARK(SP);
	XPUSHs(this_sv);
	XPUSHs(arg1);
	PUTBACK;

	call_method(method, G_DISCARD);

	FREETMPS;
	LEAVE;
}

static SV*
_sv__call_method__void(SV* this_sv, const char* method) {
	SV* ret;

	dSP;

	ENTER;
	SAVETMPS;

	PUSHMARK(SP);
	XPUSHs(this_sv);
	PUTBACK;

	call_method(method, G_SCALAR);

	SPAGAIN;
	ret = SvREFCNT_inc(POPs);
	PUTBACK;

	FREETMPS;
	LEAVE;

	return ret;
}

static SV*
_sv__call_method__sv(SV* this_sv, const char* method, SV* arg1) {
	SV* ret;

	dSP;

	ENTER;
	SAVETMPS;

	PUSHMARK(SP);
	XPUSHs(this_sv);
	XPUSHs(arg1);
	PUTBACK;

	call_method(method, G_SCALAR);

	SPAGAIN;
	ret = SvREFCNT_inc(POPs);
	PUTBACK;

	FREETMPS;
	LEAVE;

	return ret;
}

static SV*
_get_from_this_as_sv(AV* this_av, int key) {
	SV** ret = av_fetch(this_av, key, 0);

	if (ret != NULL) {
		return *ret;
	}
	else {
		return &PL_sv_undef;
	}
}

static AV*
_get_from_this_as_av(AV* this_av, int key) {
	SV* sv = _get_from_this_as_sv(this_av, key);

	if (SvROK(sv)) {
		AV* ret = (AV*)SvRV(sv);

		if (SvTYPE(ret) == SVt_PVAV) {
			return ret;
		}
		else {
			return NULL;
		}
	}
	else {
		return NULL;
	}
}

static HV*
_get_from_this_as_hv(AV* this_av, int key) {
	SV* sv = _get_from_this_as_sv(this_av, key);

	if (SvROK(sv)) {
		HV* ret = (HV*)SvRV(sv);

		if (SvTYPE(ret) == SVt_PVHV) {
			return ret;
		}
		else {
			return NULL;
		}
	}
	else {
		return NULL;
	}
}

static bool
_get_from_this_as_bool(AV* this_av, int key) {
	SV* sv = _get_from_this_as_sv(this_av, key);

	return SvTRUE(sv);
}

static void
_set_sv_to_this(AV* this_av, int key, SV* sv) {
	SV** ret;
	
	ret = av_store(this_av, key, sv);
	if (ret == NULL) {
		SvREFCNT_dec(sv);
	}
}

static SV*
_lc_COW(SV* str) {
	/* str 内に大文字がある場合、それらを小文字にした SV* を新たに作っ
	 * て返す。無ければ参照カウントを 1 上げただけでコピーせずに返す。
	 * COW は Copy-On-Write の略。
	 */
	SV* ret = NULL;
	char* ret_cstr = NULL;
	
	STRLEN len;
	const char* cstr;
	STRLEN i;

	cstr = SvPV(str, len);
	for (i = 0; i < len; i++) {
		if (isUPPER(cstr[i])) {
			if (ret == NULL) {
				/* まだコピーしていなかった */
				STRLEN dummy;
				
				ret = newSVpvn(cstr, len);
				ret_cstr = SvPV(ret, dummy);
			}

			ret_cstr[i] = toLOWER(cstr[i]);
		}
	}

	if (ret == NULL) {
		/* 最後まで大文字が見付からなかった */
		return SvREFCNT_inc(str);
	}
	else {
		return ret;
	}
}

static bool
_strEQ_ignore_case(SV* str1, SV* str2) {
	STRLEN s1_len, s2_len;
	const char *s1, *s2;

	s1 = SvPV(str1, s1_len);
	s2 = SvPV(str2, s2_len);

	if (s1_len == s2_len && s1_len > 0 &&
		strncasecmp(s1, s2, s1_len) == 0) {

		return TRUE;
	}
	else {
		return FALSE;
	}
}

static void
_store_unary_attr(AV* this_av, const char* ptr, int len) {
	SV* str = newSVpvn(ptr, len);

	_set_sv_to_this(this_av, ELEM_TAIL, str);
}

static bool
_does_look_like_element(SV* sv_str) {
	STRLEN len;
	const char* str = SvPV(sv_str, len);

	if (len >= 1 && str[0] == '<') {
		return TRUE;
	}
	else {
		return FALSE;
	}
}

static SV*
_parse_html_comment(SV* str_sv) {
	/* return ($str =~ m/^<!--\s*(.+?)\s*-->$/) ? $1 : undef */
	STRLEN len;
	const char* str = SvPV(str_sv, len);

	/* 前から <!--\s* を削る */
	if (len < 4 || strnNE(str, "<!--", 4)) {
		return &PL_sv_undef;
	}
	str += 4; len -= 4;

	while (len && isSPACE(str[0])) {
		str++; len--;
	}

	/* 後ろから \s*--> を削る */
	if (len < 3 || strnNE(&str[len - 3], "-->", 3)) {
		return &PL_sv_undef;
	}
	len -= 3;

	while (len && isSPACE(str[len - 1])) {
		len--;
	}

	if (len == 0) {
		return &PL_sv_undef;
	}
	else {
		return newSVpvn(str, len);
	}
}

static void
_parse_close_elem(SV* elem_name, bool* close, SV** nameonly) {
	STRLEN len;
	const char* str = SvPV(elem_name, len);

	if (len >= 1 && str[0] == '/') {
		*close = TRUE;
		*nameonly = newSVpvn(&str[1], len - 1);
	}
	else {
		*close = FALSE;
		*nameonly = SvREFCNT_inc(elem_name);
	}
}

static bool
_is_matched(AV* matcher, SV* str_sv) {
	int i;

	for (i = 0; i < av_len(matcher) + 1; i++) {
		SV* m = *av_fetch(matcher, i, 0);

		if (SvROK(m)) {
			int matched;
			dSP;

			ENTER;
			SAVETMPS;

			PUSHMARK(SP);
			XPUSHs(str_sv);
			PUTBACK;

			call_sv(m, G_SCALAR);

			SPAGAIN;
			matched = POPi;
			PUTBACK;

			FREETMPS;
			LEAVE;

			if (matched) {
				return TRUE;
			}
		}
		else {
			STRLEN s1_len, s2_len;
			const char *s1, *s2;

			s1 = SvPV(m, s1_len);
			s2 = SvPV(str_sv, s2_len);

			/* warn("strncasecmp(\"%s\", \"%s\")", SvPV_nolen(m), SvPV_nolen(str_sv)); */

			if (s1_len == s2_len && s1_len > 0 &&
				strncasecmp(s1, s2, s1_len) == 0) {

				/* warn("MATCHED"); */
				return TRUE;
			}
		}
	}

	return FALSE;
}

static bool
_is_tainted_pv(SV* sv)
{
	MAGIC* mg;
	if( SvTYPE(sv)!=SVt_PVMG )
	{
		return FALSE;
	}
	if( !SvPOKp(sv) )
	{
		return FALSE;
	}
	mg = SvMAGIC(sv);
	if( mg==NULL )
	{
		return FALSE;
	}
	if( mg->mg_moremagic!=NULL )
	{
		return FALSE;
	}
	if( mg->mg_type!=PERL_MAGIC_taint )
	{
		return FALSE;
	}
	return TRUE;
}

MODULE = Tripletail::HtmlFilter  PACKAGE = Tripletail::HtmlFilter
PROTOTYPES: ENABLE

void
next(SV* this_sv)
  PPCODE:
    {
		AV* this_av;
		SV* context;
		AV* html_av;
		AV* output;
		bool filter_comment;
		bool filter_text;
		bool track;
		bool interest;

		SV* ret_interested = &PL_sv_undef;
		
		this_av = (AV*)SvRV(this_sv);
		if (SvTYPE(this_av) != SVt_PVAV) {
			croak("Internal Error: $this is not an ARRAY ref");
		}

		context  = _get_from_this_as_sv(this_av, FILT_CONTEXT);
		html_av  = _get_from_this_as_av(this_av, FILT_HTML);
		output   = _get_from_this_as_av(this_av, FILT_OUTPUT);
		filter_comment = _get_from_this_as_bool(this_av, FILT_FILTER_COMMENT);
		filter_text    = _get_from_this_as_bool(this_av, FILT_FILTER_TEXT);
		track    = _get_from_this_as_bool(this_av, FILT_TRACK);
		interest = _get_from_this_as_bool(this_av, FILT_INTEREST);

		if (html_av == NULL) {
			croak("Internal Error: $this->{html} is not an ARRAY ref");
		}

		if (output == NULL) {
			croak("Internal Error: $this->{output} is not an ARRAY ref");
		}

		/* $this->[CONTEXT]->_flush($this); # 未確定の部分を確定する */
		_void__call_method__sv(context, "_flush", this_sv);

		/* while (@{$this->[HTML]}) { ... } */
		while (av_len(html_av) >= 0) { /* av_len は空の時に -1 */
			/* my $str = shift @{$this->[HTML]}; */
			SV* str_sv = av_shift(html_av);
			SV* comment;
			SV* interested = &PL_sv_undef;
			SV* parsed = &PL_sv_undef;

			if (!SvPOK(str_sv) && !_is_tainted_pv(str_sv) ) {
				croak("Internal Error: "
					  "$this->[HTML] contains an element other than string");
			}

			/* if ($str =~ m/^<!--\s*(.+?)\s*-->$/) { ... } */
			comment = _parse_html_comment(str_sv);
			if (comment != &PL_sv_undef && SvOK(comment)) {
				/* コメント
				 *
				 * if ($this->[FILTER_COMMENT]) {
				 *   $interested = $this->[CONTEXT]->newComment($1);
				 * }
				 */
				if (filter_comment) {
					interested = _sv__call_method__sv(
						context, "newComment", comment);
				}

				SvREFCNT_dec(comment);
			}
			else if (_does_look_like_element(str_sv)) {
				/* 要素
				 *
				 * if ($this->[TRACK] or $this->[INTEREST]) {
				 *   ($interested,$parsed) = $this->_next_elem($str);
				 * }
				 */
				if (track || interest) {
					int count;
					dSP;

					ENTER;
					SAVETMPS;

					PUSHMARK(SP);
					XPUSHs(this_sv);
					XPUSHs(str_sv);
					PUTBACK;

					count = call_method("_next_elem", G_ARRAY);

					SPAGAIN;

					if (count != 2) {
						croak("$this->_next_elem returned %d values", count);
					}

					parsed = SvREFCNT_inc(POPs);
					interested = SvREFCNT_inc(POPs);

					PUTBACK;
					FREETMPS;
					LEAVE;
				}
			}
			else {
				/* # テキスト
				 * if ($this->[FILTER_TEXT]) {
				 *   # 興味を持ってるときはオブジェクトにして返す. 
				 *   $interested = $this->[CONTEXT]->newText($str);
				 * }
				 */
				if (filter_text) {
					interested = _sv__call_method__sv(context, "newText", str_sv);
				}
			}

			/* if ($interested) { ... } */
			if (SvTRUE(interested)) {
				/* # この要素は興味を持たれている。
				 * $this->[CONTEXT]->_current($interested);
				 * return ($this->[CONTEXT], $interested);
				 */
				_void__call_method__sv(context, "_current", interested);
			}
			else {
				/* # そうでないなら出力に書いて次へ
				 * push(@{$this->[OUTPUT]},$parsed||$str);
				 */
				av_push(output, SvREFCNT_inc(
							SvTRUE(parsed) ? parsed : str_sv));
			}

			/* ここで str_sv と parsed がスコープから外れる */
			SvREFCNT_dec(str_sv);
			SvREFCNT_dec(parsed);

			if (SvTRUE(interested)) {
				ret_interested = sv_2mortal(interested);
				break;
			}
		}

		if (ret_interested != &PL_sv_undef && SvOK(ret_interested)) {
			XPUSHs(context);
			XPUSHs(ret_interested);
		}
	}

void
_next_elem(SV* this_sv, SV* str_sv)
PPCODE:
  {
	  AV* this_av;
	  SV* context;
	  AV* track;
	  AV* interest;
	  SV* elem;
	  SV* elem_name;
	  SV* interested = &PL_sv_undef;
	  SV* parsed     = &PL_sv_undef;

	  this_av = (AV*)SvRV(this_sv);
	  if (SvTYPE(this_av) != SVt_PVAV) {
		  croak("Internal Error: $this is not an ARRAY ref");
	  }

	  context  = _get_from_this_as_sv(this_av, FILT_CONTEXT);
	  track    = _get_from_this_as_av(this_av, FILT_TRACK);
	  interest = _get_from_this_as_av(this_av, FILT_INTEREST);

	  /* my $elem = $this->[CONTEXT]->newElement->parse($str); */
	  elem = _sv__call_method__void(context, "newElement");
	  _void__call_method__sv(elem, "parse", str_sv);

	  /* my $elem_name = $elem->name; */
	  /* 直接 name から取り出している事に注意 */
	  do {
		  AV* elem_av = (AV*)SvRV(elem);
		  SV** ret;
		  
		  if (SvTYPE(elem_av) != SVt_PVAV) {
			  croak("$elem is not an ARRAY ref");
		  }

		  ret = av_fetch(elem_av, ELEM_NAME, 0);
		  elem_name = (ret == NULL ? &PL_sv_undef : SvREFCNT_inc(*ret));
	  } while (0);

	  /* if (defined $elem_name) { ... } */
	  if (elem_name != &PL_sv_undef && SvOK(elem_name)) {
		  /* my ($close,$nameonly) = $elem_name =~ /^(\/?)(.*)/; */
		  bool close;
		  SV* nameonly;

		  if (!SvPOK(elem_name) && !_is_tainted_pv(elem_name) ) {
			  croak("$elem->name returned non-string");
		  }

		  _parse_close_elem(elem_name, &close, &nameonly);

		  /* if ($this->[TRACK] and $is_matched->($this->[TRACK], $nameonly)) { ... } */
		  if (track != NULL && _is_matched(track, nameonly)) {
			  /* $parsed = $elem; */
			  parsed = SvREFCNT_inc(elem);

			  /* if ($close) {
			   *     $this->[CONTEXT]->removein($nameonly);
			   * }
			   * else {
			   *     $this->[CONTEXT]->addin($nameonly => $parsed);
			   * }
			   */
			  do {
				  dSP;

				  ENTER;
				  SAVETMPS;

				  PUSHMARK(SP);
				  XPUSHs(context);
				  XPUSHs(nameonly);
				  if (!close) {
					  XPUSHs(parsed);
				  }
				  PUTBACK;

				  call_method(close ? "removein" : "addin", G_DISCARD);
				  
				  FREETMPS;
				  LEAVE;
			  } while (0);
		  }

		  /* if ($this->[INTEREST] and $is_matched->($this->[INTEREST], $elem_name)) { ... } */
		  if (interest != NULL && _is_matched(interest, elem_name)) {
			  /* $interested = $elem; */
			  interested = SvREFCNT_inc(elem);
		  }

		  /* ここで nameonly がスコープを外れる */
		  SvREFCNT_dec(nameonly);
	  }

	  /* ここで elem と elem_name がスコープを外れる */
	  SvREFCNT_dec(elem_name);
	  SvREFCNT_dec(elem);

	  /* ($interested,$parsed); */
	  XPUSHs(sv_2mortal(interested));
	  XPUSHs(sv_2mortal(parsed));
  }


MODULE = Tripletail::HtmlFilter  PACKAGE = Tripletail::HtmlFilter::Element
PROTOTYPES: ENABLE

void
parse(SV* this_sv, SV* str_sv)
CODE:
  if( SvROK(this_sv) && str_sv!=&PL_sv_undef && (SvPOK(str_sv) || _is_tainted_pv(str_sv)) )
  {
    STRLEN len;
    const char* str = SvPV(str_sv,len);
    AV* this_av = (AV*)SvRV(this_sv);
    if( SvTYPE(this_av)==SVt_PVAV && len>=2 )
    {
      STRLEN pos,i;
      /* s/^<//; */
      pos = str[0]=='<' ? 1 : 0;

      /* (s/^\s*(\/?\w+)//) and ($this->[NAME] = $1); */
      while( pos<len && isSPACE(str[pos]) )
      {
        ++pos;
      }
      i = pos<len && str[pos]=='/' ? pos+1 : pos;
      while( i<len && isALNUM(str[i]) )
      {
        ++i;
      }
      if( i!=pos )
      {
		/* 直接 name に入れている事に注意 */
		_set_sv_to_this(this_av, ELEM_NAME, newSVpvn(str+pos,i-pos));
        pos = i;
      }
      
      /*
        while(1) {
            (s/([\w:\-]+)\s*=\s*"([^"]*)"//)     ? ($this->attr($1 => $2)) :
              (s/([\w:\-]+)\s*=\s*'([^']*)'//)   ? ($this->attr($1 => $2)) :
                (s/([\w:\-]+)\s*=\s*([^\s>]+)//) ? ($this->attr($1 => $2)) :
                  (s~(\w+|/)~~)                  ? ($this->end($1)) :
                    last;
        }
        \w* (-:[\w:-]+)?  \s* = \s* " [^"]*   "
        \w* (-:[\w:-]+)?  \s* = \s* ' [^']*   '
        \w* (-:[\w:-]+)?  \s* = \s*   [^\s>]*   
        ^1  ^2          ^3          ^4        ^5
        1:name_start, 2:unary_end, 3:name_end
        4:value_start, 5:value_end
      */
      for( ; i<len; pos=i )
      {
        int pos_name_start, pos_name_end;
        int pos_value_start, pos_value_end;
        int pos_unary_end;
        assert( i==pos );
        if( isSPACE(str[i]) )
        {
          ++i;
          continue;
        }
        pos_name_start = i;
        while( i<len && isALNUM(str[i]) ) ++i;
        pos_unary_end = i;
        while( i<len && (isALNUM(str[i])||str[i]==':'||str[i]=='-') ) ++i;
        pos_name_end = i;
        if( i==pos )
        {
          while( i<len && !(isALNUM(str[i])||str[i]==':'||str[i]=='-'||str[i]=='/') )
          {
            ++i;
          }
          if( i<len && str[i]=='/' )
          {
            _store_unary_attr(this_av,"/",1);
            ++i;
          }
          if( i==pos )
          {
            break;
          }
          continue;
        }
        while( i<len && isSPACE(str[i]) ) ++i;
        if( i>=len || str[i]!='=' )
        { /* unary attr */
          if( pos_unary_end-pos_name_start!=0 )
          {
            _store_unary_attr(this_av,str+pos_name_start,pos_unary_end-pos_name_start);
            i = pos_unary_end;
          }
          continue;
        }
        ++i; /* skip '=' */
        while( i<len && isSPACE(str[i]) ) ++i;
        if( i>=len )
        { /* unary attr */
          if( pos_unary_end-pos_name_start!=0 )
          {
            _store_unary_attr(this_av,str+pos_name_start,pos_unary_end-pos_name_start);
            i = pos_unary_end;
          }
          continue;
        }else if( str[i]=='\"' || str[i]=='\'' )
        {
          const char endmark = str[i];
          pos_value_start = ++i;
          while( i<len && str[i]!=endmark ) ++i;
          if( i>=len )
          { /* unary attr */
            if( pos_unary_end-pos_name_start!=0 )
            {
              _store_unary_attr(this_av,str+pos_name_start,pos_unary_end-pos_name_start);
              i = pos_unary_end;
            }
            continue;
          }
          pos_value_end = i++;
        }else
        {
          pos_value_start = i;
          while( i<len && (!isSPACE(str[i]) && str[i]!='>') ) ++i;
          pos_value_end = i;
        }
        {
          /* $this->attr($name => $value); */
          ENTER;
          SAVETMPS;
          PUSHMARK(sp);
          XPUSHs(this_sv);
          XPUSHs(sv_2mortal(newSVpvn(str+pos_name_start, pos_name_end-pos_name_start)));
          XPUSHs(sv_2mortal(newSVpvn(str+pos_value_start, pos_value_end-pos_value_start)));
          PUTBACK;
          call_method("attr", G_DISCARD);
          SPAGAIN;
          PUTBACK;
          FREETMPS;
          LEAVE;
        }
      }
    }
  }
  XPUSHs(this_sv);

SV*
attr(SV* this_sv, SV* key, ...)
PROTOTYPE: $$;$
CODE:
  {
	  AV* this_av;
	  SV* lc_key;
	  HV* attr_h;

	  this_av = (AV*)SvRV(this_sv);
	  if (SvTYPE(this_av) != SVt_PVAV) {
		  croak("Internal Error: $this is not an ARRAY ref");
	  }

	  /*
	  if (not defined $key) {
		  die __PACKAGE__."#attr: ARG[1] is not defined.\n";
	  }
	  elsif (ref $key) {
		  die __PACKAGE__."#attr: ARG[1] is a Ref. [$key]\n";
	  }
	  */
	  if (key == &PL_sv_undef || !SvOK(key)) {
		  croak("Tripletail::HtmlFilter::Element#attr: ARG[1] is not defined.\n");
	  }
	  else if (SvROK(key)) {
		  croak("Tripletail::HtmlFilter::Element#attr: ARG[1] is a Ref. [%s]\n",
				SvPV_nolen(key));
	  }

	  lc_key = _lc_COW(key);

	  attr_h = _get_from_this_as_hv(this_av, ELEM_ATTR_H);
	  if (attr_h == NULL) {
		  croak("Internal error: $this->[ATTR_H] is not a HASH ref");
	  }

	  /* if (@_) { ... } */
	  if (items > 2) {
		  /*
		  my $val = shift;

		  if (ref $val) {
			  die __PACKAGE__."#attr: ARG[2] is a Ref. [$val]\n";
		  }
		  */
		  SV* val = ST(2);
		  AV* attrs;

		  if (SvROK(val)) {
			  croak("Tripletail::HtmlFilter::Element#attr: ARG[2] is a Ref. [%s]\n",
					SvPV_nolen(val));
		  }

		  attrs = _get_from_this_as_av(this_av, ELEM_ATTRS);
		  if (attrs == NULL) {
			  croak("Internal error: $this->[ATTRS] is not a ARRAY ref");
		  }

		  if (val != &PL_sv_undef && SvOK(val)) {
			  /*
			  # この属性が既にあるなら上書き。無ければ末尾に追加。
			  my $lc_key = lc $key;
			
			  if (my $old = $this->[ATTR_H]{$lc_key}) {
			    $old->[1] = $val;
			  }
			  else {
				my $pair = [$key, $val];
				push @{$this->[ATTRS]}, $pair;
				$this->[ATTR_H]{$lc_key} = $pair;
			  }
			  */
			  HE* old_ent;

			  old_ent = hv_fetch_ent(attr_h, lc_key, 0, 0);
			  if (old_ent != NULL && HeVAL(old_ent) != &PL_sv_undef && 
				  SvOK(HeVAL(old_ent))) {

				  /* $old->[1] = $val; */
				  AV* old_av;
				  SV** ret;

				  if (!SvROK(HeVAL(old_ent))) {
					  croak("Internal error: non-ARRAY element in ATTR_H: %s",
							SvPV_nolen(HeVAL(old_ent)));
				  }

				  old_av = (AV*)SvRV(HeVAL(old_ent));
				  if (SvTYPE(old_av) != SVt_PVAV) {
					  croak("Internal error: non-ARRAY element in ATTR_H: %s",
							SvPV_nolen(HeVAL(old_ent)));
				  }

				  if (av_len(old_av) != 1) {
					  croak("Internal error: pair with an invalid length");
				  }

				  ret = av_store(old_av, 1, SvREFCNT_inc(val));
				  if (ret == NULL) {
					  SvREFCNT_dec(val);
				  }
			  }
			  else {
				  /*
				  my $pair = [$key, $val];
				  push @{$this->[ATTRS]}, $pair;
				  $this->[ATTR_H]{$lc_key} = $pair;
				  */
				  AV* pair_av = newAV();
				  SV* pair;
				  HE* ret;

				  av_extend(pair_av, 1); /* $pair_av[1] が存在するように */
				  av_push(pair_av, SvREFCNT_inc(key));
				  av_push(pair_av, SvREFCNT_inc(val));

				  pair = newRV_inc((SV*)pair_av);
				  av_push(attrs, pair);

				  pair = newRV_noinc((SV*)pair_av);
				  ret = hv_store_ent(attr_h, lc_key, pair, 0);
				  if (ret == NULL) {
					  SvREFCNT_dec(pair);
				  }
			  }
		  }
		  else {
			  /*
			  # この属性を消去
			  if (my $old = $this->[ATTR_H]{lc $key}) {
				  delete $this->[ATTR_H]{$key};
				
				  @{$this->[ATTRS]} = grep {
					  lc($_->[0]) ne lc($key);
				  } @{$this->[ATTRS]};
			  }
			  */
			  HE* old_ent;

			  old_ent = hv_fetch_ent(attr_h, lc_key, 0, 0);
			  if (old_ent != NULL) {
				  AV* new_attrs = newAV();
				  int i;
				  
				  hv_delete_ent(attr_h, lc_key, G_DISCARD, HeHASH(old_ent));

				  av_extend(new_attrs, av_len(attrs) - 1);
				  for (i = 0; i <= av_len(attrs); i++) {
					  SV* old_elem_sv;
					  AV* old_elem;
					  SV* old_key;

					  old_elem_sv = *av_fetch(attrs, i, 0);
					  if (!SvROK(old_elem_sv)) {
						  croak("Internal error: non-ARRAY in $this->[ATTRS]: %s",
								SvPV_nolen(old_elem_sv));
					  }
					  
					  old_elem = (AV*)SvRV(old_elem_sv);
					  if (SvTYPE(old_elem) != SVt_PVAV) {
						  croak("Internal error: non-ARRAY in $this->[ATTRS]: %s",
								SvPV_nolen(old_elem_sv));
					  }

					  if (av_len(old_elem) != 1) {
						  croak("Internal error: pair with an invalid length");
					  }

					  old_key = *av_fetch(old_elem, 0, 0);

					  if (!_strEQ_ignore_case(old_key, key)) {
						  av_push(new_attrs, SvREFCNT_inc(old_elem_sv));
					  }
				  }

				  _set_sv_to_this(this_av, ELEM_ATTRS, newRV_noinc((SV*)new_attrs));
			  }
		  }

		  RETVAL = SvREFCNT_inc(val);
	  }
	  else {
		  /*
		  if (my $pair = $this->[ATTR_H]{lc $key}) {
			$pair->[1];
		  }
		  else {
			  undef; # 存在しない
		  }
		  */
		  HE* ent;

		  ent = hv_fetch_ent(attr_h, lc_key, 0, 0);
		  if (ent != NULL && HeVAL(ent) != &PL_sv_undef && SvOK(HeVAL(ent))) {
			  AV* pair_av;
			  SV* ret;

			  if (!SvROK(HeVAL(ent))) {
				  croak("Internal error: non-ARRAY element in ATTR_H: %s",
						SvPV_nolen(HeVAL(ent)));
			  }

			  pair_av = (AV*)SvRV(HeVAL(ent));
			  if (SvTYPE(pair_av) != SVt_PVAV) {
				  croak("Internal error: non-ARRAY element in ATTR_H: %s",
						SvPV_nolen(HeVAL(ent)));
			  }

			  if (av_len(pair_av) != 1) {
				  croak("Internal error: pair with an invalid length");
			  }

			  ret = *av_fetch(pair_av, 1, 0);
			  RETVAL = SvREFCNT_inc(ret);
		  }
		  else {
			  RETVAL = &PL_sv_undef;
		  }
	  }

	  /* ここで lc_key がスコープから外れる */
	  SvREFCNT_dec(lc_key);
  }
OUTPUT:
  RETVAL

# -----------------------------------------------------------------------------
# End of File.
# -----------------------------------------------------------------------------