﻿/**************************************************************************************************/
/*			ParserXML.js - Une librairie pour parser des documents XML et             */
/*                                     des documents HTML simples (et bien formés, si possible)   */
/* Auteur : Jonathan Pasquier                                                                     */
/* Date : 05/12/2005                                                                              */
/**************************************************************************************************/
var isHTML = false;
var errorMSG = "";

function parse(XMLCode, parseHTML) {
	if (arguments.length < 2) { parseHTML = false }
	
	isHTML = parseHTML;
	
	var racine = null;
	var pile_tag = new Array();

	// On enlève tous les sauts de ligne
	XMLCode = XMLCode.replace(/\s+/g, " ");
	
	// On enlève la déclaration XML et la déclaration de DOCTYPE
	XMLCode = removeXMLDeclarationTag(XMLCode);
	XMLCode = removeDoctypeDeclarationTag(XMLCode);
	
	if (parseHTML) {
		XMLCode = removeScripts(XMLCode);
	}
	
	// On récupère la première balise
	var tmp =  getNextTag(XMLCode);
	var node = new Node(tmp[0]);
	node.setAttributes(parseAttributes(tmp[1]));
	
	racine = node;
	
	// On place le premier noeud
	pile_tag.unshift(node);
	
	tmp = getRemainingCode(XMLCode);
	
	pile_tag[0].addContent(tmp[0]);
	
	XMLCode = tmp[1];
	
	while (pile_tag.length != 0 && XMLCode != "") {
		// On récupère le prochain tag
		var array_tag =  getNextTag(XMLCode); // 0 - tag; 1 - attribute; 2 - type
	
		// Suivant le type de balise...
		switch (array_tag[2]) {
			case OPENING :
				var node = new Node(array_tag[0]);
				
				node.setAttributes(parseAttributes(array_tag[1]));
				
				// On place le noeud dans la pile
				pile_tag[0].addChild(node);
				pile_tag.unshift(node);
				
				// On récupère le contenu entre cette balise et la suivante
				var remain = getRemainingCode(XMLCode); // 0 - data; 1 - XML code
							
				pile_tag[0].addContent(remain[0]);
							
				XMLCode = remain[1];
				
				break;
			case CLOSING :
				// On enlève le noeud courant de la pile
				var node = pile_tag.shift();
				
				// Si le tag fermant ne correspond pas au tag ouvrant qui était dans la pile, on indique une erreur
				if (array_tag[0].replace(/\//, "") != node.tag) {
					errorMSG += "<p>Erreur - Le tag <strong>" + node.tag + "</strong> n'est pas fermé correctement.</p>";
					var tmp = node.tag;
					while ((node = node.parent) != null) {
						tmp = node.tag + " &rarr; " + tmp;
					}
					errorMSG += "<p>" + tmp + "</p>";
					// On peut indiquer un traitement spécial pour cette erreur, cependant, la meilleur conduite
					// à tenir (et la plus simple) consiste à arrêter le traitement et à renvoyer une valeur nulle.
					return null;
				}
				
				var remain = getRemainingCode(XMLCode);
				
				/*if (pile_tag.length == 1) {
					racine = pile_tag[0];
				}*/
				
				if (pile_tag.length > 0) {
					pile_tag[0].addContent(remain[0]);
					
					XMLCode = remain[1];
				}
						
				break;
			case SINGLE :
				// On ajoute juste le noeud au premier noeud de la pile et on continue
				var node = new Node(array_tag[0]);
				node.setAttributes(parseAttributes(array_tag[1]));
				pile_tag[0].addChild(node);
				
				var remain = getRemainingCode(XMLCode);
				
				pile_tag[0].addContent(remain[0]);
				
				XMLCode = remain[1];
				break;
		}
	}
	return racine;
}

/*
*	getNextTag - Retrouve la première balise dans le code XML
* paramètres	:
* 	XMLCode - le code XML où on cherche la balise
* retourne	:
* 	Un tableau t contenant :
*		t[0] - le nom du tag
*		t[1] - la chaine d'attributs de la balise
*		t[2] - le type de balise
*/
function getNextTag(XMLCode) {
	var pattTag = eval("/(<(.*?)( [^>]*)?\\/?>)/");
	
	if (XMLCode.search(pattTag) != -1) {
		var array = pattTag.exec(XMLCode);
		var tag_all = RegExp.$1;
		var tag = RegExp.$2;
		var attributes = RegExp.$3;
		var type;
		if (isHTML) {
			type = (isHTMLsingleTag(tag) ? SINGLE : testType(tag_all));
		} else {
			type = testType(tag_all);
		}

		return new Array(tag.toLowerCase(), attributes, type);
	} else {
		return new Array("","","");
	}
}

var OPENING = 0;
var CLOSING = 1;
var SINGLE = 2;

/*
*	testType - Trouve le type de balise auquel on a affaire
* paramètres	:
* 	string - le tag (ex <item/> ou <br> ou </toto>)
* retourne	:
*	un entier indiquant le type de balise (ouvrante, fermante, ou single)
*/
function testType(tag) {
	if (tag.indexOf("/") == 1) { // type : </tag>
		return CLOSING;
	} else if (tag.charAt(tag.length-2) == "/") { // type :  <tag/>
		return SINGLE;
	} else { // type : <tag>
		return OPENING;
	}
}

/*
*	parseAttributes - parse la chaine d'attributs d'une balise
* paramètres	:
* 	string - la chaine d'attribut
* retourne	:
* 	Un tableau associatif contenant de la forme t[nom_attr] = valeur_attr
*/
function parseAttributes(string) {
	var resultat = new Array();
	var pattAttr = /\s(.*?)=('|")(.*?)\2/g;
	
	var array_tmp = string.match(pattAttr);
	
	if (array_tmp) {
		for (var i = 0; i < array_tmp.length; i++) {
			if (array_tmp[i].search(pattAttr) != -1) {
				var attr = "" + RegExp.$1;
				var value = "" + RegExp.$3;
				resultat[attr] = value; 
			}		
		}
	}
	
	return resultat;
	
}


/*
*	getRemainingCode - Renvoie le contenu inclus entre la balise actuellement traitée et la prochaine balise
* paramètres	:
* 	string - le code XML commençant par la balise actuellemen traitée
* retourne	:
* 	Un tableau t contenant :
*		t[0] - le contenu entre les deux balises
*		t[1] - le reste du code XML
*/
function getRemainingCode(string) {
	var pattRemain = eval("/<[^>]*>([^<]*)(.*)/g");
	
	if (string.search(pattRemain) != -1) {
		return new Array(RegExp.$1, RegExp.$2);
	} else {
		return new Array();
	}
}

/*
*	removeXMLDeclarationTag - Enlève le tag de déclaration de document XML
* paramètres	:
*	XMLCode - le code XML à nettoyer
* retourne	:
*	une string contenant le code XML sans la déclaration
*/
function removeXMLDeclarationTag(XMLCode) {
	return XMLCode.replace(/<\?.*?\?>/g, "");
}

/*
*	removeDoctypeDeclarationTag - Enlève le tag de déclaration de DOCTYPE
* paramètres	:
*	XMLCode - le code XML à nettoyer
* retourne	:
*	une string contenant le code XML sans la déclaration
*/
function removeDoctypeDeclarationTag(XMLCode) {
	return XMLCode.replace(/<![^>]*>/g, "");
}

function removeScripts(XMLCode) {
	return XMLCode.replace(/<script.*?<\/script>/gi, "");
}

var pattHTMLTag = /^(br|img|input|hr|area|link|meta|param|base|basefont|col|frame)$/gi;

/*
*	isHTMLsingleTag - indique si un tag est un tag HTML sans balise fermante
* paramètres	:
*	tag - le nom du tag (ex : 'input' ou 'item')
* retourne	:
*	true si le tag est un tag HTML sans balise fermante
*	false sinon
*/
function isHTMLsingleTag(tag) {
	if (tag.search(pattHTMLTag) != -1) {
		return true;
	} else {
		return false;
	}
}


function Node(tag) {
	this.parent	= null;
	this.tag = tag;
	this.childrenList = new Array();
	this.attributesList = new Array();
	this.contentList = new Array();
}

Node.prototype.setAttributes = function(array_attr) {
	this.attributesList = array_attr;
};

Node.prototype.getAttributes = function(attr) {
	return this.attributesList;
};

Node.prototype.addChild = function(child) {
	this.childrenList.push(child);
	child.parent	= this;
};

Node.prototype.getChildren = function() {
	return this.childrenList;
};

Node.prototype.addContent = function(content) {
	this.contentList.push(content);
};

Node.prototype.getContents = function() {
	return this.contentList;
};

Node.prototype.display = function() {
	var chaine = "";
	chaine += "Noeud : <strong>" + this.tag + "</strong>";	
	
	var flag = false;
		
	for (var attr in this.attributesList) {
		(!flag ? chaine += "<br/>&nbsp;&nbsp;Attributs = " : true);
		flag= true;
		chaine += "<strong>" + attr + "</strong> &rarr; <em>" + this.attributesList[attr] + "</em> &bull; ";
	}

	chaine += "<ul>";
	
	for (var i = 0; i < this.childrenList.length; i++) {
		chaine += this.contentList[i];
		chaine += "<li>";
		chaine += this.childrenList[i].display();
		chaine += "</li>";
	}
	
	(this.contentList.length > 0 ? chaine += this.contentList[this.contentList.length-1] : true);
	
	chaine += "</ul>";
	
	return chaine;

};
