package snikket;
import haxe.DynamicAccess;
import haxe.Exception;
import haxe.ds.StringMap;
import Xml;
enum Node {
Element(stanza:Stanza);
CData(textNode:TextNode);
}
typedef NodeList = Array<Node>;
private interface NodeInterface {
public function serialize():String;
public function clone():NodeInterface;
public function traverse(f: (Stanza)->Bool):NodeInterface;
}
class TextNode implements NodeInterface {
public var content(default, null):String = "";
public function new (content:String) {
this.content = content;
}
public function serialize():String {
return Util.xmlEscape(content);
}
public function clone():TextNode {
return new TextNode(this.content);
}
public function traverse(f: (Stanza)->Bool) {
return this;
}
}
class StanzaError {
public var type:String;
public var condition:String;
public var text:Null<String>;
public function new(type_:String, condition_:String, ?text_:String) {
type = type_;
condition = condition_;
text = text_;
}
}
@:expose
class Stanza implements NodeInterface {
public var name(default, null):String = null;
public var attr(default, null):DynamicAccess<String> = {};
public var children(default, null):Array<Node> = [];
private var last_added(null, null):Stanza;
private var last_added_stack(null, null):Array<Stanza> = [];
public function new(name:String, ?attr:DynamicAccess<String>) {
this.name = name;
if(attr != null) {
this.attr = attr;
}
this.last_added = this;
};
public function serialize():String {
var el = Xml.createElement(name);
for (attr_k in this.attr.keys()) {
el.set(attr_k, this.attr.get(attr_k));
}
if (this.children.length == 0) {
return el.toString();
}
var serialized = el.toString();
var buffer = [serialized.substring(0, serialized.length-2)+">"];
for (child in children) {
buffer.push(switch (child) {
case Element(c): c.serialize();
case CData(c): c.serialize();
});
}
buffer.push("</"+this.name+">");
return buffer.join("");
}
public function toString():String {
return this.serialize();
}
public static function parse(s:String):Stanza {
return fromXml(Xml.parse(s));
}
@:allow(snikket)
@:allow(test)
private static function fromXml(el:Xml):Stanza {
if(el.nodeType == XmlType.Document) {
return fromXml(el.firstElement());
}
var attrs: DynamicAccess<String> = {};
for (a in el.attributes()) {
attrs.set(a, el.get(a));
}
var stanza = new Stanza(el.nodeName, attrs);
for (child in el) {
if(child.nodeType == XmlType.Element) {
stanza.addChild(fromXml(child));
} else if (child.nodeType == XmlType.ProcessingInstruction || child.nodeType == XmlType.DocType || child.nodeType == XmlType.Comment) {
// Ignore non-operative XML items
} else {
stanza.text(child.nodeValue);
}
}
return stanza;
}
public function tag(name:String, ?attr:DynamicAccess<String>) {
var child = new Stanza(name, attr);
this.last_added.addDirectChild(Element(child));
this.last_added_stack.push(this.last_added);
this.last_added = child;
return this;
}
public function text(content:String) {
this.last_added.addDirectChild(CData(new TextNode(content)));
return this;
}
public function textTag(tagName:String, textContent:String, ?attr:DynamicAccess<String>) {
this.last_added.addDirectChild(Element(new Stanza(tagName, attr ?? {}).text(textContent)));
return this;
}
public function up() {
if(this.last_added != this) {
this.last_added = this.last_added_stack.pop();
}
return this;
}
public function reset():Stanza {
this.last_added = this;
return this;
}
@:allow(snikket)
private function addChildren(children:Iterable<Stanza>) {
for (child in children) {
addChild(child);
}
return this;
}
@:allow(snikket)
private function addChildNodes(children:Iterable<Node>) {
for (child in children) {
addDirectChild(child);
}
return this;
}
public function addChild(stanza:Stanza) {
this.last_added.children.push(Element(stanza));
return this;
}
public function addDirectChild(child:Node) {
this.children.push(child);
return this;
}
public function clone():Stanza {
var clone = new Stanza(this.name, this.attr);
for (child in children) {
clone.addDirectChild(switch(child) {
case Element(c): Element(c.clone());
case CData(c): CData(c.clone());
});
}
return clone;
}
public function allTags(?name:String, ?xmlns:String):Array<Stanza> {
var tags = this.children
.filter((child) -> child.match(Element(_)))
.map(function (child:Node) {
return switch(child) {
case Element(c): c;
case _: null;
};
});
if (name != null || xmlns != null) {
var ourXmlns = this.attr.get("xmlns");
tags = tags.filter(function (child:Stanza):Bool {
var childXmlns = child.attr.get("xmlns");
return ((name == null || child.name == name)
&& ((xmlns == null && (ourXmlns == childXmlns || childXmlns == null))
|| childXmlns == xmlns));
});
}
return tags;
}
public function allText():Array<String> {
return this.children
.filter((child) -> child.match(CData(_)))
.map(function (child:Node) {
return switch(child) {
case CData(c): c.content;
case _: null;
};
});
}
public function getFirstChild():Stanza {
return allTags()[0];
}
public function getChildren():Array<NodeInterface> {
return children.map(child -> switch(child) {
case Element(el): el;
case CData(text): text;
});
}
public function getChild(?name:Null<String>, ?xmlns:Null<String>):Null<Stanza> {
var ourXmlns = this.attr.get("xmlns");
/*
for (child in allTags()) {
if (name == null || child.name == name
&& ((xmlns == null && ourXmlns == child.attr.get("xmlns"))
|| child.attr.get("xmlns") == xmlns)) {
return child;
}
}*/
var tags = allTags(name, xmlns);
if(tags.length == 0) {
return null;
}
return tags[0];
}
public function getChildText(?name:Null<String>, ?xmlns:Null<String>):String {
var child = getChild(name, xmlns);
if(child == null) {
return null;
}
return child.getText();
}
public function hasChild(?name:Null<String>, ?xmlns:Null<String>):Bool {
return getChild(name, xmlns) != null;
}
public function getText():String {
return allText().join("");
}
public function find(path:String): Null<Node> {
var pos = 0;
var len = path.length;
var cursor = this;
do {
var xmlns = null, name = null, text = null;
var char = path.charAt(pos);
if (char == "@") {
return CData(new TextNode(cursor.attr.get(path.substr(pos+1))));
} else if (char == "{") {
xmlns = path.substring(pos+1, path.indexOf("}", pos+1));
pos += xmlns.length + 2;
}
var reName = new EReg("([^@/#]*)([/#]?)", "");
if(!reName.matchSub(path, pos)) {
throw new Exception("Invalid path to Stanza.find(): "+path);
}
var name = reName.matched(1), text = reName.matched(2);
pos = reName.matchedPos().pos + reName.matchedPos().len;
if(name == "") {
name = null;
};
if(pos == len) {
if(text == "#") {
var text = cursor.getChildText(name, xmlns);
if(text == null) {
return null;
}
return CData(new TextNode(text));
}
return Element(cursor.getChild(name, xmlns));
}
cursor = cursor.getChild(name, xmlns);
} while (cursor != null);
return null;
}
public function findChild(path:String):Stanza {
var result = find(path);
if(result == null) {
return null;
}
return switch(result) {
case Element(stanza): stanza;
case _: null;
};
}
public function findText(path:String):Null<String> {
var result = find(path);
if(result == null) {
return null;
}
return switch(result) {
case CData(textNode): textNode.content;
case _: null;
};
}
public function traverse(f: (Stanza)->Bool) {
if (!f(this)) {
for (child in allTags()) {
child.traverse(f);
}
}
return this;
}
public function getError():Null<StanzaError> {
final errorTag = this.getChild("error");
if(errorTag == null) {
return null;
}
return new StanzaError(
errorTag.attr.get("type"),
errorTag.getChild(null, "urn:ietf:params:xml:ns:xmpp-stanzas")?.name,
errorTag.getChildText("text", "urn:ietf:params:xml:ns:xmpp-stanzas")
);
}
public function removeChildren(?name: String, ?xmlns_:String):Void {
final xmlns = xmlns_??attr.get("xmlns");
children = children.filter((child:Node) -> {
switch(child) {
case Element(c):
return !( (name == null || c.name == name) && c.attr.get("xmlns")??xmlns == xmlns);
default:
return true;
}
});
}
static public function parseXmlBool(x:String) {
return x == "true" || x == "1";
}
}
enum IqRequestType {
Get;
Set;
}