mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
332 lines
9.8 KiB
JavaScript
332 lines
9.8 KiB
JavaScript
////////////////////////////// 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(/</g, '<');
|
|
} else if (this.textMode === HTML.TEXTMODE.ATTRIBUTE) {
|
|
// escape `&` and `"` this time, not `&` and `<`
|
|
return str.replace(/&/g, '&').replace(/"/g, '"');
|
|
} else {
|
|
return str;
|
|
}
|
|
},
|
|
visitArray: function (array) {
|
|
var parts = [];
|
|
for (var i = 0; i < array.length; i++)
|
|
parts.push(this.visit(array[i]));
|
|
return parts.join('');
|
|
},
|
|
visitComment: function (comment) {
|
|
throw new Error("Can't have a comment here");
|
|
},
|
|
visitCharRef: function (charRef) {
|
|
if (this.textMode === HTML.TEXTMODE.RCDATA ||
|
|
this.textMode === HTML.TEXTMODE.ATTRIBUTE) {
|
|
return charRef.html;
|
|
} else {
|
|
return charRef.str;
|
|
}
|
|
},
|
|
visitRaw: function (raw) {
|
|
return raw.value;
|
|
},
|
|
visitTag: function (tag) {
|
|
// Really we should just disallow Tags here. However, at the
|
|
// moment it's useful to stringify any HTML we find. In
|
|
// particular, when you include a template within `{{#markdown}}`,
|
|
// we render the template as text, and since there's currently
|
|
// no way to make the template be *parsed* as text (e.g. `<template
|
|
// type="text">`), 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(/</g, '<');
|
|
},
|
|
visitArray: function (array) {
|
|
var parts = [];
|
|
for (var i = 0; i < array.length; i++)
|
|
parts.push(this.visit(array[i]));
|
|
return parts.join('');
|
|
},
|
|
visitComment: function (comment) {
|
|
return '<!--' + comment.sanitizedValue + '-->';
|
|
},
|
|
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 += '</' + tagName + '>';
|
|
}
|
|
|
|
return result;
|
|
},
|
|
visitObject: function (x) {
|
|
throw new Error("Unexpected object in htmljs in toHTML: " + x);
|
|
},
|
|
toText: function (node, textMode) {
|
|
return HTML.toText(node, textMode);
|
|
}
|
|
});
|