/*==============================================================================
	RSS Feed クラス
	$Id$
	Copyright (C) 2005 OKAMURA Yuji, All rights reserved.
==============================================================================*/
/*---- 静的属性 ----*/
/*
 * 現在処理対象の CRssFeed クラスオブジェクト
 */
var	CRssFeed_M_Objects = new Array;

/*---- 静的メソッド ----*/
/*
 * RSS Feed クラスのコンストラクタ
 * @param url	RSS の URL,
 * @param welmt	RSS Feed を書き出す HTML の DOM ノード
 * @param async	非同期で動作させるかどうか(オプション)
 */
function CRssFeed(url, welmt, async) {
	/*---- メンバー変数 ----*/
	/*
	 * RSS の URL
	 */
	this.m_Url = url;
	/*
	 * RSS を出力する要素
	 */
	this.m_Welmt = welmt;
	/*
	 * 非同期で表示するかどうか
	 */
	this.m_ASync = async;
	/*
	 * 更新間隔(ミリ秒単位)
	 */
	this.m_LoadInterval = 15 * 60 * 1000;

	/*
	 * リソース文字列
	 */
	this.m_ResourceStrings = new Array();
	this.m_ResourceStrings['unsuported'] = 'サポート対象外です';
	this.m_ResourceStrings['uninitialized'] = '未初期化';
	this.m_ResourceStrings['loading'] = 'ロード中...';
	this.m_ResourceStrings['loaded'] = 'ロード終了';
	this.m_ResourceStrings['interactive'] = '処理中...';
	this.m_ResourceStrings['uninitialized'] = '未初期化';
	this.m_ResourceStrings['notRss'] = 'RSS ではありません';
	this.m_ResourceStrings['invalidRSS'] = '正しい RSS ではありません';

	/*
	 * XMLHttpRequest オブジェクト
	 */
	this.m_Xmlhttp = false;
	if (window.XMLHttpRequest) {
		try {
			this.m_Xmlhttp = new XMLHttpRequest();
		}
		catch (e) {
			this.m_Xmlhttp = false;
		}
	}
	else if (window.ActiveXObject) {
		try {
			this.m_Xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
		}
		catch (e1) {
			try {
				this.m_Xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
			}
			catch (e2) {
				this.m_Xmlhttp = false;
			}
		}
	}

	/*
	 * インスタンスの CRssFeed_M_Objects におけるインデックス(Load 用)
	 */
	this.m_LoadIdx = null;

	/*
	 * インスタンスの CRssFeed_M_Objects におけるインデックス(LoadInterval 用)
	 */
	this.m_LoadIntervalIdx = null;

	/*
	 * LoadInterval による interval の ID
	 */
	this.m_LoadIntervalID = null;

	/*---- メソッド ----*/
	// 外部インターフェースを意図

	/*
	 * RSS をロードする
	 */
	this.Load = CRssFeed_Load;

	/*
	 * 定期的に RSS をロードする
	 * @param loadNow	一回目のロードを今すぐ行うかどうか
	 */
	this.LoadInterval = CRssFeed_LoadInterval;

	/*
	 * LoadInterval による定期的な RSS のロードを中止する
	 */
	this.StopLoadInterval = CRssFeed_StopLoadInterval;

	// 下位クラスで書き換えを意図

	/*
	 * RSS 出力用の HTML を生成する
	 * @param channel	channel 要素をハッシュ化したもの
	 * @return HTML を表す文字列
	 */
	this.RssHTML = CRssFeed_RssHTML;

	/*
	 * channel 出力用の HTML を生成する<br>
	 * item は含まない
	 * @param channel	channel 要素をハッシュ化したもの
	 * @return HTML を表す文字列
	 */
	this.ChannelHTML = CRssFeed_ChannelHTML;

	/*
	 * item 出力用の HTML を生成する
	 * @param itemIdx	何番目のアイテムか
	 * @param channel	channel 要素をハッシュ化したもの
	 * @return HTML を表す文字列
	 */
	this.ItemHTML = CRssFeed_ItemHTML;

	// このクラスおよび下位クラスでの使用を意図

	/*
	 * HTML のテキストとして安全な文字列に変換する
	 * @param text	変換対象の文字列
	 * @return 変換後の文字列
	 */
	this.SafeHTMLText = CRssFeed_SafeHTMLText;

	/*
	 * HTML の属性値として安全な文字列に変換する
	 * @param text	変換対象の文字列
	 * @return 変換後の文字列
	 */
	this.SafeHTMLAttrText = CRssFeed_SafeHTMLAttrText;

	/*
	 * 文字列の HTML の div 要素をトップとするノードに変換する<br>
	 * description 要素など HTML を含んだ CDATA を DOM オブジェクトに変換するときに使用する。
	 * @param str	CDATA またはテキスト
	 * @return HTML の div 要素を表すノード
	 */
	this.HtmlStr2Node = CRssFeed_HtmlStr2Node;

	/*
	 * HTML のノードをテキストに変換する<br>
	 * HTML をテキスト表示したり、title 属性値にしたりするときに使用する。
	 * @param node	HTML のノード
	 * @param parentName	親要素のタグ名
	 * @return 変換されたテキスト
	 */
	this.Html2Text = CRssFeed_Html2Text;

	// このクラスでのみ使用を意図

	/*
	 * XMLHttpRequest の状態イベントハンドラー
	 */
	this.OnRssFeedReadyStateChange = CRssFeed_OnRssFeedReadyStateChange;

	/*
	 * RSS から HTML を生成する
	 * @param xml	XML オブジェクト
	 */
	this.WriteRSS = CRssFeed_WriteRSS;

	/*
	 * RSS 2.0, 0.9X 用の HTML 生成
	 * @param xml	XML オブジェクト
	 */
	this.WriteRSS2 = CRssFeed_WriteRSS2;

	/*
	 * RSS 1.0 用の HTML 生成
	 * @param xml	XML オブジェクト
	 */
	this.WriteRSS1 = CRssFeed_WriteRSS1;

	/*
	 * ノードの下位要素の最初のテキストまたは CDATA を取得する<br>
	 * テキストまたは CDATA のみを含む要素で示された値を取得するときに使用する。
	 * @param node	ノード
	 * @return テキストまたは CDATA の内容
	 */
	this.GetNodeValueData = CRssFeed_GetNodeValueData;
}

/*---- メソッドの実装 ----*/
function CRssFeed_Load() {
	if (this.m_Xmlhttp) {
		this.m_Xmlhttp.abort();
		if (this.m_ASync) {
			var	code;

			if (!this.m_LoadIdx) {
				for (
					this.m_LoadIdx = 0;
					this.m_LoadIdx < CRssFeed_M_Objects.length;
					this.m_LoadIdx++
				) {
					if (!CRssFeed_M_Objects[this.m_LoadIdx]) break;
				}
				CRssFeed_M_Objects[this.m_LoadIdx] = this;
			}

			code = 'this.m_Xmlhttp.onreadystatechange = function () { CRssFeed_M_Objects['+String(this.m_LoadIdx)+'].OnRssFeedReadyStateChange() }';
			eval(code);
		}
		try {
			this.m_Xmlhttp.open('GET', this.m_Url, this.m_ASync);
			this.m_Xmlhttp.send(null);
			if (!this.m_ASync) {
				this.OnRssFeedReadyStateChange();
			}
		}
		catch (e) {
			this.m_Welmt.innerHTML = this.SafeHTMLText(String(e));
		}
	}
	else {
		this.m_Welmt.innerHTML = this.m_ResourceStrings['unsuported'];
	}
}

function CRssFeed_LoadInterval(loadNow) {
	if (loadNow) this.Load();

	if (!this.m_LoadIntervalIdx) {
		for (
			this.m_LoadIntervalIdx = 0;
			this.m_LoadIntervalIdx < CRssFeed_M_Objects.length;
			this.m_LoadIntervalIdx++
		) {
			if (!CRssFeed_M_Objects[this.m_LoadIntervalIdx]) break;
		}
		CRssFeed_M_Objects[this.m_LoadIntervalIdx] = this;
	}

	this.m_LoadIntervalID = setInterval(
		'CRssFeed_M_Objects['+this.m_LoadIntervalIdx+'].Load()',
		this.m_LoadInterval
	);
}

function CRssFeed_StopLoadInterval() {
	if (!this.m_LoadIntervalID) return;
	clearInterval(this.m_LoadIntervalID);
	this.m_LoadIntervalID = null;
	CRssFeed_M_Objects[this.m_LoadIntervalIdx] = undefined;
	this.m_LoadIntervalIdx = null;
}

function CRssFeed_RssHTML(channel) {
	var	html;
	var	i;

	html = this.ChannelHTML(channel);
	html += '<ol>';
	for (i = 0; i < channel['item'].length; i++) {
		html += this.ItemHTML(i, channel);
	}
	html += '</ol>';

	return html;
}

function CRssFeed_ChannelHTML(channel) {
	var	html;

	html = '<a href="'
		+ this.SafeHTMLAttrText(channel['link'])
		+ '" title="'
		+ this.SafeHTMLAttrText(channel['description'])
	;
	if (channel['language']) {
		html += '" hreflang="' + this.SafeHTMLAttrText(channel['language']);
	}
	html += '">' + this.SafeHTMLText(channel['title']) + '</a>';

	return html;
}

function CRssFeed_ItemHTML(itemIdx, channel) {
	var	html;

	html = '<li><a href="'
		+ this.SafeHTMLAttrText(channel['item'][itemIdx]['link'])
		+ '" title="'
		+ this.SafeHTMLAttrText(this.Html2Text(this.HtmlStr2Node(
			channel['item'][itemIdx]['description']
		  )))
	;
	if (channel['language']) {
		html += '" hreflang="' + this.SafeHTMLAttrText(channel['language']);
	}
	html += '">'
		+ this.SafeHTMLText(channel['item'][itemIdx]['title'])
		+ '</a></li>'
	;

	return html;
}

function CRssFeed_SafeHTMLText(text) {
	text = String(text).replace(/&/g, '&amp;');
	text = text.replace(/</g, '&lt;');
	text = text.replace(/"/g, '&quot;');
	text = text.replace(/>/g, '&gt;');

	return text;
}

function CRssFeed_SafeHTMLAttrText(text) {
	text = this.SafeHTMLText(text);
	text = text.replace(/\r/g, ' ');
	text = text.replace(/\n/g, ' ');

	return String(text);
}

function CRssFeed_HtmlStr2Node(str) {
	var	node = document.createElement('div');

	node.innerHTML = str;
	return node;
}

function CRssFeed_Html2Text(node, parentName) {
	var	text = '';
	var	child;
	var	i;
	var	alt;

	child = node.childNodes;
	for (i = 0; i < child.length; i++) {
		var	nodeName = child[i].nodeName;

		nodeName = nodeName.toLowerCase();
		switch (nodeName) {
		// テキストを取り出すノード
		case '#text':
		case '#cdata-section':
			alt = child[i].nodeValue;
			alt = alt.replace(/\r/, '\n');
			if (parentName != 'pre') {
				alt = alt.replace(/[\n\t ]+/g, ' ');
			}
			text += alt;
			break;
		// 無視するノード
		case 'script':
			break;
		// ブロック要素のノード
		case 'p':
		case 'h1':
		case 'h2':
		case 'h3':
		case 'h4':
		case 'h5':
		case 'h6':
		case 'ul':
		case 'ol':
		case 'dir':
		case 'menu':
		case 'pre':
		case 'dl':
		case 'div':
		case 'center':
		case 'noframes':
		case 'blockquote':
		case 'form':
		case 'isindex':
		case 'hr':
		case 'table':
		case 'fieldset':
		case 'address':
		case 'multicol':
		case 'noscript':
			if (text != '') text += '\n';
			text = text.replace(/\n+$/, '\n');
			text += this.Html2Text(child[i], child[i].nodeName);
			if (i + 1 < child.length) text += '\n';
			text = text.replace(/\n+$/, '\n');
			break;
		// 下位要素によってブロック要素かインライン要素か決まるもの
		case 'ins':
		case 'del':
			alt = this.Html2Text(child[i], child[i].nodeName);
			if (alt.indexOf('\n') >= 0) {
				if (text != '') text += '\n';
				text = text.replace(/\n+$/, '\n');
				text += alt;
				if (i + 1 < child.length) text += '\n';
				text = text.replace(/\n+$/, '\n');
			}
			else {
				text += alt;
			}
			break;
		// alt 属性を採用する要素のノード
		case 'img':
		case 'object':
			alt = child[i].getAttribute('alt');
			if (alt) text += alt;
			break;
		// その他のノード(インライン要素)
		default:
			text += this.Html2Text(child[i], child[i].nodeName);
			break;
		}
	}

	return text;
}

function CRssFeed_OnRssFeedReadyStateChange() {
	switch (this.m_Xmlhttp.readyState) {
	case 0:	// uninitialized
		this.m_Welmt.innerHTML = this.m_ResourceStrings['uninitialized'];
		break;
	case 1:	// loading
		this.m_Welmt.innerHTML = this.m_ResourceStrings['loading'];
		break;
	case 2: // loaded
		this.m_Welmt.innerHTML = this.m_ResourceStrings['loaded'];
		break;
	case 3: // interactive
		this.m_Welmt.innerHTML = this.m_ResourceStrings['interactive'];
		break;
	case 4:	// complate
		if (this.m_Xmlhttp.status == 200) {
			var	xml = this.m_Xmlhttp.responseXML;

			try {
				xml = xml.getElementsByTagName('rss');
				xml = xml[0];
				this.WriteRSS(xml);
			}
			catch (e) {
				this.m_Welmt.innerHTML = this.m_ResourceStrings['notRss'];
			}
			if (this.m_LoadIdx) {
				CRssFeed_M_Objects[this.m_LoadIdx] = undefined;
				this.m_LoadIdx = null;
			}
		}
		else {
			this.m_Welmt.innerHTML = 
				this.m_Url + ': '
				+ this.m_Xmlhttp.status + ' ' + this.m_Xmlhttp.statusText
			;
		}
		break;
	}
}

function CRssFeed_WriteRSS(xml) {
	var	rssVersion;

	this.m_Welmt.innerHTML = '';

	try {
		rssVersion = xml.getAttribute('version');
	}
	catch (e) {
		this.m_Welm.innerHTML = this.m_ResourceStrings['invalidRSS'];
		return;
	}

	switch (rssVersion) {
	case '2.0':
	case '0.91':
	case '0.92':
		this.WriteRSS2(xml)
		break;
	case '1.0':
		this.WriteRSS1(xml)
		break;
	default:
		this.m_Welmt.innerHTML = this.m_ResourceStrings['unsuported'];
	}
}

function CRssFeed_WriteRSS2(xml) {
	var	html;

	try {
		var	channel = new Array();
		var	child;
		var	i;

		{
			var	node;

			node = xml.getElementsByTagName('channel');
			node = node[0];
			child = node.childNodes;
		}

		for (i = 0; i < child.length; i++) {
			switch (child[i].nodeName) {
			// 唯一の下位要素で単純なデータを表す要素
			case 'title':
			case 'link':
			case 'description':
			case 'language':
			case 'copyright':
			case 'managingEditor':
			case 'webMaster':
			case 'lastBuildDate':
			case 'generator':
			case 'docs':
			case 'ttl':
			case 'rating':
			case 'ttl':
				channel[child[i].nodeName] = this.GetNodeValueData(
					child[i]
				);
				break;
			// 単純なデータだが複数あり得る要素
			case 'category':
				{
					var	array;

					if (channel[child[i].nodeName]) {
						array = channel[child[i].nodeName];
					}
					else {
						array = new Array();
					}
					array[array.lenght] = this.GetNodeValueData(child[i]);
					channel[child[i].nodeName] = array;
				}
				break;
			// 属性値でデータを示す要素
			case 'cloud':
				{
					var	hash = new Array();
					var	attr = child[i].attributes;
					var	j;

					for (j = 0; j < attr.length; j++) {
						var	attrItem = attr.item[j];

						hash[attrItem.nodeName] = attrItem.nodeValue;
					}
					channel[child[i].nodeName] = hash;
				}
				break;
			// 単純なデータを下位要素とする下位要素で属性を示すもの
			case 'image':
			case 'textInput':
				{
					var	hash = new Array();
					var	subChild;

					subChild = child[i].childNodes;
					for (j = 0; j < subChild.length; j++) {
						hash[subChild[j].nodeName] = this.GetNodeValueData(
							subChild[j]
						);
					}
					channel[child[i].nodeName] = hash;
				}
				break;
			// item 要素
			case 'item':
				// 一旦そのまま格納して後から処理する
				{
					var	array;

					if (channel[child[i].nodeName]) {
						array = channel[child[i].nodeName];
					}
					else {
						array = new Array();
					}
					array[array.length] = child[i];
					channel[child[i].nodeName] = array;
				}
				break;
			// その他独自フォーマットの要素
			case 'skipHours':
				{
					var	array = new Array();
					var	hours = child[i].getElementsByTagName('hour');

					for (j = 0; j < hours.length; j++) {
						var	hour = this.GetNodeValueData(hours[j]);

						hour = hour.replace(/^[ \t\n\r]*/, '');
						hour = hour.replace(/[ \t\n\r]*$/, '');
						array[array.length] = hour;
					}
					channel[child[i].nodeName] = array;
				}
				break;
			case 'skipDays':
				{
					var	array = new Array();
					var	hours = child[i].getElementsByTagName('day');

					for (j = 0; j < hours.length; j++) {
						var	hour = this.GetNodeValueData(hours[j]);

						hour = hour.replace(/^[ \t\n\r]*/, '');
						hour = hour.replace(/[ \t\n\r]*$/, '');
						array[array.length] = hour;
					}
					channel[child[i].nodeName] = array;
				}
				break;
			}
		}

		for (i = 0; i < channel['item'].length; i++) {
			var	item = new Array();
			var	j;

			child = channel['item'][i].childNodes;
			for (j = 0; j < child.length; j++) {
				switch (child[j].nodeName) {
				// 唯一の下位要素で単純なデータを表す要素
				case 'title':
				case 'link':
				case 'description':
				case 'author':
				case 'comments':
				case 'enclosure':
				case 'guid':
				case 'pubDate':
					item[child[j].nodeName] = this.GetNodeValueData(
						child[j]
					);
					break;
				// 単純なデータだが複数あり得る要素
				case 'category':
					{
						var	array;

						if (item[child[j].nodeName]) {
							array = item[child[j].nodeName];
						}
						else {
							array = new Array();
						}
						array[array.lenght] = this.GetNodeValueData(
							child[j]
						);
						item[child[j].nodeName] = array;
					}
					break;
				// その他独自のフォーマットの要素
				case 'source':
					{
						var	hash = new Array();
						var	attr = child[i].attributes;
	
						hash['url'] = attr.getNamedItem('url').nodeValue;
						hash['title'] = this.GetNodeValueData(child[i]);
						item[child[j].nodeName] = hash;
					}
					break;
				}
			}

			channel['item'][i] = item;
		}

		html = this.RssHTML(channel);
	}
	catch (e) {
		html = this.m_ResourceStrings['invalidRSS'];
	}

	this.m_Welmt.innerHTML = html;
}

function CRssFeed_WriteRSS1(xml) {
	// TODO
	this.m_Welmt.innerHTML = this.m_ResourceStrings['unsuported'];
}

function CRssFeed_GetNodeValueData(node) {
	var	child = node.childNodes;

	for (i = 0; i < child.length; i++) {
		if (
			child[i].nodeName == '#text'
				||
			child[i].nodeName == '#cdata-section'
		) {
			return child[i].nodeValue;
		}
	}
	return null;
}
