////////////////////////////// VISITORS // _assign is like _.extend or the upcoming Object.assign. // Copy src's own, enumerable properties onto tgt and return // tgt. var _hasOwnProperty = Object.prototype.hasOwnProperty; var _assign = function (tgt, src) { for (var k in src) { if (_hasOwnProperty.call(src, k)) tgt[k] = src[k]; } return tgt; }; HTML.Visitor = function (props) { _assign(this, props); }; HTML.Visitor.def = function (options) { _assign(this.prototype, options); }; HTML.Visitor.extend = function (options) { var curType = this; var subType = function HTMLVisitorSubtype(/*arguments*/) { HTML.Visitor.apply(this, arguments); }; subType.prototype = new curType; subType.extend = curType.extend; subType.def = curType.def; if (options) _assign(subType.prototype, options); return subType; }; HTML.Visitor.def({ visit: function (content/*, ...*/) { if (content == null) // null or undefined. return this.visitNull.apply(this, arguments); if (typeof content === 'object') { if (content.htmljsType) { switch (content.htmljsType) { case HTML.Tag.htmljsType: return this.visitTag.apply(this, arguments); case HTML.CharRef.htmljsType: return this.visitCharRef.apply(this, arguments); case HTML.Comment.htmljsType: return this.visitComment.apply(this, arguments); case HTML.Raw.htmljsType: return this.visitRaw.apply(this, arguments); default: throw new Error("Unknown htmljs type: " + content.htmljsType); } } if (HTML.isArray(content)) return this.visitArray.apply(this, arguments); return this.visitObject.apply(this, arguments); } else if ((typeof content === 'string') || (typeof content === 'boolean') || (typeof content === 'number')) { return this.visitPrimitive.apply(this, arguments); } else if (typeof content === 'function') { return this.visitFunction.apply(this, arguments); } throw new Error("Unexpected object in htmljs: " + content); }, visitNull: function (nullOrUndefined/*, ...*/) {}, visitPrimitive: function (stringBooleanOrNumber/*, ...*/) {}, visitArray: function (array/*, ...*/) {}, visitComment: function (comment/*, ...*/) {}, visitCharRef: function (charRef/*, ...*/) {}, visitRaw: function (raw/*, ...*/) {}, visitTag: function (tag/*, ...*/) {}, visitObject: function (obj/*, ...*/) { throw new Error("Unexpected object in htmljs: " + obj); }, visitFunction: function (obj/*, ...*/) { throw new Error("Unexpected function in htmljs: " + obj); } }); HTML.TransformingVisitor = HTML.Visitor.extend(); HTML.TransformingVisitor.def({ visitNull: IDENTITY, visitPrimitive: IDENTITY, visitArray: function (array/*, ...*/) { var argsCopy = SLICE.call(arguments); var result = array; for (var i = 0; i < array.length; i++) { var oldItem = array[i]; argsCopy[0] = oldItem; var newItem = this.visit.apply(this, argsCopy); if (newItem !== oldItem) { // copy `array` on write if (result === array) result = array.slice(); result[i] = newItem; } } return result; }, visitComment: IDENTITY, visitCharRef: IDENTITY, visitRaw: IDENTITY, visitObject: IDENTITY, visitFunction: IDENTITY, visitTag: function (tag/*, ...*/) { var oldChildren = tag.children; var argsCopy = SLICE.call(arguments); argsCopy[0] = oldChildren; var newChildren = this.visitChildren.apply(this, argsCopy); var oldAttrs = tag.attrs; argsCopy[0] = oldAttrs; var newAttrs = this.visitAttributes.apply(this, argsCopy); if (newAttrs === oldAttrs && newChildren === oldChildren) return tag; var newTag = HTML.getTag(tag.tagName).apply(null, newChildren); newTag.attrs = newAttrs; return newTag; }, visitChildren: function (children/*, ...*/) { return this.visitArray.apply(this, arguments); }, // Transform the `.attrs` property of a tag, which may be a dictionary, // an array, or in some uses, a foreign object (such as // a template tag). visitAttributes: function (attrs/*, ...*/) { if (HTML.isArray(attrs)) { var argsCopy = SLICE.call(arguments); var result = attrs; for (var i = 0; i < attrs.length; i++) { var oldItem = attrs[i]; argsCopy[0] = oldItem; var newItem = this.visitAttributes.apply(this, argsCopy); if (newItem !== oldItem) { // copy on write if (result === attrs) result = attrs.slice(); result[i] = newItem; } } return result; } if (attrs && HTML.isConstructedObject(attrs)) { throw new Error("The basic HTML.TransformingVisitor does not support " + "foreign objects in attributes. Define a custom " + "visitAttributes for this case."); } var oldAttrs = attrs; var newAttrs = oldAttrs; if (oldAttrs) { var attrArgs = [null, null]; attrArgs.push.apply(attrArgs, arguments); for (var k in oldAttrs) { var oldValue = oldAttrs[k]; attrArgs[0] = k; attrArgs[1] = oldValue; var newValue = this.visitAttribute.apply(this, attrArgs); if (newValue !== oldValue) { // copy on write if (newAttrs === oldAttrs) newAttrs = _assign({}, oldAttrs); newAttrs[k] = newValue; } } } return newAttrs; }, // Transform the value of one attribute name/value in an // attributes dictionary. visitAttribute: function (name, value, tag/*, ...*/) { var args = SLICE.call(arguments, 2); args[0] = value; return this.visit.apply(this, args); } }); HTML.ToTextVisitor = HTML.Visitor.extend(); HTML.ToTextVisitor.def({ visitNull: function (nullOrUndefined) { return ''; }, visitPrimitive: function (stringBooleanOrNumber) { var str = String(stringBooleanOrNumber); if (this.textMode === HTML.TEXTMODE.RCDATA) { return str.replace(/&/g, '&').replace(/`), we hackishly support HTML tags in markdown // in templates by parsing them and stringifying them. return this.visit(this.toHTML(tag)); }, visitObject: function (x) { throw new Error("Unexpected object in htmljs in toText: " + x); }, toHTML: function (node) { return HTML.toHTML(node); } }); HTML.ToHTMLVisitor = HTML.Visitor.extend(); HTML.ToHTMLVisitor.def({ visitNull: function (nullOrUndefined) { return ''; }, visitPrimitive: function (stringBooleanOrNumber) { var str = String(stringBooleanOrNumber); return str.replace(/&/g, '&').replace(/'; }, visitCharRef: function (charRef) { return charRef.html; }, visitRaw: function (raw) { return raw.value; }, visitTag: function (tag) { var attrStrs = []; var tagName = tag.tagName; var children = tag.children; var attrs = tag.attrs; if (attrs) { attrs = HTML.flattenAttributes(attrs); for (var k in attrs) { if (k === 'value' && tagName === 'textarea') { children = [attrs[k], children]; } else { var v = this.toText(attrs[k], HTML.TEXTMODE.ATTRIBUTE); attrStrs.push(' ' + k + '="' + v + '"'); } } } var startTag = '<' + tagName + attrStrs.join('') + '>'; var childStrs = []; var content; if (tagName === 'textarea') { for (var i = 0; i < children.length; i++) childStrs.push(this.toText(children[i], HTML.TEXTMODE.RCDATA)); content = childStrs.join(''); if (content.slice(0, 1) === '\n') // TEXTAREA will absorb a newline, so if we see one, add // another one. content = '\n' + content; } else { for (var i = 0; i < children.length; i++) childStrs.push(this.visit(children[i])); content = childStrs.join(''); } var result = startTag + content; if (children.length || ! HTML.isVoidElement(tagName)) { // "Void" elements like BR are the only ones that don't get a close // tag in HTML5. They shouldn't have contents, either, so we could // throw an error upon seeing contents here. result += ''; } return result; }, visitObject: function (x) { throw new Error("Unexpected object in htmljs in toHTML: " + x); }, toText: function (node, textMode) { return HTML.toText(node, textMode); } });