/** * @ignore * represent tag, it can nest other tag * @author yiminghe@gmail.com */ KISSY.add("html-parser/nodes/tag", function (S, Node, Attribute, Dtd) { function createTag(self, tagName, attrs) { self.nodeName = self.tagName = tagName.toLowerCase(); self._updateSelfClosed(); S.each(attrs, function (v, n) { self.setAttribute(n, v); }); } /** * Html Tag Class * @param page * @param startPosition * @param endPosition * @param attributes * @class KISSY.HtmlParser.Tag */ function Tag(page, startPosition, endPosition, attributes) { var self = this; self.childNodes = []; self.firstChild = null; self.lastChild = null; self.attributes = attributes || []; self.nodeType = 1; if (typeof page == 'string') { createTag.apply(null, [self].concat(S.makeArray(arguments))); } else { Tag.superclass.constructor.apply(self, arguments); attributes = self.attributes; // first attribute is actually nodeName if (attributes[0]) { self.nodeName = attributes[0].name.toLowerCase(); // end tag (</div>) is a tag too in lexer , but not exist in parsed dom tree self.tagName = self.nodeName.replace(/\//, ""); self._updateSelfClosed(); attributes.splice(0, 1); } var lastAttr = attributes[attributes.length - 1], lastSlash = !!(lastAttr && /\/$/.test(lastAttr.name)); if (lastSlash) { attributes.length = attributes.length - 1; } // self-closing flag self.isSelfClosed = self.isSelfClosed || lastSlash; // whether has been closed by its end tag // !TODO how to set closed position correctly self['closed'] = self.isSelfClosed; } self['closedStartPosition'] = -1; self['closedEndPosition'] = -1; } function refreshChildNodes(self) { var c = self.childNodes; self.firstChild = c[0]; self.lastChild = c[c.length - 1]; if (c.length >= 1) { c[0].nextSibling = c[0].nextSibling = null; c[0].parentNode = self; } if (c.length > 1) { for (var i = 0; i < c.length - 1; i++) { c[i].nextSibling = c[i + 1]; c[i + 1].previousSibling = c[i]; c[i + 1].parentNode = self; } c[c.length - 1].nextSibling = null } } S.extend(Tag, Node, { _updateSelfClosed: function () { var self = this; // <br> <img> <input> , just recognize them immediately self.isSelfClosed = !!(Dtd.$empty[self.nodeName]); if (!self.isSelfClosed) { self.isSelfClosed = /\/$/.test(self.nodeName); } self['closed'] = self.isSelfClosed; }, clone: function () { var ret = new Tag(), attrs = []; S.each(this.attributes, function (a) { attrs.push(a.clone()); }); S.mix(ret, { childNodes: [], firstChild: null, lastChild: null, attributes: attrs, nodeType: this.nodeType, nodeName: this.nodeName, tagName: this.tagName, isSelfClosed: this.isSelfClosed, closed: this.closed, closedStartPosition: this.closedStartPosition, closedEndPosition: this.closedEndPosition }); return ret; }, setTagName: function (v) { var self = this; self.nodeName = self.tagName = v; if (v) { self._updateSelfClosed(); } }, equals: function (tag) { if (!tag || this.nodeName != tag.nodeName) { return 0; } if (this.nodeType != tag.nodeType) { return 0; } if (this.attributes.length != tag.attributes.length) { return 0; } for (var i = 0; i < this.attributes.length; i++) { if (!this.attributes[i].equals(tag.attributes[i])) { return 0; } } return 1; }, isEndTag: function () { return /^\//.test(this.nodeName); }, appendChild: function (node) { this.childNodes.push(node); refreshChildNodes(this); }, replace: function (ref) { var silbing = ref.parentNode.childNodes, index = S.indexOf(ref, silbing); silbing[index] = this; refreshChildNodes(ref.parentNode); }, replaceChild: function (newC, refC) { var self = this, childNodes = self.childNodes; var index = S.indexOf(refC, childNodes); childNodes[index] = newC; refreshChildNodes(self); }, prepend: function (node) { this.childNodes.unshift(node); refreshChildNodes(this); }, insertBefore: function (ref) { var silbing = ref.parentNode.childNodes, index = S.indexOf(ref, silbing); silbing.splice(index, 0, this); refreshChildNodes(ref.parentNode); }, insertAfter: function (ref) { var silbing = ref.parentNode.childNodes, index = S.indexOf(ref, silbing); if (index == silbing.length - 1) { ref.parentNode.appendChild(this); } else { this.insertBefore(ref.parentNode.childNodes[[index + 1]]); } }, empty: function () { this.childNodes = []; refreshChildNodes(this); }, removeChild: function (node) { var silbing = node.parentNode.childNodes, index = S.indexOf(node, silbing); silbing.splice(index, 1); refreshChildNodes(node.parentNode); }, getAttribute: function (name) { var attr = findAttributeByName(this.attributes, name); return attr && attr.value; }, setAttribute: function (name, value) { var attr = findAttributeByName(this.attributes, name); if (attr) { attr.value = value; } else { this.attributes.push(new Attribute(name, '=', value, '"')); } }, removeAttribute: function (name) { var attr = findAttributeByName(this.attributes, name); if (attr) { var index = S.indexOf(attr, this.attributes); this.attributes.splice(index, 1); } }, /** * give root node a chance to filter children first */ filterChildren: function () { var self = this; if (!self.isChildrenFiltered) { var writer = new (S.require('html-parser/writer/basic'))(); self._writeChildrenHTML(writer); var parser = new (S.require('html-parser/parser'))(writer.getHtml()), children = parser.parse().childNodes; self.empty(); S.each(children, function (c) { self.appendChild(c); }); self.isChildrenFiltered = 1; } }, /** * serialize tag to html string in writer * @param writer * @param filter */ writeHtml: function (writer, filter) { var el = this, tmp, attrName, tagName = el.tagName; // special treat for doctype if (tagName == "!doctype") { writer.append(this.toHtml() + "\n"); return; } el.__filter = filter; el.isChildrenFiltered = 0; // process its open tag if (filter) { // element filtered by its name directly if (!(tagName = filter.onTagName(tagName))) { return; } el.tagName = tagName; tmp = filter.onTag(el); if (tmp === false) { return; } // replaced if (tmp) { el = tmp; } // replaced by other type of node if (el.nodeType !== 1) { el.writeHtml(writer, filter); return; } // preserve children but delete itself if (!el.tagName) { el._writeChildrenHTML(writer); return; } } writer.openTag(el); // process its attributes var attributes = el.attributes; for (var i = 0; i < attributes.length; i++) { var attr = attributes[i]; attrName = attr.name; if (filter) { // filtered directly by name if (!(attrName = filter.onAttributeName(attrName, el))) { continue; } attr.name = attrName; // filtered by value and node if (filter.onAttribute(attr, el) === false) { continue; } } writer.attribute(attr, el); } // close its open tag writer.openTagClose(el); if (!el.isSelfClosed) { el._writeChildrenHTML(writer); // process its close tag writer.closeTag(el); } }, /** * @param writer * @protected */ _writeChildrenHTML: function (writer) { var self = this, filter = self.isChildrenFiltered ? 0 : self.__filter; // process its children recursively S.each(self.childNodes, function (child) { child.writeHtml(writer, filter); }); } }); function findAttributeByName(attributes, name) { if (attributes && attributes.length) { for (var i = 0; i < attributes.length; i++) { if (attributes[i].name == name) { return attributes[i]; } } } return null; } return Tag; }, { requires: ['./node', './attribute', '../dtd'] });