diff --git a/docs/client/api.html b/docs/client/api.html index 9b1e2b4e89..5e8a7b30f6 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2036,14 +2036,7 @@ Example: When you write a template as `<{{! }}template name="foo"> ... <{{! }}/template>` in an HTML file in your app, Meteor generates a -"component object" named `Template.foo`. - -{{#note}} - Meteor's component API is currently in flux. This section documents a few - features of the component object that are useful for writing apps; a future - release will elaborate more about how components work and about how to build - components that aren't just template. -{{/note}} +"template object" named `Template.foo`. The same template may occur many times on a page, and these occurrences are called template instances. Template instances have a @@ -2214,30 +2207,35 @@ You can define helpers and event maps on `UI.body` just like on any {{> api_box ui_render}} -This returns an "instantiated component" object, which can be passed to -[`UI.insert`](#ui_insert). The template's [`created`](#template_created) callback -will be invoked. The component will continue to be updated reactively as the -data used changes. +This returns an "rendered template" object, which can be passed to +[`UI.insert`](#ui_insert). The template's +[`created`](#template_created) callback will be invoked. The rendered +template will continue to be updated reactively as the data used +changes. {{#warning}} - Future releases will provide a richer API for "instantiated components" - (probably unifying them with "template instances"). For now, all you can - do with them is pass them to `UI.insert`. + Future releases will provide a richer API for working with rendered + templates, for example unifying them "template instances." For now, all you + can do with them is pass them to `UI.insert`. +{{/warning}} - Most users will not need to manually render components or manually insert them - into the DOM at all. As of 0.8.0, if you call `UI.render` and never insert - the result into the DOM, the logic to keep the instantiated component updated +{{#warning}} + Most users will not need to manually render templates or manually insert them + into the DOM at all. As of 0.8.x, if you call `UI.render` and never insert + the result into the DOM, the logic to keep the rendered template updated will continue running in your browser forever. Additionally, if you remove any - part of your DOM using any mechanism other than jQuery, the logic to keep that - part of the the DOM updated will continue running. To avoid these issues, - either avoid directly updating the DOM or ensure that any removals go through - jQuery. + part of your DOM using any mechanism other than Meteor or jQuery, the logic + to keep that part of the the DOM updated will continue running. To avoid these + issues, either avoid directly updating the DOM or ensure that any removals go + through Meteor or jQuery. {{/warning}} {{> api_box ui_renderwithdata}} {{> api_box ui_insert}} +{{> api_box ui_remove}} + {{> api_box ui_getelementdata}} diff --git a/docs/client/api.js b/docs/client/api.js index b389f4fad9..a4c9a3a828 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -1866,7 +1866,7 @@ Template.api.ui_body = { id: "ui_body", name: "UI.body", locus: "Client", - descr: ["The [component object](#templates_api) representing your `
` tag."] + descr: ["The [template object](#templates_api) representing your `` tag."] }; Template.api.ui_render = { @@ -1899,12 +1899,12 @@ Template.api.ui_renderwithdata = { Template.api.ui_insert = { id: "ui_insert", - name: "UI.insert(instantiatedComponent, parentNode[, nextNode])", + name: "UI.insert(renderedTemplate, parentNode[, nextNode])", locus: "Client", - descr: ["Inserts an instantiated component into the DOM and calls its [`rendered`](#template_rendered) callback."], + descr: ["Inserts a rendered template into the DOM and calls its [`rendered`](#template_rendered) callback."], args: [ - {name: "instantiatedComponent", - type: "Instantiated component object", + {name: "renderedTemplate", + type: "Rendered template object", descr: "The return value from `UI.render` or `UI.renderWithData`." }, {name: "parentNode", @@ -1917,6 +1917,19 @@ Template.api.ui_insert = { }] }; +Template.api.ui_remove = { + id: "ui_remove", + name: "UI.remove(renderedTemplate)", + locus: "Client", + descr: ["Removes a rendered template from the DOM and destroys it, calling the [`destroyed`](#template_destroyed) callback and stopping the logic that reactively updates the template."], + args: [ + {name: "renderedTemplate", + type: "Rendered template object", + descr: "The return value from `UI.render` or `UI.renderWithData`." + } + ] +}; + Template.api.ui_getelementdata = { id: "ui_getelementdata", name: "UI.getElementData(el)", diff --git a/docs/client/docs.js b/docs/client/docs.js index fdcdf640d8..bb3fc8eec8 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -261,6 +261,7 @@ var toc = [ "UI.render", "UI.renderWithData", "UI.insert", + "UI.remove", "UI.getElementData" ], {type: "spacer"}, diff --git a/examples/unfinished/blaze-test/.meteor/.gitignore b/examples/unfinished/blaze-test/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/examples/unfinished/blaze-test/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/examples/unfinished/blaze-test/.meteor/packages b/examples/unfinished/blaze-test/.meteor/packages new file mode 100644 index 0000000000..7f72e45268 --- /dev/null +++ b/examples/unfinished/blaze-test/.meteor/packages @@ -0,0 +1,11 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +standard-app-packages +autopublish +insecure +blaze +htmljs +blaze-tools diff --git a/examples/unfinished/blaze-test/.meteor/release b/examples/unfinished/blaze-test/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/examples/unfinished/blaze-test/.meteor/release @@ -0,0 +1 @@ +none diff --git a/examples/unfinished/blaze-test/client/blaze-test.css b/examples/unfinished/blaze-test/client/blaze-test.css new file mode 100644 index 0000000000..b6b4052b43 --- /dev/null +++ b/examples/unfinished/blaze-test/client/blaze-test.css @@ -0,0 +1 @@ +/* CSS declarations go here */ diff --git a/examples/unfinished/blaze-test/client/blaze-test.html b/examples/unfinished/blaze-test/client/blaze-test.html new file mode 100644 index 0000000000..4cbb558b2e --- /dev/null +++ b/examples/unfinished/blaze-test/client/blaze-test.html @@ -0,0 +1,7 @@ + +")); + succeed('', TEXTAREA({value: "asdf"})); + succeed('', TEXTAREA({x: "y", value: "asdf"})); + succeed('', TEXTAREA({value: "
"}));
succeed('',
- TEXTAREA("a", CharRef({html: '&', str: '&'}), "b"));
- succeed('', TEXTAREA("', TEXTAREA({value: "\n', TEXTAREA());
- succeed('', TEXTAREA("asdf"));
- succeed('', TEXTAREA("\nasdf"));
- succeed('', TEXTAREA("\n"));
- succeed('', TEXTAREA("asdf\n"));
- succeed('', TEXTAREA(""));
- succeed('', TEXTAREA("asdf"));
+ succeed('', TEXTAREA({value: "asdf"}));
+ succeed('', TEXTAREA({value: "\nasdf"}));
+ succeed('', TEXTAREA({value: "\n"}));
+ succeed('', TEXTAREA({value: "asdf\n"}));
+ succeed('', TEXTAREA({value: ""}));
+ succeed('', TEXTAREA({value: "asdf"}));
fatal('');
- succeed('
', BR({x:'y'}));
succeed('', Comment('\n'));
succeed('', Comment('\n'));
- succeed('
', BR({x:'\n\n'}));
succeed('
', BR({x:'\n\n'}));
succeed('
', BR({x:'y'}));
@@ -148,8 +149,8 @@ Tinytest.add("html-tools - parser getContent", function (test) {
});
Tinytest.add("html-tools - parseFragment", function (test) {
- test.equal(HTML.toJS(HTMLTools.parseFragment("
Hello
Hello
` tag or `HTML.INPUT` for an `` tag. The
+resulting object is `instanceof HTML.Tag`. (The `HTML.Tag`
+constructor should not be called directly.)
+
+Tag constructors take an optional attributes dictionary followed
+by zero or more children:
```
-var amp = HTML.CharRef({html: '&', str: '&'});
+HTML.HR()
-HTML.toHTML(HTML.SPAN({title: ['M', amp, 'Ms']},
- 'M', amp, 'M candies'))
+HTML.DIV(HTML.P("First paragraph"),
+ HTML.P("Second paragraph"))
+
+HTML.INPUT({type: "text"})
+
+HTML.SPAN({'class': "foo"}, "Some text")
```
-```
-M&M candies
-```
+### Instance properties
-A comment looks like `HTML.Comment("value here")`, where the value
-should not contain two consecutive hyphen (`-`) characters or an
-initial or final hyphen (or they will be stripped out).
+Tags have the following properties:
-A "raw" object like `HTML.Raw(" ` tag or `HTML.INPUT` for an `` tag. The
+ * resulting object is `instanceof HTML.Tag`. (The `HTML.Tag`
+ * constructor should not be called directly.)
+ *
+ * Tag constructors take an optional attributes dictionary followed
+ * by zero or more children:
+ *
+ * ```
+ * HTML.HR()
+ *
+ * HTML.DIV(HTML.P("First paragraph"),
+ * HTML.P("Second paragraph"))
+ *
+ * HTML.INPUT({type: "text"})
+ *
+ * HTML.SPAN({'class': "foo"}, "Some text")
+ * ```
+ *
+ * ### Instance properties
+ *
+ * Tags have the following properties:
+ *
+ * * `tagName` - The tag name in lowercase (or camelCase)
+ * * `children` - An array of children (always present)
+ * * `attrs` - An attributes dictionary, `null`, or an array (see below)
+ */
+
-// Tag instances are `instanceof HTML.Tag`.
-//
-// Tag objects should be considered immutable.
-//
-// This is a private constructor of an abstract class; don't call it.
HTML.Tag = function () {};
HTML.Tag.prototype.tagName = ''; // this will be set per Tag subclass
HTML.Tag.prototype.attrs = null;
HTML.Tag.prototype.children = Object.freeze ? Object.freeze([]) : [];
-
-// Given "p", create and assign `HTML.P` if it doesn't already exist.
-// Then return it. `tagName` must have proper case (usually all lowercase).
-HTML.getTag = function (tagName) {
- var symbolName = HTML.getSymbolName(tagName);
- if (symbolName === tagName) // all-caps tagName
- throw new Error("Use the lowercase or camelCase form of '" + tagName + "' here");
-
- if (! HTML[symbolName])
- HTML[symbolName] = makeTagConstructor(tagName);
-
- return HTML[symbolName];
-};
-
-// Given "p", make sure `HTML.P` exists. `tagName` must have proper case
-// (usually all lowercase).
-HTML.ensureTag = function (tagName) {
- HTML.getTag(tagName); // don't return it
-};
+HTML.Tag.prototype.htmljsType = HTML.Tag.htmljsType = ['Tag'];
// Given "p" create the function `HTML.P`.
var makeTagConstructor = function (tagName) {
@@ -39,18 +109,29 @@ var makeTagConstructor = function (tagName) {
var i = 0;
var attrs = arguments.length && arguments[0];
- if (attrs && (typeof attrs === 'object') &&
- (attrs.constructor === Object)) {
- instance.attrs = attrs;
- i++;
+ if (attrs && (typeof attrs === 'object')) {
+ // Treat vanilla JS object as an attributes dictionary.
+ if (! HTML.isConstructedObject(attrs)) {
+ instance.attrs = attrs;
+ i++;
+ } else if (attrs instanceof HTML.Attrs) {
+ var array = attrs.value;
+ if (array.length === 1) {
+ instance.attrs = array[0];
+ } else if (array.length > 1) {
+ instance.attrs = array;
+ }
+ i++;
+ }
}
+
// If no children, don't create an array at all, use the prototype's
// (frozen, empty) array. This way we don't create an empty array
// every time someone creates a tag without `new` and this constructor
// calls itself with no arguments (above).
if (i < arguments.length)
- instance.children = Array.prototype.slice.call(arguments, i);
+ instance.children = SLICE.call(arguments, i);
return instance;
};
@@ -61,46 +142,195 @@ var makeTagConstructor = function (tagName) {
return HTMLTag;
};
-var CharRef = HTML.CharRef = function (attrs) {
- if (! (this instanceof CharRef))
- // called without `new`
- return new CharRef(attrs);
+/**
+ * ### Special forms of attributes
+ *
+ * The attributes of a Tag may be an array of dictionaries. In order
+ * for a tag constructor to recognize an array as the attributes argument,
+ * it must be written as `HTML.Attrs(attrs1, attrs2, ...)`, as in this
+ * example:
+ *
+ * ```
+ * var extraAttrs = {'class': "container"};
+ *
+ * var div = HTML.DIV(HTML.Attrs({id: "main"}, extraAttrs),
+ * "This is the content.");
+ *
+ * div.attrs // => [{id: "main"}, {'class': "container"}]
+ * ```
+ *
+ * `HTML.Attrs` may also be used to pass a foreign object in place of
+ * an attributes dictionary of a tag.
+ *
+ */
+// Not an HTMLjs node, but a wrapper to pass multiple attrs dictionaries
+// to a tag (for the purpose of implementing dynamic attributes).
+var Attrs = HTML.Attrs = function (/*attrs dictionaries*/) {
+ // Work with or without `new`. If not called with `new`,
+ // perform instantiation by recursively calling this constructor.
+ // We can't pass varargs, so pass no args.
+ var instance = (this instanceof Attrs) ? this : new Attrs;
- if (! (attrs && attrs.html && attrs.str))
- throw new Error(
- "HTML.CharRef must be constructed with ({html:..., str:...})");
+ instance.value = SLICE.call(arguments);
- this.html = attrs.html;
- this.str = attrs.str;
+ return instance;
};
-var Comment = HTML.Comment = function (value) {
- if (! (this instanceof Comment))
- // called without `new`
- return new Comment(value);
+/**
+ * ### Normalized Case for Tag Names
+ *
+ * The `tagName` field is always in "normalized case," which is the
+ * official case for that particular element name (usually lowercase).
+ * For example, `HTML.DIV().tagName` is `"div"`. For some elements
+ * used in inline SVG graphics, the correct case is "camelCase." For
+ * example, there is an element named `clipPath`.
+ *
+ * Web browsers have a confusing policy about case. They perform case
+ * normalization when parsing HTML, but not when creating SVG elements
+ * at runtime; the correct case is required.
+ *
+ * Therefore, in order to avoid ever having to normalize case at
+ * runtime, the policy of HTMLjs is to put the burden on the caller
+ * of functions like `HTML.ensureTag` -- for example, a template
+ * engine -- of supplying correct normalized case.
+ *
+ * Briefly put, normlized case is usually lowercase, except for certain
+ * elements where it is camelCase.
+ */
- if (typeof value !== 'string')
- throw new Error('HTML.Comment must be constructed with a string');
+////////////////////////////// KNOWN ELEMENTS
- this.value = value;
- // Kill illegal hyphens in comment value (no way to escape them in HTML)
- this.sanitizedValue = value.replace(/^-|--+|-$/g, '');
+/**
+ * ### Known Elements
+ *
+ * HTMLjs comes preloaded with constructors for all "known" HTML and
+ * SVG elements. You can use `HTML.P`, `HTML.DIV`, and so on out of
+ * the box. If you want to create a tag like ` s around
- // "paragraphs" that are wrapped in non-block-level tags, such as anchors,
- // phrase emphasis, and spans. The list of tags we're looking for is
- // hard-coded:
- var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del|style|section|header|footer|nav|article|aside";
- var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|style|section|header|footer|nav|article|aside";
-
- // First, look for nested blocks, e.g.:
- // tags around block-level tags.
- text = _HashHTMLBlocks(text);
- text = _FormParagraphs(text);
-
- return text;
-};
-
-
-var _RunSpanGamut = function(text) {
-//
-// These are all the transformations that occur *within* block-level
-// tags like paragraphs, headers, and list items.
-//
-
- text = _DoCodeSpans(text);
- text = _EscapeSpecialCharsWithinTagAttributes(text);
- text = _EncodeBackslashEscapes(text);
-
- // Process anchor and image tags. Images must come first,
- // because ![foo][f] looks like an anchor.
- text = _DoImages(text);
- text = _DoAnchors(text);
-
- // Make links out of things like ` Just type tags
-//
-
- // Strip leading and trailing lines:
- text = text.replace(/^\n+/g,"");
- text = text.replace(/\n+$/g,"");
-
- var grafs = text.split(/\n{2,}/g);
- var grafsOut = [];
-
- //
- // Wrap tags.
- //
- var end = grafs.length;
- for (var i=0; i ");
- str += " s around
+ // "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ // phrase emphasis, and spans. The list of tags we're looking for is
+ // hard-coded:
+ var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del|style|section|header|footer|nav|article|aside";
+ var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|style|section|header|footer|nav|article|aside";
+
+ // First, look for nested blocks, e.g.:
+ // tags around block-level tags.
+ text = _HashHTMLBlocks(text);
+ text = _FormParagraphs(text);
+
+ return text;
+};
+
+
+var _RunSpanGamut = function(text) {
+//
+// These are all the transformations that occur *within* block-level
+// tags like paragraphs, headers, and list items.
+//
+
+ text = _DoCodeSpans(text);
+ text = _EscapeSpecialCharsWithinTagAttributes(text);
+ text = _EncodeBackslashEscapes(text);
+
+ // Process anchor and image tags. Images must come first,
+ // because ![foo][f] looks like an anchor.
+ text = _DoImages(text);
+ text = _DoAnchors(text);
+
+ // Make links out of things like ` Just type tags
+//
+
+ // Strip leading and trailing lines:
+ text = text.replace(/^\n+/g,"");
+ text = text.replace(/\n+$/g,"");
+
+ var grafs = text.split(/\n{2,}/g);
+ var grafsOut = [];
+
+ //
+ // Wrap tags.
+ //
+ var end = grafs.length;
+ for (var i=0; i ");
+ str += " Foo is bar. Hi there! `. When it re-runs due to a dependency changing the
// value for a given attribute might stay the same. Test that the
// attribute is not set on the DOM element.
-Tinytest.add('spacebars - templates - attribute object helpers are isolated', function (test) {
+Tinytest.add('spacebars-tests - template_tests - attribute object helpers are isolated', function (test) {
var tmpl = Template.spacebars_template_test_attr_object_helpers_are_isolated;
var dep = new Deps.Dependency;
tmpl.attrs = function () {
@@ -1133,25 +1120,27 @@ Tinytest.add('spacebars - templates - attribute object helpers are isolated', fu
//
// Also, verify that an error is thrown if the return value from such
// a helper is not a component.
-Tinytest.add('spacebars - templates - inclusion helpers are isolated', function (test) {
+Tinytest.add('spacebars-tests - template_tests - inclusion helpers are isolated', function (test) {
var tmpl = Template.spacebars_template_test_inclusion_helpers_are_isolated;
var dep = new Deps.Dependency;
- var subtmpl = Template.
- spacebars_template_test_inclusion_helpers_are_isolated_subtemplate
- .extend({}); // fresh instance
- var R = new ReactiveVar(subtmpl);
+ var subtmpl = Template.spacebars_template_test_inclusion_helpers_are_isolated_subtemplate;
+ // make a copy so we can set "rendered" without mutating the original
+ var subtmplCopy = Template.__create__(
+ subtmpl.__viewName,
+ subtmpl.__render);
+ var R = new ReactiveVar(subtmplCopy);
tmpl.foo = function () {
dep.depend();
return R.get();
};
var div = renderToDiv(tmpl);
- subtmpl.rendered = function () {
+ subtmplCopy.rendered = function () {
test.fail("shouldn't re-render when same value returned from helper");
};
dep.changed();
- Deps.flush({_throwFirstError: true}); // `subtmpl.rendered` not called
+ Deps.flush({_throwFirstError: true}); // `subtmplCopy.rendered` not called
R.set(null);
Deps.flush({_throwFirstError: true}); // no error thrown
@@ -1160,10 +1149,10 @@ Tinytest.add('spacebars - templates - inclusion helpers are isolated', function
test.throws(function () {
Deps.flush({_throwFirstError: true});
- }, /Expected null or template/);
+ }, /Expected template or null/);
});
-Tinytest.add('spacebars - templates - nully attributes', function (test) {
+Tinytest.add('spacebars-tests - template_tests - nully attributes', function (test) {
var tmpls = {
0: Template.spacebars_template_test_nully_attributes0,
1: Template.spacebars_template_test_nully_attributes1,
@@ -1175,9 +1164,7 @@ Tinytest.add('spacebars - templates - nully attributes', function (test) {
};
var run = function (whichTemplate, data, expectTrue) {
- var templateWithData = tmpls[whichTemplate].extend({data: function () {
- return data; }});
- var div = renderToDiv(templateWithData);
+ var div = renderToDiv(tmpls[whichTemplate], data);
var input = div.querySelector('input');
var descr = JSON.stringify([whichTemplate, data, expectTrue]);
if (expectTrue) {
@@ -1188,7 +1175,10 @@ Tinytest.add('spacebars - templates - nully attributes', function (test) {
test.equal(JSON.stringify(input.getAttribute('stuff')), 'null', descr);
}
- var html = HTML.toHTML(templateWithData);
+ var html = Blaze.toHTML(Blaze.With(data, function () {
+ return tmpls[whichTemplate];
+ }));
+
test.equal(/ checked="[^"]*"/.test(html), !! expectTrue);
test.equal(/ stuff="[^"]*"/.test(html), !! expectTrue);
};
@@ -1226,7 +1216,7 @@ Tinytest.add('spacebars - templates - nully attributes', function (test) {
run(3, {foo: false}, false);
});
-Tinytest.add("spacebars - templates - double", function (test) {
+Tinytest.add("spacebars-tests - template_tests - double", function (test) {
var tmpl = Template.spacebars_template_test_double;
var run = function (foo, expectedResult) {
@@ -1244,11 +1234,11 @@ Tinytest.add("spacebars - templates - double", function (test) {
run(undefined, '');
});
-Tinytest.add("spacebars - templates - inclusion lookup order", function (test) {
+Tinytest.add("spacebars-tests - template_tests - inclusion lookup order", function (test) {
// test that {{> foo}} looks for a helper named 'foo', then a
// template named 'foo', then a 'foo' field in the data context.
var tmpl = Template.spacebars_template_test_inclusion_lookup;
- tmpl.data = function () {
+ var tmplData = function () {
return {
// shouldn't have an effect since we define a helper with the
// same name.
@@ -1261,12 +1251,12 @@ Tinytest.add("spacebars - templates - inclusion lookup order", function (test) {
tmpl.spacebars_template_test_inclusion_lookup_subtmpl =
Template.spacebars_template_test_inclusion_lookup_subtmpl2;
- test.equal(canonicalizeHtml(renderToDiv(tmpl).innerHTML),
+ test.equal(canonicalizeHtml(renderToDiv(tmpl, tmplData).innerHTML),
["This is generated by a helper with the same name.",
"This is a template passed in the data context."].join(' '));
});
-Tinytest.add("spacebars - templates - content context", function (test) {
+Tinytest.add("spacebars-tests - template_tests - content context", function (test) {
var tmpl = Template.spacebars_template_test_content_context;
var R = ReactiveVar(true);
tmpl.foo = {
@@ -1288,7 +1278,7 @@ Tinytest.add("spacebars - templates - content context", function (test) {
_.each(['textarea', 'text', 'password', 'submit', 'button',
'reset', 'select', 'hidden'], function (type) {
- Tinytest.add("spacebars - controls - " + type, function(test) {
+ Tinytest.add("spacebars-tests - template_tests - controls - " + type, function(test) {
var R = ReactiveVar({x:"test"});
var R2 = ReactiveVar("");
var tmpl;
@@ -1371,7 +1361,7 @@ _.each(['textarea', 'text', 'password', 'submit', 'button',
});
});
-Tinytest.add("spacebars - controls - radio", function(test) {
+Tinytest.add("spacebars-tests - template_tests - radio", function(test) {
var R = ReactiveVar("");
var R2 = ReactiveVar("");
var change_buf = [];
@@ -1442,7 +1432,7 @@ Tinytest.add("spacebars - controls - radio", function(test) {
document.body.removeChild(div);
});
-Tinytest.add("spacebars - controls - checkbox", function(test) {
+Tinytest.add("spacebars-tests - template_tests - checkbox", function(test) {
var tmpl = Template.spacebars_test_control_checkbox;
tmpl.labels = ["Foo", "Bar", "Baz"];
var Rs = {};
@@ -1504,13 +1494,13 @@ Tinytest.add("spacebars - controls - checkbox", function(test) {
document.body.removeChild(div);
});
-Tinytest.add('spacebars - template - unfound template', function (test) {
+Tinytest.add('spacebars-tests - template_tests - unfound template', function (test) {
test.throws(function () {
renderToDiv(Template.spacebars_test_nonexistent_template);
- }, /Can't find template/);
+ }, /No such template/);
});
-Tinytest.add('spacebars - template - helper passed to #if called exactly once when invalidated', function (test) {
+Tinytest.add('spacebars-tests - template_tests - helper passed to #if called exactly once when invalidated', function (test) {
var tmpl = Template.spacebars_test_if_helper;
var count = 0;
@@ -1532,7 +1522,7 @@ Tinytest.add('spacebars - template - helper passed to #if called exactly once wh
test.equal(count, 2);
});
-Tinytest.add('spacebars - template - custom block helper functions called exactly once when invalidated', function (test) {
+Tinytest.add('spacebars-tests - template_tests - custom block helper functions called exactly once when invalidated', function (test) {
var tmpl = Template.spacebars_test_block_helper_function;
var count = 0;
@@ -1540,7 +1530,7 @@ Tinytest.add('spacebars - template - custom block helper functions called exactl
tmpl.foo = function () {
d.depend();
count++;
- return UI.block(function () { return []; });
+ return Template.spacebars_template_test_aaa;
};
foo = false;
@@ -1606,32 +1596,32 @@ var runOneTwoTest = function (test, subTemplateName, optionsData) {
});
};
-Tinytest.add('spacebars - template - with stops without re-running helper', function (test) {
+Tinytest.add('spacebars-tests - template_tests - with stops without re-running helper', function (test) {
runOneTwoTest(test, 'spacebars_test_helpers_stop_with');
});
-Tinytest.add('spacebars - template - each stops without re-running helper', function (test) {
+Tinytest.add('spacebars-tests - template_tests - each stops without re-running helper', function (test) {
runOneTwoTest(test, 'spacebars_test_helpers_stop_each');
});
-Tinytest.add('spacebars - template - each inside with stops without re-running helper', function (test) {
+Tinytest.add('spacebars-tests - template_tests - each inside with stops without re-running helper', function (test) {
runOneTwoTest(test, 'spacebars_test_helpers_stop_with_each');
});
-Tinytest.add('spacebars - template - if stops without re-running helper', function (test) {
+Tinytest.add('spacebars-tests - template_tests - if stops without re-running helper', function (test) {
runOneTwoTest(test, 'spacebars_test_helpers_stop_if', ['a', 'b', 'a']);
});
-Tinytest.add('spacebars - template - unless stops without re-running helper', function (test) {
+Tinytest.add('spacebars-tests - template_tests - unless stops without re-running helper', function (test) {
runOneTwoTest(test, 'spacebars_test_helpers_stop_unless', ['a', 'b', 'a']);
});
-Tinytest.add('spacebars - template - inclusion stops without re-running function', function (test) {
+Tinytest.add('spacebars-tests - template_tests - inclusion stops without re-running function', function (test) {
var t = Template.spacebars_test_helpers_stop_inclusion3;
runOneTwoTest(test, 'spacebars_test_helpers_stop_inclusion', [t, t, t]);
});
-Tinytest.add('spacebars - template - template with callbacks inside with stops without recalculating data', function (test) {
+Tinytest.add('spacebars-tests - template_tests - template with callbacks inside with stops without recalculating data', function (test) {
var tmpl = Template.spacebars_test_helpers_stop_with_callbacks3;
tmpl.created = function () {};
tmpl.rendered = function () {};
@@ -1639,7 +1629,7 @@ Tinytest.add('spacebars - template - template with callbacks inside with stops w
runOneTwoTest(test, 'spacebars_test_helpers_stop_with_callbacks');
});
-Tinytest.add('spacebars - template - no data context is seen as an empty object', function (test) {
+Tinytest.add('spacebars-tests - template_tests - no data context is seen as an empty object', function (test) {
var tmpl = Template.spacebars_test_no_data_context;
var dataInHelper = 'UNSET';
@@ -1681,7 +1671,7 @@ Tinytest.add('spacebars - template - no data context is seen as an empty object'
test.equal(dataInEvent, {});
});
-Tinytest.add('spacebars - template - falsy with', function (test) {
+Tinytest.add('spacebars-tests - template_tests - falsy with', function (test) {
var tmpl = Template.spacebars_test_falsy_with;
var R = ReactiveVar(null);
tmpl.obj = function () { return R.get(); };
@@ -1699,7 +1689,7 @@ Tinytest.add('spacebars - template - falsy with', function (test) {
divRendersTo(test, div, "alpha");
});
-Tinytest.add("spacebars - template - helpers don't leak", function (test) {
+Tinytest.add("spacebars-tests - template_tests - helpers don't leak", function (test) {
var tmpl = Template.spacebars_test_helpers_dont_leak;
tmpl.foo = "wrong";
tmpl.bar = function () { return "WRONG"; };
@@ -1713,8 +1703,7 @@ Tinytest.add("spacebars - template - helpers don't leak", function (test) {
divRendersTo(test, div, "correct BONUS");
});
-Tinytest.add(
- "spacebars - template - event handler returns false",
+Tinytest.add("spacebars-tests - template_tests - event handler returns false",
function (test) {
var tmpl = Template.spacebars_test_event_returns_false;
var elemId = "spacebars_test_event_returns_false_link";
@@ -1725,6 +1714,9 @@ Tinytest.add(
var div = renderToDiv(tmpl);
document.body.appendChild(div);
clickIt(document.getElementById(elemId));
+ // NOTE: This failure can stick across test runs! Try
+ // removing '#bad-url' from the location bar and run
+ // the tests again. :)
test.isFalse(/#bad-url/.test(window.location.hash));
document.body.removeChild(div);
}
@@ -1735,7 +1727,7 @@ Tinytest.add(
// `$(elem).find(...)` works this way, but the browser's
// querySelector doesn't.
Tinytest.add(
- "spacebars - template - event map selector scope",
+ "spacebars-tests - template_tests - event map selector scope",
function (test) {
var tmpl = Template.spacebars_test_event_selectors1;
var tmpl2 = Template.spacebars_test_event_selectors2;
@@ -1761,7 +1753,7 @@ if (document.addEventListener) {
// nice to get rid of the network dependency, though.)
// We skip this test in IE 8.
Tinytest.add(
- "spacebars - template - event map selector scope (capturing)",
+ "spacebars-tests - template_tests - event map selector scope (capturing)",
function (test) {
var tmpl = Template.spacebars_test_event_selectors_capturing1;
var tmpl2 = Template.spacebars_test_event_selectors_capturing2;
@@ -1784,7 +1776,7 @@ if (document.addEventListener) {
);
}
-Tinytest.add("spacebars - template - tables", function (test) {
+Tinytest.add("spacebars-tests - template_tests - tables", function (test) {
var tmpl1 = Template.spacebars_test_tables1;
var div = renderToDiv(tmpl1);
@@ -1800,8 +1792,7 @@ Tinytest.add("spacebars - template - tables", function (test) {
divRendersTo(test, div, ' Hello Hello Hello World Hello World
")` represents raw HTML to insert
-into the document. The HTML should be known to be safe and contain
-balanced tags! It will be injected without any parsing or checking
-when the representation is converted to an HTML string. If the
-representation is used to generate DOM directly, the "raw" node will
-be materialized using an innerHTML-like method.
+* `tagName` - The tag name in lowercase (or camelCase)
+* `children` - An array of children (always present)
+* `attrs` - An attributes dictionary, `null`, or an array (see below)
-Functions in the tree are used as reactivity boundaries when
-generating DOM directly. When generating HTML, they are simply called
-for their return value. Functions are passed no arguments and are
-given no particular value of `this`.
-Templates/components like `Template.foo` can also be included in the
-representation. HTMLjs has very limited knowledge of what a component
-is. It knows components have an `instantiate` method that returns
-something with a `render` method. Operations that realize an HTMLjs
-tree as HTML, DOM, or some other form have a bit of boilerplate that
-they use to detect and instantiate components:
+### Special forms of attributes
+
+The attributes of a Tag may be an array of dictionaries. In order
+for a tag constructor to recognize an array as the attributes argument,
+it must be written as `HTML.Attrs(attrs1, attrs2, ...)`, as in this
+example:
```
-HTML.toHTML = function (node, parentComponent) {
- // ... handle various types of `node`
- if (typeof node.instantiate === 'function') {
- // component
- var instance = node.instantiate(parentComponent || null);
- var content = instance.render();
- // recurse with a new value for parentComponent
- return HTML.toHTML(content, instance);
- }
- // ...
-};
+var extraAttrs = {'class': "container"};
+
+var div = HTML.DIV(HTML.Attrs({id: "main"}, extraAttrs),
+ "This is the content.");
+
+div.attrs // => [{id: "main"}, {'class': "container"}]
```
-The argument `parentComponent` is used to set a pointer that points
-from each component to its parent, used for name lookups.
+`HTML.Attrs` may also be used to pass a foreign object in place of
+an attributes dictionary of a tag.
-## "Known" and Custom Tags
-All the usual HTML and HTML5 tags are available as `HTML.A`,
-`HTML.ABBR`, `HTML.ADDRESS`, etc. These tags are called "known" tags
-and have predefined tag constructors. If you want to use a custom
-tag, you'll have to create the tag constructor using `getTag` or `ensureTag`.
+
+### Normalized Case for Tag Names
+
+The `tagName` field is always in "normalized case," which is the
+official case for that particular element name (usually lowercase).
+For example, `HTML.DIV().tagName` is `"div"`. For some elements
+used in inline SVG graphics, the correct case is "camelCase." For
+example, there is an element named `clipPath`.
+
+Web browsers have a confusing policy about case. They perform case
+normalization when parsing HTML, but not when creating SVG elements
+at runtime; the correct case is required.
+
+Therefore, in order to avoid ever having to normalize case at
+runtime, the policy of HTMLjs is to put the burden on the caller
+of functions like `HTML.ensureTag` -- for example, a template
+engine -- of supplying correct normalized case.
+
+Briefly put, normlized case is usually lowercase, except for certain
+elements where it is camelCase.
+
+
+### Known Elements
+
+HTMLjs comes preloaded with constructors for all "known" HTML and
+SVG elements. You can use `HTML.P`, `HTML.DIV`, and so on out of
+the box. If you want to create a tag like `
"
```
-Similarly, objects constructed with `HTML.Comment` are instances of `HTML.Comment`, and so on.
+Foreign objects are not allowed in `content`. To generate HTML
+containing foreign objects, create a subclass of
+`HTML.ToHTMLVisitor` and override `visitObject`.
-In general, HTMLjs objects should be considered immutable.
-HTML.Tag objects have these properties:
+## HTML.toText(content, textMode)
-* `tagName` - the uppercase tag name
-* `attrs` - an object or null
-* `children` - an array of zero or more children
+* `content` - any HTMLjs content
+* `textMode` - the type of text to generate; one of
+ `HTML.TEXTMODE.STRING`, `HTML.TEXTMODE.RCDATA`, or
+ `HTML.TEXTMODE.ATTRIBUTE`
-HTML.CharRef objects have `html` and `str` properties, specified by
-the object passed to the constructor.
+Generating HTML or DOM from HTMLjs content requires generating text
+for attribute values and for the contents of TEXTAREA elements,
+among others. The input content may contain strings, arrays,
+booleans, numbers, nulls, and CharRefs. Behavior on other types
+is undefined.
-HTML.Comment and HTML.Raw objects have a `value` property.
-
-### Attributes
-
-Attribute values can contain most kinds of HTMLjs content, but not Tag, Comment, or Raw. They may contain functions and components, even though these functions won't ever establish reactivity boundaries at a finer level than an entire attribute value.
-
-The attributes dictionary of a tag can have a special entry `$dynamic`, which holds additional attributes dictionaries to combine with the main dictionary. These additional dictionaries may be computed by functions, lending generality to the calculation of the attributes dictionary that would not otherwise be present.
-
-Specifically, the value of `$dynamic` must be an array, each element of which is either an attributes dictionary or a function returning an attributes dictionary. (These dictionaries may not themselves have a `$dynamic` key.) When calculating the final attributes dictionary for a tag, each dictionary obtained from the `$dynamic` array is used to modify the existing dictionary by copying the new attribute entries over it, except for entries with a "nully" value. A "nully" value is one that is either `null`, `undefined`, `[]`, or an array of nully values. Before checking if the dynamic attribute value is nully, all functions and components are evaluated (i.e. the functions are called and the components are instantiated, such that no functions or components remain).
-
-The `$dynamic` feature is designed to support writing `
` instead of `
`. Note that neither HTML4 nor HTML5 has true self-closing tags (except when parsing SVG). `
` is the same as `
` and `` is the same as `
+
+```
+ *
+ * The functions `UL`, `LI`, and `B` are constructors which
+ * return instances of `HTML.Tag`. These tag objects can
+ * then be converted to an HTML string or directly into DOM nodes.
+ *
+ * The flexible structure of HTMLjs allows different kinds of Blaze
+ * directives to be embedded in the tree. HTMLjs does not know about
+ * these directives, which are considered "foreign objects."
+ *
+ * # Built-in Types
+ *
+ * The following types are built into HTMLjs. Built-in methods like
+ * `HTML.toHTML` require a tree consisting only of these types.
+ *
+ * * __`null`, `undefined`__ - Render to nothing.
+ *
+ * * __boolean, number__ - Render to the string form of the boolean or number.
+ *
+ * * __string__ - Renders to a text node (or part of an attribute value). All characters are safe, and no HTML injection is possible. The string `""` renders `<a>` in HTML, and `document.createTextNode("")` in DOM.
+ *
+ * * __Array__ - Renders to its elements in order. An array may be empty. Arrays are detected using `HTML.isArray(...)`.
+ *
+ * * __`HTML.Tag`__ - Renders to an HTML element (including start tag, contents, and end tag).
+ *
+ * * __`HTML.CharRef({html: ..., str: ...})`__ - Renders to a character reference (such as ` `) when generating HTML.
+ *
+ * * __`HTML.Comment(text)`__ - Renders to an HTML comment.
+ *
+ * * __`HTML.Raw(html)`__ - Renders to a string of HTML to include verbatim.
+ *
+ * The `new` keyword is not required before constructors of HTML object types.
+ *
+ * All objects and arrays should be considered immutable. Instance properties
+ * are public, but they should only be read, not written. Arrays should not
+ * be spliced in place. This convention allows for clean patterns of
+ * processing and transforming HTMLjs trees.
+ */
+
+/**
+ * ## HTML.Tag
+ *
+ * An `HTML.Tag` is created using a tag-specific constructor, like
+ * `HTML.P` for a `
"
+ * ```
+ *
+ * Foreign objects are not allowed in `content`. To generate HTML
+ * containing foreign objects, create a subclass of
+ * `HTML.ToHTMLVisitor` and override `visitObject`.
+ */
+HTML.toHTML = function (content) {
+ return (new HTML.ToHTMLVisitor).visit(content);
+};
+
+// Escaping modes for outputting text when generating HTML.
+HTML.TEXTMODE = {
+ STRING: 1,
+ RCDATA: 2,
+ ATTRIBUTE: 3
+};
+
+/**
+ * ## HTML.toText(content, textMode)
+ *
+ * * `content` - any HTMLjs content
+ * * `textMode` - the type of text to generate; one of
+ * `HTML.TEXTMODE.STRING`, `HTML.TEXTMODE.RCDATA`, or
+ * `HTML.TEXTMODE.ATTRIBUTE`
+ *
+ * Generating HTML or DOM from HTMLjs content requires generating text
+ * for attribute values and for the contents of TEXTAREA elements,
+ * among others. The input content may contain strings, arrays,
+ * booleans, numbers, nulls, and CharRefs. Behavior on other types
+ * is undefined.
+ *
+ * The required `textMode` argument specifies the type of text to
+ * generate:
+ *
+ * * `HTML.TEXTMODE.STRING` - a string with no special
+ * escaping or encoding performed, suitable for passing to
+ * `setAttribute` or `document.createTextNode`.
+ * * `HTML.TEXTMODE.RCDATA` - a string with `<` and `&` encoded
+ * as character references (and CharRefs included in their
+ * "HTML" form), suitable for including in a string of HTML
+ * * `HTML.TEXTMODE.ATTRIBUTE` - a string with `"` and `&` encoded
+ * as character references (and CharRefs included in their
+ * "HTML" form), suitable for including in an HTML attribute
+ * value surrounded by double quotes
+ */
+
+HTML.toText = function (content, textMode) {
+ if (! textMode)
+ throw new Error("textMode required for HTML.toText");
+ if (! (textMode === HTML.TEXTMODE.STRING ||
+ textMode === HTML.TEXTMODE.RCDATA ||
+ textMode === HTML.TEXTMODE.ATTRIBUTE))
+ throw new Error("Unknown textMode: " + textMode);
+
+ var visitor = new HTML.ToTextVisitor({textMode: textMode});;
+ return visitor.visit(content);
};
diff --git a/packages/htmljs/htmljs_test.js b/packages/htmljs/htmljs_test.js
index 65cf28ecaa..a8657fab83 100644
--- a/packages/htmljs/htmljs_test.js
+++ b/packages/htmljs/htmljs_test.js
@@ -79,24 +79,6 @@ Tinytest.add("htmljs - utils", function (test) {
});
-Tinytest.add("htmljs - attributes", function (test) {
- var SPAN = HTML.SPAN;
- var amp = HTML.CharRef({html: '&', str: '&'});
-
- test.equal(HTML.toHTML(SPAN({title: ['M', amp, 'Ms']}, 'M', amp, 'M candies')),
- 'M&M candies');
-
- // test that evaluateAttributes calls functions in both normal and dynamic attributes
- test.equal(HTML.evaluateAttributes({x: function () { return 'abc'; }}),
- { x: 'abc' });
- test.equal(HTML.evaluateAttributes({x: function () { return 'abc'; },
- $dynamic: []}),
- { x: 'abc' });
- test.equal(HTML.evaluateAttributes({x: function () { return 'abc'; },
- $dynamic: [{ x: function () { return 'def'; }}]}),
- { x: 'def' });
-});
-
Tinytest.add("htmljs - details", function (test) {
test.equal(HTML.toHTML(false), "false");
});
\ No newline at end of file
diff --git a/packages/htmljs/package.js b/packages/htmljs/package.js
index 04bcb4b0f8..1824b02c44 100644
--- a/packages/htmljs/package.js
+++ b/packages/htmljs/package.js
@@ -6,7 +6,9 @@ Package.describe({
Package.on_use(function (api) {
api.export('HTML');
- api.add_files(['utils.js', 'html.js', 'tohtml.js']);
+ api.add_files(['preamble.js',
+ 'visitors.js',
+ 'html.js']);
});
Package.on_test(function (api) {
diff --git a/packages/htmljs/preamble.js b/packages/htmljs/preamble.js
new file mode 100644
index 0000000000..160e038041
--- /dev/null
+++ b/packages/htmljs/preamble.js
@@ -0,0 +1,4 @@
+HTML = {};
+
+IDENTITY = function (x) { return x; };
+SLICE = Array.prototype.slice;
diff --git a/packages/htmljs/tohtml.js b/packages/htmljs/tohtml.js
deleted file mode 100644
index 2d9e9453f3..0000000000
--- a/packages/htmljs/tohtml.js
+++ /dev/null
@@ -1,159 +0,0 @@
-
-HTML.toHTML = function (node, parentComponent) {
- if (node == null) {
- // null or undefined
- return '';
- } else if ((typeof node === 'string') || (typeof node === 'boolean') || (typeof node === 'number')) {
- // string; escape special chars
- return HTML.escapeData(String(node));
- } else if (node instanceof Array) {
- // array
- var parts = [];
- for (var i = 0; i < node.length; i++)
- parts.push(HTML.toHTML(node[i], parentComponent));
- return parts.join('');
- } else if (typeof node.instantiate === 'function') {
- // component
- var instance = node.instantiate(parentComponent || null);
- var content = instance.render('STATIC');
- stopWithLater(instance);
- // recurse with a new value for parentComponent
- return HTML.toHTML(content, instance);
- } else if (typeof node === 'function') {
- return HTML.toHTML(callReactiveFunction(node), parentComponent);
- } else if (node.toHTML) {
- // Tag or something else
- return node.toHTML(parentComponent);
- } else {
- throw new Error("Expected tag, string, array, component, null, undefined, or " +
- "object with a toHTML method; found: " + node);
- }
-};
-
-HTML.Comment.prototype.toHTML = function () {
- return '';
-};
-
-HTML.CharRef.prototype.toHTML = function () {
- return this.html;
-};
-
-HTML.Raw.prototype.toHTML = function () {
- return this.value;
-};
-
-HTML.Tag.prototype.toHTML = function (parentComponent) {
- var attrStrs = [];
- var attrs = this.evaluateAttributes(parentComponent);
- if (attrs) {
- for (var k in attrs) {
- var v = HTML.toText(attrs[k], HTML.TEXTMODE.ATTRIBUTE, parentComponent);
- attrStrs.push(' ' + k + '="' + v + '"');
- }
- }
-
- var tagName = this.tagName;
- var startTag = '<' + tagName + attrStrs.join('') + '>';
-
- var childStrs = [];
- var content;
- if (tagName === 'textarea') {
- for (var i = 0; i < this.children.length; i++)
- childStrs.push(HTML.toText(this.children[i], HTML.TEXTMODE.RCDATA, parentComponent));
-
- 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 < this.children.length; i++)
- childStrs.push(HTML.toHTML(this.children[i], parentComponent));
-
- content = childStrs.join('');
- }
-
- var result = startTag + content;
-
- if (this.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;
-};
-
-HTML.TEXTMODE = {
- ATTRIBUTE: 1,
- RCDATA: 2,
- STRING: 3
-};
-
-HTML.toText = function (node, textMode, parentComponent) {
- if (node == null) {
- // null or undefined
- return '';
- } else if ((typeof node === 'string') || (typeof node === 'boolean') || (typeof node === 'number')) {
- node = String(node);
- // string
- if (textMode === HTML.TEXTMODE.STRING) {
- return node;
- } else if (textMode === HTML.TEXTMODE.RCDATA) {
- return HTML.escapeData(node);
- } else if (textMode === HTML.TEXTMODE.ATTRIBUTE) {
- // escape `&` and `"` this time, not `&` and `<`
- return node.replace(/&/g, '&').replace(/"/g, '"');
- } else {
- throw new Error("Unknown TEXTMODE: " + textMode);
- }
- } else if (node instanceof Array) {
- // array
- var parts = [];
- for (var i = 0; i < node.length; i++)
- parts.push(HTML.toText(node[i], textMode, parentComponent));
- return parts.join('');
- } else if (typeof node === 'function') {
- return HTML.toText(callReactiveFunction(node), textMode, parentComponent);
- } else if (typeof node.instantiate === 'function') {
- // component
- var instance = node.instantiate(parentComponent || null);
- var content = instance.render('STATIC');
- var result = HTML.toText(content, textMode, instance);
- stopWithLater(instance);
- return result;
- } else if (node.toText) {
- // Something else
- return node.toText(textMode, parentComponent);
- } else {
- throw new Error("Expected tag, string, array, component, null, undefined, or " +
- "object with a toText method; found: " + node);
- }
-
-};
-
-HTML.Raw.prototype.toText = function () {
- return this.value;
-};
-
-// used when including templates within {{#markdown}}
-HTML.Tag.prototype.toText = function (textMode, parentComponent) {
- if (textMode === HTML.TEXTMODE.STRING)
- // stringify the tag as HTML, then convert to text
- return HTML.toText(this.toHTML(parentComponent), textMode);
- else
- throw new Error("Can't insert tags in attributes or TEXTAREA elements");
-};
-
-HTML.CharRef.prototype.toText = function (textMode) {
- if (textMode === HTML.TEXTMODE.STRING)
- return this.str;
- else if (textMode === HTML.TEXTMODE.RCDATA)
- return this.html;
- else if (textMode === HTML.TEXTMODE.ATTRIBUTE)
- return this.html;
- else
- throw new Error("Unknown TEXTMODE: " + textMode);
-};
diff --git a/packages/htmljs/utils.js b/packages/htmljs/utils.js
deleted file mode 100644
index 94b31d5c5f..0000000000
--- a/packages/htmljs/utils.js
+++ /dev/null
@@ -1,42 +0,0 @@
-
-HTML = {};
-
-HTML.isNully = function (node) {
- if (node == null)
- // null or undefined
- return true;
-
- if (node instanceof Array) {
- // is it an empty array or an array of all nully items?
- for (var i = 0; i < node.length; i++)
- if (! HTML.isNully(node[i]))
- return false;
- return true;
- }
-
- return false;
-};
-
-HTML.escapeData = function (str) {
- // string; escape the two special chars in HTML data and RCDATA
- return str.replace(/&/g, '&').replace(/`), while
-// `setAttribute` seems to use something like the XML grammar for names (and
-// throws an error if a name is invalid, making that attribute unsettable).
-// If we knew exactly what grammar browsers used for `setAttribute`, we could
-// include various Unicode ranges in what's legal. For now, allow ASCII chars
-// that are known to be valid XML, valid HTML, and settable via `setAttribute`:
-//
-// * Starts with `:`, `_`, `A-Z` or `a-z`
-// * Consists of any of those plus `-`, `.`, and `0-9`.
-//
-// See tags get encoded.
-//
-
- // Clear the global hashes. If we don't clear these, you get conflicts
- // from other articles when generating a page which contains more than
- // one article (e.g. an index page that shows the N most recent
- // articles):
- g_urls = {};
- g_titles = {};
- g_html_blocks = [];
-
- // attacklab: Replace ~ with ~T
- // This lets us use tilde as an escape char to avoid md5 hashes
- // The choice of character is arbitray; anything that isn't
- // magic in Markdown will work.
- text = text.replace(/~/g,"~T");
-
- // attacklab: Replace $ with ~D
- // RegExp interprets $ as a special character
- // when it's in a replacement string
- text = text.replace(/\$/g,"~D");
-
- // Standardize line endings
- text = text.replace(/\r\n/g,"\n"); // DOS to Unix
- text = text.replace(/\r/g,"\n"); // Mac to Unix
-
- // Make sure text begins and ends with a couple of newlines:
- text = "\n\n" + text + "\n\n";
-
- // Convert all tabs to spaces.
- text = _Detab(text);
-
- // Strip any lines consisting only of spaces and tabs.
- // This makes subsequent regexen easier to write, because we can
- // match consecutive blank lines with /\n+/ instead of something
- // contorted like /[ \t]*\n+/ .
- text = text.replace(/^[ \t]+$/mg,"");
-
- // Run language extensions
- Showdown.forEach(g_lang_extensions, function(x){
- text = _ExecuteExtension(x, text);
- });
-
- // Handle github codeblocks prior to running HashHTML so that
- // HTML contained within the codeblock gets escaped propertly
- text = _DoGithubCodeBlocks(text);
-
- // Turn block-level HTML blocks into hash entries
- text = _HashHTMLBlocks(text);
-
- // Strip link definitions, store in hashes.
- text = _StripLinkDefinitions(text);
-
- text = _RunBlockGamut(text);
-
- text = _UnescapeSpecialChars(text);
-
- // attacklab: Restore dollar signs
- text = text.replace(/~D/g,"$$");
-
- // attacklab: Restore tildes
- text = text.replace(/~T/g,"~");
-
- // Run output modifiers
- Showdown.forEach(g_output_modifiers, function(x){
- text = _ExecuteExtension(x, text);
- });
-
- return text;
-};
-//
-// Options:
-//
-
-// Parse extensions options into separate arrays
-if (converter_options && converter_options.extensions) {
-
- var self = this;
-
- // Iterate over each plugin
- Showdown.forEach(converter_options.extensions, function(plugin){
-
- // Assume it's a bundled plugin if a string is given
- if (typeof plugin === 'string') {
- plugin = Showdown.extensions[stdExtName(plugin)];
- }
-
- if (typeof plugin === 'function') {
- // Iterate over each extension within that plugin
- Showdown.forEach(plugin(self), function(ext){
- // Sort extensions by type
- if (ext.type) {
- if (ext.type === 'language' || ext.type === 'lang') {
- g_lang_extensions.push(ext);
- } else if (ext.type === 'output' || ext.type === 'html') {
- g_output_modifiers.push(ext);
- }
- } else {
- // Assume language extension
- g_output_modifiers.push(ext);
- }
- });
- } else {
- throw "Extension '" + plugin + "' could not be loaded. It was either not found or is not a valid extension.";
- }
- });
-}
-
-
-var _ExecuteExtension = function(ext, text) {
- if (ext.regex) {
- var re = new RegExp(ext.regex, 'g');
- return text.replace(re, ext.replace);
- } else if (ext.filter) {
- return ext.filter(text);
- }
-};
-
-var _StripLinkDefinitions = function(text) {
-//
-// Strips link definitions from text, stores the URLs and titles in
-// hash references.
-//
-
- // Link defs are in the form: ^[id]: url "optional title"
-
- /*
- var text = text.replace(/
- ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1
- [ \t]*
- \n? // maybe *one* newline
- [ \t]*
- (\S+?)>? // url = $2
- [ \t]*
- \n? // maybe one newline
- [ \t]*
- (?:
- (\n*) // any lines skipped = $3 attacklab: lookbehind removed
- ["(]
- (.+?) // title = $4
- [")]
- [ \t]*
- )? // title is optional
- (?:\n+|$)
- /gm,
- function(){...});
- */
-
- // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
- text += "~0";
-
- text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|(?=~0))/gm,
- function (wholeMatch,m1,m2,m3,m4) {
- m1 = m1.toLowerCase();
- g_urls[m1] = _EncodeAmpsAndAngles(m2); // Link IDs are case-insensitive
- if (m3) {
- // Oops, found blank lines, so it's not a title.
- // Put back the parenthetical statement we stole.
- return m3+m4;
- } else if (m4) {
- g_titles[m1] = m4.replace(/"/g,""");
- }
-
- // Completely remove the definition from the text
- return "";
- }
- );
-
- // attacklab: strip sentinel
- text = text.replace(/~0/,"");
-
- return text;
-}
-
-
-var _HashHTMLBlocks = function(text) {
- // attacklab: Double up blank lines to reduce lookaround
- text = text.replace(/\n/g,"\n\n");
-
- // Hashify HTML blocks:
- // We only want to do this for block-level HTML tags, such as headers,
- // lists, and tables. That's because we still want to wrap
. It was easier to make a special case than
- // to make the other regex more complicated.
-
- /*
- text = text.replace(/
- ( // save in $1
- \n\n // Starting after a blank line
- [ ]{0,3}
- (<(hr) // start tag = $2
- \b // word break
- ([^<>])*? //
- \/?>) // the matching end tag
- [ \t]*
- (?=\n{2,}) // followed by a blank line
- )
- /g,hashElement);
- */
- text = text.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,hashElement);
-
- // Special case for standalone HTML comments:
-
- /*
- text = text.replace(/
- ( // save in $1
- \n\n // Starting after a blank line
- [ ]{0,3} // attacklab: g_tab_width - 1
-
- [ \t]*
- (?=\n{2,}) // followed by a blank line
- )
- /g,hashElement);
- */
- text = text.replace(/(\n\n[ ]{0,3}[ \t]*(?=\n{2,}))/g,hashElement);
-
- // PHP and ASP-style processor instructions (...?> and <%...%>)
-
- /*
- text = text.replace(/
- (?:
- \n\n // Starting after a blank line
- )
- ( // save in $1
- [ ]{0,3} // attacklab: g_tab_width - 1
- (?:
- <([?%]) // $2
- [^\r]*?
- \2>
- )
- [ \t]*
- (?=\n{2,}) // followed by a blank line
- )
- /g,hashElement);
- */
- text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,hashElement);
-
- // attacklab: Undo double lines (see comment at top of this function)
- text = text.replace(/\n\n/g,"\n");
- return text;
-}
-
-var hashElement = function(wholeMatch,m1) {
- var blockText = m1;
-
- // Undo double lines
- blockText = blockText.replace(/\n\n/g,"\n");
- blockText = blockText.replace(/^\n/,"");
-
- // strip trailing blank lines
- blockText = blockText.replace(/\n+$/g,"");
-
- // Replace the element text with a marker ("~KxK" where x is its key)
- blockText = "\n\n~K" + (g_html_blocks.push(blockText)-1) + "K\n\n";
-
- return blockText;
-};
-
-var _RunBlockGamut = function(text) {
-//
-// These are all the transformations that form block-level
-// tags like paragraphs, headers, and list items.
-//
- text = _DoHeaders(text);
-
- // Do Horizontal Rules:
- var key = hashBlock("
");
- text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm,key);
- text = text.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm,key);
- text = text.replace(/^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$/gm,key);
-
- text = _DoLists(text);
- text = _DoCodeBlocks(text);
- text = _DoBlockQuotes(text);
-
- // We already ran _HashHTMLBlocks() before, in Markdown(), but that
- // was to escape raw HTML in the original Markdown source. This time,
- // we're escaping the markup we've just created, so that we don't wrap
- //
\n");
-
- return text;
-}
-
-var _EscapeSpecialCharsWithinTagAttributes = function(text) {
-//
-// Within tags -- meaning between < and > -- encode [\ ` * _] so they
-// don't conflict with their use in Markdown for code, italics and strong.
-//
-
- // Build a regex to find HTML tags and comments. See Friedl's
- // "Mastering Regular Expressions", 2nd Ed., pp. 200-201.
- var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|)/gi;
-
- text = text.replace(regex, function(wholeMatch) {
- var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g,"$1`");
- tag = escapeCharacters(tag,"\\`*_");
- return tag;
- });
-
- return text;
-}
-
-var _DoAnchors = function(text) {
-//
-// Turn Markdown link shortcuts into XHTML tags.
-//
- //
- // First, handle reference-style links: [link text] [id]
- //
-
- /*
- text = text.replace(/
- ( // wrap whole match in $1
- \[
- (
- (?:
- \[[^\]]*\] // allow brackets nested one level
- |
- [^\[] // or anything else
- )*
- )
- \]
-
- [ ]? // one optional space
- (?:\n[ ]*)? // one optional newline followed by spaces
-
- \[
- (.*?) // id = $3
- \]
- )()()()() // pad remaining backreferences
- /g,_DoAnchors_callback);
- */
- text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeAnchorTag);
-
- //
- // Next, inline-style links: [link text](url "optional title")
- //
-
- /*
- text = text.replace(/
- ( // wrap whole match in $1
- \[
- (
- (?:
- \[[^\]]*\] // allow brackets nested one level
- |
- [^\[\]] // or anything else
- )
- )
- \]
- \( // literal paren
- [ \t]*
- () // no id, so leave $3 empty
- (.*?)>? // href = $4
- [ \t]*
- ( // $5
- (['"]) // quote char = $6
- (.*?) // Title = $7
- \6 // matching quote
- [ \t]* // ignore any spaces/tabs between closing quote and )
- )? // title is optional
- \)
- )
- /g,writeAnchorTag);
- */
- text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()(.*?(?:\(.*?\).*?)?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeAnchorTag);
-
- //
- // Last, handle reference-style shortcuts: [link text]
- // These must come last in case you've also got [link test][1]
- // or [link test](/foo)
- //
-
- /*
- text = text.replace(/
- ( // wrap whole match in $1
- \[
- ([^\[\]]+) // link text = $2; can't contain '[' or ']'
- \]
- )()()()()() // pad rest of backreferences
- /g, writeAnchorTag);
- */
- text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag);
-
- return text;
-}
-
-var writeAnchorTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) {
- if (m7 == undefined) m7 = "";
- var whole_match = m1;
- var link_text = m2;
- var link_id = m3.toLowerCase();
- var url = m4;
- var title = m7;
-
- if (url == "") {
- if (link_id == "") {
- // lower-case and turn embedded newlines into spaces
- link_id = link_text.toLowerCase().replace(/ ?\n/g," ");
- }
- url = "#"+link_id;
-
- if (g_urls[link_id] != undefined) {
- url = g_urls[link_id];
- if (g_titles[link_id] != undefined) {
- title = g_titles[link_id];
- }
- }
- else {
- if (whole_match.search(/\(\s*\)$/m)>-1) {
- // Special case for explicit empty url
- url = "";
- } else {
- return whole_match;
- }
- }
- }
-
- url = escapeCharacters(url,"*_");
- var result = "" + link_text + "";
-
- return result;
-}
-
-
-var _DoImages = function(text) {
-//
-// Turn Markdown image shortcuts into tags.
-//
-
- //
- // First, handle reference-style labeled images: ![alt text][id]
- //
-
- /*
- text = text.replace(/
- ( // wrap whole match in $1
- !\[
- (.*?) // alt text = $2
- \]
-
- [ ]? // one optional space
- (?:\n[ ]*)? // one optional newline followed by spaces
-
- \[
- (.*?) // id = $3
- \]
- )()()()() // pad rest of backreferences
- /g,writeImageTag);
- */
- text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeImageTag);
-
- //
- // Next, handle inline images: 
- // Don't forget: encode * and _
-
- /*
- text = text.replace(/
- ( // wrap whole match in $1
- !\[
- (.*?) // alt text = $2
- \]
- \s? // One optional whitespace character
- \( // literal paren
- [ \t]*
- () // no id, so leave $3 empty
- (\S+?)>? // src url = $4
- [ \t]*
- ( // $5
- (['"]) // quote char = $6
- (.*?) // title = $7
- \6 // matching quote
- [ \t]*
- )? // title is optional
- \)
- )
- /g,writeImageTag);
- */
- text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()(\S+?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeImageTag);
-
- return text;
-}
-
-var writeImageTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) {
- var whole_match = m1;
- var alt_text = m2;
- var link_id = m3.toLowerCase();
- var url = m4;
- var title = m7;
-
- if (!title) title = "";
-
- if (url == "") {
- if (link_id == "") {
- // lower-case and turn embedded newlines into spaces
- link_id = alt_text.toLowerCase().replace(/ ?\n/g," ");
- }
- url = "#"+link_id;
-
- if (g_urls[link_id] != undefined) {
- url = g_urls[link_id];
- if (g_titles[link_id] != undefined) {
- title = g_titles[link_id];
- }
- }
- else {
- return whole_match;
- }
- }
-
- alt_text = alt_text.replace(/"/g,""");
- url = escapeCharacters(url,"*_");
- var result = "
";
-
- return result;
-}
-
-
-var _DoHeaders = function(text) {
-
- // Setext-style headers:
- // Header 1
- // ========
- //
- // Header 2
- // --------
- //
- text = text.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm,
- function(wholeMatch,m1){return hashBlock('
' + _RunSpanGamut(m1) + "
");});
-
- text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,
- function(matchFound,m1){return hashBlock('' + _RunSpanGamut(m1) + "
");});
-
- // atx-style headers:
- // # Header 1
- // ## Header 2
- // ## Header 2 with closing hashes ##
- // ...
- // ###### Header 6
- //
-
- /*
- text = text.replace(/
- ^(\#{1,6}) // $1 = string of #'s
- [ \t]*
- (.+?) // $2 = Header text
- [ \t]*
- \#* // optional closing #'s (not counted)
- \n+
- /gm, function() {...});
- */
-
- text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,
- function(wholeMatch,m1,m2) {
- var h_level = m1.length;
- return hashBlock("` blocks.
-//
-
- /*
- text = text.replace(text,
- /(?:\n\n|^)
- ( // $1 = the code block -- one or more lines, starting with a space/tab
- (?:
- (?:[ ]{4}|\t) // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
- .*\n+
- )+
- )
- (\n*[ ]{0,3}[^ \t\n]|(?=~0)) // attacklab: g_tab_width
- /g,function(){...});
- */
-
- // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
- text += "~0";
-
- text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
- function(wholeMatch,m1,m2) {
- var codeblock = m1;
- var nextChar = m2;
-
- codeblock = _EncodeCode( _Outdent(codeblock));
- codeblock = _Detab(codeblock);
- codeblock = codeblock.replace(/^\n+/g,""); // trim leading newlines
- codeblock = codeblock.replace(/\n+$/g,""); // trim trailing whitespace
-
- codeblock = "
";
-
- return hashBlock(codeblock) + nextChar;
- }
- );
-
- // attacklab: strip sentinel
- text = text.replace(/~0/,"");
-
- return text;
-};
-
-var _DoGithubCodeBlocks = function(text) {
-//
-// Process Github-style code blocks
-// Example:
-// ```ruby
-// def hello_world(x)
-// puts "Hello, #{x}"
-// end
-// ```
-//
-
-
- // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
- text += "~0";
-
- text = text.replace(/(?:^|\n)```(.*)\n([\s\S]*?)\n```/g,
- function(wholeMatch,m1,m2) {
- var language = m1;
- var codeblock = m2;
-
- codeblock = _EncodeCode(codeblock);
- codeblock = _Detab(codeblock);
- codeblock = codeblock.replace(/^\n+/g,""); // trim leading newlines
- codeblock = codeblock.replace(/\n+$/g,""); // trim trailing whitespace
-
- codeblock = "" + codeblock + "\n
";
-
- return hashBlock(codeblock);
- }
- );
-
- // attacklab: strip sentinel
- text = text.replace(/~0/,"");
-
- return text;
-}
-
-var hashBlock = function(text) {
- text = text.replace(/(^\n+|\n+$)/g,"");
- return "\n\n~K" + (g_html_blocks.push(text)-1) + "K\n\n";
-}
-
-var _DoCodeSpans = function(text) {
-//
-// * Backtick quotes are used for " + codeblock + "\n spans.
-//
-// * You can use multiple backticks as the delimiters if you want to
-// include literal backticks in the code span. So, this input:
-//
-// Just type ``foo `bar` baz`` at the prompt.
-//
-// Will translate to:
-//
-// foo `bar` baz at the prompt.`bar` ...
-//
-
- /*
- text = text.replace(/
- (^|[^\\]) // Character before opening ` can't be a backslash
- (`+) // $2 = Opening run of `
- ( // $3 = The code block
- [^\r]*?
- [^`] // attacklab: work around lack of lookbehind
- )
- \2 // Matching closer
- (?!`)
- /gm, function(){...});
- */
-
- text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,
- function(wholeMatch,m1,m2,m3,m4) {
- var c = m3;
- c = c.replace(/^([ \t]*)/g,""); // leading whitespace
- c = c.replace(/[ \t]*$/g,""); // trailing whitespace
- c = _EncodeCode(c);
- return m1+""+c+"";
- });
-
- return text;
-}
-
-var _EncodeCode = function(text) {
-//
-// Encode/escape certain characters inside Markdown code runs.
-// The point is that in code, these characters are literals,
-// and lose their special Markdown meanings.
-//
- // Encode all ampersands; HTML entities are not
- // entities within a Markdown code span.
- text = text.replace(/&/g,"&");
-
- // Do the angle bracket song and dance:
- text = text.replace(//g,">");
-
- // Now, escape characters that are magic in Markdown:
- text = escapeCharacters(text,"\*_{}[]\\",false);
-
-// jj the line above breaks this:
-//---
-
-//* Item
-
-// 1. Subitem
-
-// special char: *
-//---
-
- return text;
-}
-
-
-var _DoItalicsAndBold = function(text) {
-
- // must go first:
- text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g,
- "$2");
-
- text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g,
- "$2");
-
- return text;
-}
-
-
-var _DoBlockQuotes = function(text) {
-
- /*
- text = text.replace(/
- ( // Wrap whole match in $1
- (
- ^[ \t]*>[ \t]? // '>' at the start of a line
- .+\n // rest of the first line
- (.+\n)* // subsequent consecutive lines
- \n* // blanks
- )+
- )
- /gm, function(){...});
- */
-
- text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,
- function(wholeMatch,m1) {
- var bq = m1;
-
- // attacklab: hack around Konqueror 3.5.4 bug:
- // "----------bug".replace(/^-/g,"") == "bug"
-
- bq = bq.replace(/^[ \t]*>[ \t]?/gm,"~0"); // trim one level of quoting
-
- // attacklab: clean up hack
- bq = bq.replace(/~0/g,"");
-
- bq = bq.replace(/^[ \t]+$/gm,""); // trim whitespace-only lines
- bq = _RunBlockGamut(bq); // recurse
-
- bq = bq.replace(/(^|\n)/g,"$1 ");
- // These leading spaces screw with content, so we need to fix that:
- bq = bq.replace(
- /(\s*
[^\r]+?<\/pre>)/gm,
- function(wholeMatch,m1) {
- var pre = m1;
- // attacklab: hack around Konqueror 3.5.4 bug:
- pre = pre.replace(/^ /mg,"~0");
- pre = pre.replace(/~0/g,"");
- return pre;
- });
-
- return hashBlock("\n" + bq + "\n
");
- });
- return text;
-}
-
-
-var _FormParagraphs = function(text) {
-//
-// Params:
-// $text - string to process with html tags get encoded.
+//
+
+ // Clear the global hashes. If we don't clear these, you get conflicts
+ // from other articles when generating a page which contains more than
+ // one article (e.g. an index page that shows the N most recent
+ // articles):
+ g_urls = {};
+ g_titles = {};
+ g_html_blocks = [];
+
+ // attacklab: Replace ~ with ~T
+ // This lets us use tilde as an escape char to avoid md5 hashes
+ // The choice of character is arbitray; anything that isn't
+ // magic in Markdown will work.
+ text = text.replace(/~/g,"~T");
+
+ // attacklab: Replace $ with ~D
+ // RegExp interprets $ as a special character
+ // when it's in a replacement string
+ text = text.replace(/\$/g,"~D");
+
+ // Standardize line endings
+ text = text.replace(/\r\n/g,"\n"); // DOS to Unix
+ text = text.replace(/\r/g,"\n"); // Mac to Unix
+
+ // Make sure text begins and ends with a couple of newlines:
+ text = "\n\n" + text + "\n\n";
+
+ // Convert all tabs to spaces.
+ text = _Detab(text);
+
+ // Strip any lines consisting only of spaces and tabs.
+ // This makes subsequent regexen easier to write, because we can
+ // match consecutive blank lines with /\n+/ instead of something
+ // contorted like /[ \t]*\n+/ .
+ text = text.replace(/^[ \t]+$/mg,"");
+
+ // Run language extensions
+ Showdown.forEach(g_lang_extensions, function(x){
+ text = _ExecuteExtension(x, text);
+ });
+
+ // Handle github codeblocks prior to running HashHTML so that
+ // HTML contained within the codeblock gets escaped propertly
+ text = _DoGithubCodeBlocks(text);
+
+ // Turn block-level HTML blocks into hash entries
+ text = _HashHTMLBlocks(text);
+
+ // Strip link definitions, store in hashes.
+ text = _StripLinkDefinitions(text);
+
+ text = _RunBlockGamut(text);
+
+ text = _UnescapeSpecialChars(text);
+
+ // attacklab: Restore dollar signs
+ text = text.replace(/~D/g,"$$");
+
+ // attacklab: Restore tildes
+ text = text.replace(/~T/g,"~");
+
+ // Run output modifiers
+ Showdown.forEach(g_output_modifiers, function(x){
+ text = _ExecuteExtension(x, text);
+ });
+
+ return text;
+};
+//
+// Options:
+//
+
+// Parse extensions options into separate arrays
+if (converter_options && converter_options.extensions) {
+
+ var self = this;
+
+ // Iterate over each plugin
+ Showdown.forEach(converter_options.extensions, function(plugin){
+
+ // Assume it's a bundled plugin if a string is given
+ if (typeof plugin === 'string') {
+ plugin = Showdown.extensions[stdExtName(plugin)];
+ }
+
+ if (typeof plugin === 'function') {
+ // Iterate over each extension within that plugin
+ Showdown.forEach(plugin(self), function(ext){
+ // Sort extensions by type
+ if (ext.type) {
+ if (ext.type === 'language' || ext.type === 'lang') {
+ g_lang_extensions.push(ext);
+ } else if (ext.type === 'output' || ext.type === 'html') {
+ g_output_modifiers.push(ext);
+ }
+ } else {
+ // Assume language extension
+ g_output_modifiers.push(ext);
+ }
+ });
+ } else {
+ throw "Extension '" + plugin + "' could not be loaded. It was either not found or is not a valid extension.";
+ }
+ });
+}
+
+
+var _ExecuteExtension = function(ext, text) {
+ if (ext.regex) {
+ var re = new RegExp(ext.regex, 'g');
+ return text.replace(re, ext.replace);
+ } else if (ext.filter) {
+ return ext.filter(text);
+ }
+};
+
+var _StripLinkDefinitions = function(text) {
+//
+// Strips link definitions from text, stores the URLs and titles in
+// hash references.
+//
+
+ // Link defs are in the form: ^[id]: url "optional title"
+
+ /*
+ var text = text.replace(/
+ ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1
+ [ \t]*
+ \n? // maybe *one* newline
+ [ \t]*
+ (\S+?)>? // url = $2
+ [ \t]*
+ \n? // maybe one newline
+ [ \t]*
+ (?:
+ (\n*) // any lines skipped = $3 attacklab: lookbehind removed
+ ["(]
+ (.+?) // title = $4
+ [")]
+ [ \t]*
+ )? // title is optional
+ (?:\n+|$)
+ /gm,
+ function(){...});
+ */
+
+ // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
+ text += "~0";
+
+ text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|(?=~0))/gm,
+ function (wholeMatch,m1,m2,m3,m4) {
+ m1 = m1.toLowerCase();
+ g_urls[m1] = _EncodeAmpsAndAngles(m2); // Link IDs are case-insensitive
+ if (m3) {
+ // Oops, found blank lines, so it's not a title.
+ // Put back the parenthetical statement we stole.
+ return m3+m4;
+ } else if (m4) {
+ g_titles[m1] = m4.replace(/"/g,""");
+ }
+
+ // Completely remove the definition from the text
+ return "";
+ }
+ );
+
+ // attacklab: strip sentinel
+ text = text.replace(/~0/,"");
+
+ return text;
+}
+
+
+var _HashHTMLBlocks = function(text) {
+ // attacklab: Double up blank lines to reduce lookaround
+ text = text.replace(/\n/g,"\n\n");
+
+ // Hashify HTML blocks:
+ // We only want to do this for block-level HTML tags, such as headers,
+ // lists, and tables. That's because we still want to wrap
. It was easier to make a special case than
+ // to make the other regex more complicated.
+
+ /*
+ text = text.replace(/
+ ( // save in $1
+ \n\n // Starting after a blank line
+ [ ]{0,3}
+ (<(hr) // start tag = $2
+ \b // word break
+ ([^<>])*? //
+ \/?>) // the matching end tag
+ [ \t]*
+ (?=\n{2,}) // followed by a blank line
+ )
+ /g,hashElement);
+ */
+ text = text.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,hashElement);
+
+ // Special case for standalone HTML comments:
+
+ /*
+ text = text.replace(/
+ ( // save in $1
+ \n\n // Starting after a blank line
+ [ ]{0,3} // attacklab: g_tab_width - 1
+
+ [ \t]*
+ (?=\n{2,}) // followed by a blank line
+ )
+ /g,hashElement);
+ */
+ text = text.replace(/(\n\n[ ]{0,3}[ \t]*(?=\n{2,}))/g,hashElement);
+
+ // PHP and ASP-style processor instructions (...?> and <%...%>)
+
+ /*
+ text = text.replace(/
+ (?:
+ \n\n // Starting after a blank line
+ )
+ ( // save in $1
+ [ ]{0,3} // attacklab: g_tab_width - 1
+ (?:
+ <([?%]) // $2
+ [^\r]*?
+ \2>
+ )
+ [ \t]*
+ (?=\n{2,}) // followed by a blank line
+ )
+ /g,hashElement);
+ */
+ text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,hashElement);
+
+ // attacklab: Undo double lines (see comment at top of this function)
+ text = text.replace(/\n\n/g,"\n");
+ return text;
+}
+
+var hashElement = function(wholeMatch,m1) {
+ var blockText = m1;
+
+ // Undo double lines
+ blockText = blockText.replace(/\n\n/g,"\n");
+ blockText = blockText.replace(/^\n/,"");
+
+ // strip trailing blank lines
+ blockText = blockText.replace(/\n+$/g,"");
+
+ // Replace the element text with a marker ("~KxK" where x is its key)
+ blockText = "\n\n~K" + (g_html_blocks.push(blockText)-1) + "K\n\n";
+
+ return blockText;
+};
+
+var _RunBlockGamut = function(text) {
+//
+// These are all the transformations that form block-level
+// tags like paragraphs, headers, and list items.
+//
+ text = _DoHeaders(text);
+
+ // Do Horizontal Rules:
+ var key = hashBlock("
");
+ text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm,key);
+ text = text.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm,key);
+ text = text.replace(/^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$/gm,key);
+
+ text = _DoLists(text);
+ text = _DoCodeBlocks(text);
+ text = _DoBlockQuotes(text);
+
+ // We already ran _HashHTMLBlocks() before, in Markdown(), but that
+ // was to escape raw HTML in the original Markdown source. This time,
+ // we're escaping the markup we've just created, so that we don't wrap
+ //
\n");
+
+ return text;
+}
+
+var _EscapeSpecialCharsWithinTagAttributes = function(text) {
+//
+// Within tags -- meaning between < and > -- encode [\ ` * _] so they
+// don't conflict with their use in Markdown for code, italics and strong.
+//
+
+ // Build a regex to find HTML tags and comments. See Friedl's
+ // "Mastering Regular Expressions", 2nd Ed., pp. 200-201.
+ var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|)/gi;
+
+ text = text.replace(regex, function(wholeMatch) {
+ var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g,"$1`");
+ tag = escapeCharacters(tag,"\\`*_");
+ return tag;
+ });
+
+ return text;
+}
+
+var _DoAnchors = function(text) {
+//
+// Turn Markdown link shortcuts into XHTML tags.
+//
+ //
+ // First, handle reference-style links: [link text] [id]
+ //
+
+ /*
+ text = text.replace(/
+ ( // wrap whole match in $1
+ \[
+ (
+ (?:
+ \[[^\]]*\] // allow brackets nested one level
+ |
+ [^\[] // or anything else
+ )*
+ )
+ \]
+
+ [ ]? // one optional space
+ (?:\n[ ]*)? // one optional newline followed by spaces
+
+ \[
+ (.*?) // id = $3
+ \]
+ )()()()() // pad remaining backreferences
+ /g,_DoAnchors_callback);
+ */
+ text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeAnchorTag);
+
+ //
+ // Next, inline-style links: [link text](url "optional title")
+ //
+
+ /*
+ text = text.replace(/
+ ( // wrap whole match in $1
+ \[
+ (
+ (?:
+ \[[^\]]*\] // allow brackets nested one level
+ |
+ [^\[\]] // or anything else
+ )
+ )
+ \]
+ \( // literal paren
+ [ \t]*
+ () // no id, so leave $3 empty
+ (.*?)>? // href = $4
+ [ \t]*
+ ( // $5
+ (['"]) // quote char = $6
+ (.*?) // Title = $7
+ \6 // matching quote
+ [ \t]* // ignore any spaces/tabs between closing quote and )
+ )? // title is optional
+ \)
+ )
+ /g,writeAnchorTag);
+ */
+ text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()(.*?(?:\(.*?\).*?)?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeAnchorTag);
+
+ //
+ // Last, handle reference-style shortcuts: [link text]
+ // These must come last in case you've also got [link test][1]
+ // or [link test](/foo)
+ //
+
+ /*
+ text = text.replace(/
+ ( // wrap whole match in $1
+ \[
+ ([^\[\]]+) // link text = $2; can't contain '[' or ']'
+ \]
+ )()()()()() // pad rest of backreferences
+ /g, writeAnchorTag);
+ */
+ text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag);
+
+ return text;
+}
+
+var writeAnchorTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) {
+ if (m7 == undefined) m7 = "";
+ var whole_match = m1;
+ var link_text = m2;
+ var link_id = m3.toLowerCase();
+ var url = m4;
+ var title = m7;
+
+ if (url == "") {
+ if (link_id == "") {
+ // lower-case and turn embedded newlines into spaces
+ link_id = link_text.toLowerCase().replace(/ ?\n/g," ");
+ }
+ url = "#"+link_id;
+
+ if (g_urls[link_id] != undefined) {
+ url = g_urls[link_id];
+ if (g_titles[link_id] != undefined) {
+ title = g_titles[link_id];
+ }
+ }
+ else {
+ if (whole_match.search(/\(\s*\)$/m)>-1) {
+ // Special case for explicit empty url
+ url = "";
+ } else {
+ return whole_match;
+ }
+ }
+ }
+
+ url = escapeCharacters(url,"*_");
+ var result = "" + link_text + "";
+
+ return result;
+}
+
+
+var _DoImages = function(text) {
+//
+// Turn Markdown image shortcuts into tags.
+//
+
+ //
+ // First, handle reference-style labeled images: ![alt text][id]
+ //
+
+ /*
+ text = text.replace(/
+ ( // wrap whole match in $1
+ !\[
+ (.*?) // alt text = $2
+ \]
+
+ [ ]? // one optional space
+ (?:\n[ ]*)? // one optional newline followed by spaces
+
+ \[
+ (.*?) // id = $3
+ \]
+ )()()()() // pad rest of backreferences
+ /g,writeImageTag);
+ */
+ text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeImageTag);
+
+ //
+ // Next, handle inline images: 
+ // Don't forget: encode * and _
+
+ /*
+ text = text.replace(/
+ ( // wrap whole match in $1
+ !\[
+ (.*?) // alt text = $2
+ \]
+ \s? // One optional whitespace character
+ \( // literal paren
+ [ \t]*
+ () // no id, so leave $3 empty
+ (\S+?)>? // src url = $4
+ [ \t]*
+ ( // $5
+ (['"]) // quote char = $6
+ (.*?) // title = $7
+ \6 // matching quote
+ [ \t]*
+ )? // title is optional
+ \)
+ )
+ /g,writeImageTag);
+ */
+ text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()(\S+?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeImageTag);
+
+ return text;
+}
+
+var writeImageTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) {
+ var whole_match = m1;
+ var alt_text = m2;
+ var link_id = m3.toLowerCase();
+ var url = m4;
+ var title = m7;
+
+ if (!title) title = "";
+
+ if (url == "") {
+ if (link_id == "") {
+ // lower-case and turn embedded newlines into spaces
+ link_id = alt_text.toLowerCase().replace(/ ?\n/g," ");
+ }
+ url = "#"+link_id;
+
+ if (g_urls[link_id] != undefined) {
+ url = g_urls[link_id];
+ if (g_titles[link_id] != undefined) {
+ title = g_titles[link_id];
+ }
+ }
+ else {
+ return whole_match;
+ }
+ }
+
+ alt_text = alt_text.replace(/"/g,""");
+ url = escapeCharacters(url,"*_");
+ var result = "
";
+
+ return result;
+}
+
+
+var _DoHeaders = function(text) {
+
+ // Setext-style headers:
+ // Header 1
+ // ========
+ //
+ // Header 2
+ // --------
+ //
+ text = text.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm,
+ function(wholeMatch,m1){return hashBlock('
' + _RunSpanGamut(m1) + "
");});
+
+ text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,
+ function(matchFound,m1){return hashBlock('' + _RunSpanGamut(m1) + "
");});
+
+ // atx-style headers:
+ // # Header 1
+ // ## Header 2
+ // ## Header 2 with closing hashes ##
+ // ...
+ // ###### Header 6
+ //
+
+ /*
+ text = text.replace(/
+ ^(\#{1,6}) // $1 = string of #'s
+ [ \t]*
+ (.+?) // $2 = Header text
+ [ \t]*
+ \#* // optional closing #'s (not counted)
+ \n+
+ /gm, function() {...});
+ */
+
+ text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,
+ function(wholeMatch,m1,m2) {
+ var h_level = m1.length;
+ return hashBlock("` blocks.
+//
+
+ /*
+ text = text.replace(text,
+ /(?:\n\n|^)
+ ( // $1 = the code block -- one or more lines, starting with a space/tab
+ (?:
+ (?:[ ]{4}|\t) // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
+ .*\n+
+ )+
+ )
+ (\n*[ ]{0,3}[^ \t\n]|(?=~0)) // attacklab: g_tab_width
+ /g,function(){...});
+ */
+
+ // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
+ text += "~0";
+
+ text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
+ function(wholeMatch,m1,m2) {
+ var codeblock = m1;
+ var nextChar = m2;
+
+ codeblock = _EncodeCode( _Outdent(codeblock));
+ codeblock = _Detab(codeblock);
+ codeblock = codeblock.replace(/^\n+/g,""); // trim leading newlines
+ codeblock = codeblock.replace(/\n+$/g,""); // trim trailing whitespace
+
+ codeblock = "
";
+
+ return hashBlock(codeblock) + nextChar;
+ }
+ );
+
+ // attacklab: strip sentinel
+ text = text.replace(/~0/,"");
+
+ return text;
+};
+
+var _DoGithubCodeBlocks = function(text) {
+//
+// Process Github-style code blocks
+// Example:
+// ```ruby
+// def hello_world(x)
+// puts "Hello, #{x}"
+// end
+// ```
+//
+
+
+ // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
+ text += "~0";
+
+ text = text.replace(/(?:^|\n)```(.*)\n([\s\S]*?)\n```/g,
+ function(wholeMatch,m1,m2) {
+ var language = m1;
+ var codeblock = m2;
+
+ codeblock = _EncodeCode(codeblock);
+ codeblock = _Detab(codeblock);
+ codeblock = codeblock.replace(/^\n+/g,""); // trim leading newlines
+ codeblock = codeblock.replace(/\n+$/g,""); // trim trailing whitespace
+
+ codeblock = "" + codeblock + "\n
";
+
+ return hashBlock(codeblock);
+ }
+ );
+
+ // attacklab: strip sentinel
+ text = text.replace(/~0/,"");
+
+ return text;
+}
+
+var hashBlock = function(text) {
+ text = text.replace(/(^\n+|\n+$)/g,"");
+ return "\n\n~K" + (g_html_blocks.push(text)-1) + "K\n\n";
+}
+
+var _DoCodeSpans = function(text) {
+//
+// * Backtick quotes are used for " + codeblock + "\n spans.
+//
+// * You can use multiple backticks as the delimiters if you want to
+// include literal backticks in the code span. So, this input:
+//
+// Just type ``foo `bar` baz`` at the prompt.
+//
+// Will translate to:
+//
+// foo `bar` baz at the prompt.`bar` ...
+//
+
+ /*
+ text = text.replace(/
+ (^|[^\\]) // Character before opening ` can't be a backslash
+ (`+) // $2 = Opening run of `
+ ( // $3 = The code block
+ [^\r]*?
+ [^`] // attacklab: work around lack of lookbehind
+ )
+ \2 // Matching closer
+ (?!`)
+ /gm, function(){...});
+ */
+
+ text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,
+ function(wholeMatch,m1,m2,m3,m4) {
+ var c = m3;
+ c = c.replace(/^([ \t]*)/g,""); // leading whitespace
+ c = c.replace(/[ \t]*$/g,""); // trailing whitespace
+ c = _EncodeCode(c);
+ return m1+""+c+"";
+ });
+
+ return text;
+}
+
+var _EncodeCode = function(text) {
+//
+// Encode/escape certain characters inside Markdown code runs.
+// The point is that in code, these characters are literals,
+// and lose their special Markdown meanings.
+//
+ // Encode all ampersands; HTML entities are not
+ // entities within a Markdown code span.
+ text = text.replace(/&/g,"&");
+
+ // Do the angle bracket song and dance:
+ text = text.replace(//g,">");
+
+ // Now, escape characters that are magic in Markdown:
+ text = escapeCharacters(text,"\*_{}[]\\",false);
+
+// jj the line above breaks this:
+//---
+
+//* Item
+
+// 1. Subitem
+
+// special char: *
+//---
+
+ return text;
+}
+
+
+var _DoItalicsAndBold = function(text) {
+
+ // must go first:
+ text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g,
+ "$2");
+
+ text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g,
+ "$2");
+
+ return text;
+}
+
+
+var _DoBlockQuotes = function(text) {
+
+ /*
+ text = text.replace(/
+ ( // Wrap whole match in $1
+ (
+ ^[ \t]*>[ \t]? // '>' at the start of a line
+ .+\n // rest of the first line
+ (.+\n)* // subsequent consecutive lines
+ \n* // blanks
+ )+
+ )
+ /gm, function(){...});
+ */
+
+ text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,
+ function(wholeMatch,m1) {
+ var bq = m1;
+
+ // attacklab: hack around Konqueror 3.5.4 bug:
+ // "----------bug".replace(/^-/g,"") == "bug"
+
+ bq = bq.replace(/^[ \t]*>[ \t]?/gm,"~0"); // trim one level of quoting
+
+ // attacklab: clean up hack
+ bq = bq.replace(/~0/g,"");
+
+ bq = bq.replace(/^[ \t]+$/gm,""); // trim whitespace-only lines
+ bq = _RunBlockGamut(bq); // recurse
+
+ bq = bq.replace(/(^|\n)/g,"$1 ");
+ // These leading spaces screw with content, so we need to fix that:
+ bq = bq.replace(
+ /(\s*
[^\r]+?<\/pre>)/gm,
+ function(wholeMatch,m1) {
+ var pre = m1;
+ // attacklab: hack around Konqueror 3.5.4 bug:
+ pre = pre.replace(/^ /mg,"~0");
+ pre = pre.replace(/~0/g,"");
+ return pre;
+ });
+
+ return hashBlock("\n" + bq + "\n
");
+ });
+ return text;
+}
+
+
+var _FormParagraphs = function(text) {
+//
+// Params:
+// $text - string to process with html ...`, because the
+ // browser will insert a TBODY. If we just `createElement("table")` and
+ // `createElement("tr")`, on the other hand, no TBODY is necessary
+ // (assuming IE 8+).
+ return OPTIMIZABLE.NONE;
+ }
+
+ var children = tag.children;
+ for (var i = 0; i < children.length; i++)
+ if (this.visit(children[i]) !== OPTIMIZABLE.FULL)
+ return OPTIMIZABLE.PARTS;
+
+ if (this.visitAttributes(tag.attrs) !== OPTIMIZABLE.FULL)
+ return OPTIMIZABLE.PARTS;
+
+ return OPTIMIZABLE.FULL;
+ },
+ visitAttributes: function (attrs) {
+ if (attrs) {
+ var isArray = HTML.isArray(attrs);
+ for (var i = 0; i < (isArray ? attrs.length : 1); i++) {
+ var a = (isArray ? attrs[i] : attrs);
+ if ((typeof a !== 'object') || (a instanceof HTMLTools.TemplateTag))
+ return OPTIMIZABLE.PARTS;
+ for (var k in a)
+ if (this.visit(a[k]) !== OPTIMIZABLE.FULL)
+ return OPTIMIZABLE.PARTS;
+ }
+ }
+ return OPTIMIZABLE.FULL;
+ }
+});
+
+var getOptimizability = function (content) {
+ return (new CanOptimizeVisitor).visit(content);
+};
+
+var toRaw = function (x) {
+ return HTML.Raw(HTML.toHTML(x));
+};
+
+var TreeTransformer = HTML.TransformingVisitor.extend();
+TreeTransformer.def({
+ visitAttributes: function (attrs/*, ...*/) {
+ // pass template tags through by default
+ if (attrs instanceof HTMLTools.TemplateTag)
+ return attrs;
+
+ return HTML.TransformingVisitor.prototype.visitAttributes.apply(
+ this, arguments);
+ }
+});
+
+// Replace parts of the HTMLjs tree that have no template tags (or
+// tricky HTML tags) with HTML.Raw objects containing raw HTML.
+var OptimizingVisitor = TreeTransformer.extend();
+OptimizingVisitor.def({
+ visitNull: toRaw,
+ visitPrimitive: toRaw,
+ visitComment: toRaw,
+ visitCharRef: toRaw,
+ visitArray: function (array) {
+ var optimizability = getOptimizability(array);
+ if (optimizability === OPTIMIZABLE.FULL) {
+ return toRaw(array);
+ } else if (optimizability === OPTIMIZABLE.PARTS) {
+ return TreeTransformer.prototype.visitArray.call(this, array);
+ } else {
+ return array;
+ }
+ },
+ visitTag: function (tag) {
+ var optimizability = getOptimizability(tag);
+ if (optimizability === OPTIMIZABLE.FULL) {
+ return toRaw(tag);
+ } else if (optimizability === OPTIMIZABLE.PARTS) {
+ return TreeTransformer.prototype.visitTag.call(this, tag);
+ } else {
+ return tag;
+ }
+ },
+ visitChildren: function (children) {
+ // don't optimize the children array into a Raw object!
+ return TreeTransformer.prototype.visitArray.call(this, children);
+ },
+ visitAttributes: function (attrs) {
+ return attrs;
+ }
+});
+
+// Combine consecutive HTML.Raws. Remove empty ones.
+var RawCompactingVisitor = TreeTransformer.extend();
+RawCompactingVisitor.def({
+ visitArray: function (array) {
+ var result = [];
+ for (var i = 0; i < array.length; i++) {
+ var item = array[i];
+ if ((item instanceof HTML.Raw) &&
+ ((! item.value) ||
+ (result.length &&
+ (result[result.length - 1] instanceof HTML.Raw)))) {
+ // two cases: item is an empty Raw, or previous item is
+ // a Raw as well. In the latter case, replace the previous
+ // Raw with a longer one that includes the new Raw.
+ if (item.value) {
+ result[result.length - 1] = HTML.Raw(
+ result[result.length - 1].value + item.value);
+ }
+ } else {
+ result.push(item);
+ }
+ }
+ return result;
+ }
+});
+
+// Replace pointless Raws like `HTMl.Raw('foo')` that contain no special
+// characters with simple strings.
+var RawReplacingVisitor = TreeTransformer.extend();
+RawReplacingVisitor.def({
+ visitRaw: function (raw) {
+ var html = raw.value;
+ if (html.indexOf('&') < 0 && html.indexOf('<') < 0) {
+ return html;
+ } else {
+ return raw;
+ }
+ }
+});
+
+SpacebarsCompiler.optimize = function (tree) {
+ tree = (new OptimizingVisitor).visit(tree);
+ tree = (new RawCompactingVisitor).visit(tree);
+ tree = (new RawReplacingVisitor).visit(tree);
+ return tree;
+};
diff --git a/packages/spacebars-compiler/package.js b/packages/spacebars-compiler/package.js
index f5159a9774..2477e7c3c1 100644
--- a/packages/spacebars-compiler/package.js
+++ b/packages/spacebars-compiler/package.js
@@ -3,26 +3,27 @@ Package.describe({
});
Package.on_use(function (api) {
- api.use('spacebars-common');
- api.imply('spacebars-common');
+ api.export('SpacebarsCompiler');
- // we attach stuff to the global symbol `HTML`, exported
- // by `htmljs` via `html-tools`, so we both use and effectively
- // imply it.
+ api.use('htmljs');
api.use('html-tools');
- api.imply('html-tools');
+ api.use('blaze-tools');
api.use('underscore');
api.use('minifiers', ['server']);
- api.add_files(['tokens.js', 'tojs.js', 'templatetag.js',
- 'spacebars-compiler.js']);
+ api.add_files(['templatetag.js',
+ 'optimizer.js',
+ 'codegen.js',
+ 'compiler.js']);
});
Package.on_test(function (api) {
api.use('underscore');
api.use('spacebars-compiler');
api.use('tinytest');
+ api.use('blaze-tools');
+ api.use('coffeescript');
api.add_files('spacebars_tests.js');
api.add_files('compile_tests.js');
- api.add_files('token_tests.js');
+ api.add_files('compiler_output_tests.coffee');
});
diff --git a/packages/spacebars-compiler/spacebars-compiler.js b/packages/spacebars-compiler/spacebars-compiler.js
deleted file mode 100644
index a24f2a11ab..0000000000
--- a/packages/spacebars-compiler/spacebars-compiler.js
+++ /dev/null
@@ -1,544 +0,0 @@
-
-
-
-Spacebars.parse = function (input) {
-
- var tree = HTMLTools.parseFragment(
- input,
- { getSpecialTag: TemplateTag.parseCompleteTag });
-
- return tree;
-};
-
-// ============================================================
-// Optimizer for optimizing HTMLjs into raw HTML string when
-// it doesn't contain template tags.
-
-var optimize = function (tree) {
-
- var pushRawHTML = function (array, html) {
- var N = array.length;
- if (N > 0 && (array[N-1] instanceof HTML.Raw)) {
- array[N-1] = HTML.Raw(array[N-1].value + html);
- } else {
- array.push(HTML.Raw(html));
- }
- };
-
- var isPureChars = function (html) {
- return (html.indexOf('&') < 0 && html.indexOf('<') < 0);
- };
-
- // Returns `null` if no specials are found in the array, so that the
- // parent can perform the actual optimization. Otherwise, returns
- // an array of parts which have been optimized as much as possible.
- // `forceOptimize` forces the latter case.
- var optimizeArrayParts = function (array, optimizePartsFunc, forceOptimize) {
- var result = null;
- if (forceOptimize)
- result = [];
- for (var i = 0, N = array.length; i < N; i++) {
- var part = optimizePartsFunc(array[i]);
- if (part !== null) {
- // something special found
- if (result === null) {
- // This is our first special item. Stringify the other parts.
- result = [];
- for (var j = 0; j < i; j++)
- pushRawHTML(result, HTML.toHTML(array[j]));
- }
- result.push(part);
- } else {
- // just plain HTML found
- if (result !== null) {
- // we've already found something special, so convert this to Raw
- pushRawHTML(result, HTML.toHTML(array[i]));
- }
- }
- }
- if (result !== null) {
- // clean up unnecessary HTML.Raw wrappers around pure character data
- for (var j = 0; j < result.length; j++) {
- if ((result[j] instanceof HTML.Raw) &&
- isPureChars(result[j].value))
- // replace HTML.Raw with simple string
- result[j] = result[j].value;
- }
- }
- return result;
- };
-
- var doesAttributeValueHaveSpecials = function (v) {
- if (v instanceof HTMLTools.Special)
- return true;
- if (typeof v === 'function')
- return true;
-
- if (v instanceof Array) {
- for (var i = 0; i < v.length; i++)
- if (doesAttributeValueHaveSpecials(v[i]))
- return true;
- return false;
- }
-
- return false;
- };
-
- var optimizeParts = function (node) {
- // If we have nothing special going on, returns `null` (so that the
- // parent can optimize). Otherwise returns a replacement for `node`
- // with optimized parts.
- if ((node == null) || (typeof node === 'string') ||
- (node instanceof HTML.CharRef) || (node instanceof HTML.Comment) ||
- (node instanceof HTML.Raw)) {
- // not special; let parent decide how whether to optimize
- return null;
- } else if (node instanceof HTML.Tag) {
- var tagName = node.tagName;
- if (tagName === 'textarea' ||
- (! (HTML.isKnownElement(tagName) &&
- ! HTML.isKnownSVGElement(tagName)))) {
- // optimizing into a TEXTAREA's RCDATA would require being a little
- // more clever. foreign elements like SVG can't be stringified for
- // innerHTML.
- return node;
- }
-
- var mustOptimize = false;
-
- // Avoid ever producing HTML containing ` ...`, because the
- // browser will insert a TBODY. If we just `createElement("table")` and
- // `createElement("tr")`, on the other hand, no TBODY is necessary
- // (assuming IE 8+).
- if (tagName === 'table')
- mustOptimize = true;
-
- if (node.attrs && ! mustOptimize) {
- var attrs = node.attrs;
- for (var k in attrs) {
- if (doesAttributeValueHaveSpecials(attrs[k])) {
- mustOptimize = true;
- break;
- }
- }
- }
-
- var newChildren = optimizeArrayParts(node.children, optimizeParts, mustOptimize);
-
- if (newChildren === null)
- return null;
-
- var newTag = HTML.getTag(node.tagName).apply(null, newChildren);
- newTag.attrs = node.attrs;
-
- return newTag;
-
- } else if (node instanceof Array) {
- return optimizeArrayParts(node, optimizeParts);
- } else {
- return node;
- }
- };
-
- var optTree = optimizeParts(tree);
- if (optTree !== null)
- // tree was optimized in parts
- return optTree;
-
- optTree = HTML.Raw(HTML.toHTML(tree));
-
- if (isPureChars(optTree.value))
- return optTree.value;
-
- return optTree;
-};
-
-// ============================================================
-// Code-generation of template tags
-
-var builtInBlockHelpers = {
- 'if': 'UI.If',
- 'unless': 'UI.Unless',
- 'with': 'Spacebars.With',
- 'each': 'UI.Each'
-};
-
-// Some `UI.*` paths are special in that they generate code that
-// doesn't folow the normal lookup rules for dotted symbols. The
-// following names must be prefixed with `UI.` when you use them in a
-// template.
-var builtInUIPaths = {
- // `template` is a local variable defined in the generated render
- // function for the template in which `UI.contentBlock` (or
- // `UI.elseBlock`) is invoked. `template` is a reference to the
- // template itself.
- 'contentBlock': 'template.__content',
- 'elseBlock': 'template.__elseContent',
-
- // `Template` is the global template namespace. If you define a
- // template named `foo` in Spacebars, it gets defined as
- // `Template.foo` in JavaScript.
- 'dynamic': 'Template.__dynamic'
-};
-
-// A "reserved name" can't be used as a name. This
-// function is used by the template file scanner.
-Spacebars.isReservedName = function (name) {
- return builtInBlockHelpers.hasOwnProperty(name);
-};
-
-var codeGenTemplateTag = function (tag) {
- if (tag.position === HTMLTools.TEMPLATE_TAG_POSITION.IN_START_TAG) {
- // only `tag.type === 'DOUBLE'` allowed (by earlier validation)
- return HTML.EmitCode('function () { return ' +
- codeGenMustache(tag.path, tag.args, 'attrMustache')
- + '; }');
- } else {
- if (tag.type === 'DOUBLE') {
- return HTML.EmitCode('function () { return ' +
- codeGenMustache(tag.path, tag.args) + '; }');
- } else if (tag.type === 'TRIPLE') {
- return HTML.EmitCode('function () { return Spacebars.makeRaw(' +
- codeGenMustache(tag.path, tag.args) + '); }');
- } else if (tag.type === 'INCLUSION' || tag.type === 'BLOCKOPEN') {
- var path = tag.path;
-
- if (tag.type === 'BLOCKOPEN' &&
- builtInBlockHelpers.hasOwnProperty(path[0])) {
- // if, unless, with, each.
- //
- // If someone tries to do `{{> if}}`, we don't
- // get here, but an error is thrown when we try to codegen the path.
-
- // Note: If we caught these errors earlier, while scanning, we'd be able to
- // provide nice line numbers.
- if (path.length > 1)
- throw new Error("Unexpected dotted path beginning with " + path[0]);
- if (! tag.args.length)
- throw new Error("#" + path[0] + " requires an argument");
-
- var codeParts = codeGenInclusionParts(tag);
- var dataFunc = codeParts.dataFunc; // must exist (tag.args.length > 0)
- var contentBlock = codeParts.content; // must exist
- var elseContentBlock = codeParts.elseContent; // may not exist
-
- var callArgs = [dataFunc, contentBlock];
- if (elseContentBlock)
- callArgs.push(elseContentBlock);
-
- return HTML.EmitCode(
- builtInBlockHelpers[path[0]] + '(' + callArgs.join(', ') + ')');
-
- } else {
- var compCode = codeGenPath(path, {lookupTemplate: true});
-
- if (path.length !== 1) {
- // path code may be reactive; wrap it
- compCode = 'function () { return ' + compCode + '; }';
- }
-
- var codeParts = codeGenInclusionParts(tag);
- var dataFunc = codeParts.dataFunc;
- var content = codeParts.content;
- var elseContent = codeParts.elseContent;
-
- var includeArgs = [compCode];
- if (content) {
- includeArgs.push(content);
- if (elseContent)
- includeArgs.push(elseContent);
- }
-
- var includeCode =
- 'Spacebars.include(' + includeArgs.join(', ') + ')';
-
- if (dataFunc) {
- includeCode =
- 'Spacebars.TemplateWith(' + dataFunc + ', UI.block(' +
- Spacebars.codeGen(HTML.EmitCode(includeCode)) + '))';
- }
-
- if (path[0] === 'UI' &&
- (path[1] === 'contentBlock' || path[1] === 'elseBlock')) {
- includeCode = 'UI.InTemplateScope(template, ' + includeCode + ')';
- }
-
- return HTML.EmitCode(includeCode);
- }
- } else {
- // Can't get here; TemplateTag validation should catch any
- // inappropriate tag types that might come out of the parser.
- throw new Error("Unexpected template tag type: " + tag.type);
- }
- }
-};
-
-var makeObjectLiteral = function (obj) {
- var parts = [];
- for (var k in obj)
- parts.push(toObjectLiteralKey(k) + ': ' + obj[k]);
- return '{' + parts.join(', ') + '}';
-};
-
-// `path` is an array of at least one string.
-//
-// If `path.length > 1`, the generated code may be reactive
-// (i.e. it may invalidate the current computation).
-//
-// No code is generated to call the result if it's a function.
-//
-// Options:
-//
-// - lookupTemplate {Boolean} If true, generated code also looks in
-// the list of templates. (After helpers, before data context).
-// Used when generating code for `{{> foo}}` or `{{#foo}}`. Only
-// used for non-dotted paths.
-var codeGenPath = function (path, opts) {
- if (builtInBlockHelpers.hasOwnProperty(path[0]))
- throw new Error("Can't use the built-in '" + path[0] + "' here");
- // Let `{{#if UI.contentBlock}}` check whether this template was invoked via
- // inclusion or as a block helper, in addition to supporting
- // `{{> UI.contentBlock}}`.
- if (path.length >= 2 &&
- path[0] === 'UI' && builtInUIPaths.hasOwnProperty(path[1])) {
- if (path.length > 2)
- throw new Error("Unexpected dotted path beginning with " +
- path[0] + '.' + path[1]);
- return builtInUIPaths[path[1]];
- }
-
- var args = [toJSLiteral(path[0])];
- var lookupMethod = 'lookup';
- if (opts && opts.lookupTemplate && path.length === 1)
- lookupMethod = 'lookupTemplate';
- var code = 'self.' + lookupMethod + '(' + args.join(', ') + ')';
-
- if (path.length > 1) {
- code = 'Spacebars.dot(' + code + ', ' +
- _.map(path.slice(1), toJSLiteral).join(', ') + ')';
- }
-
- return code;
-};
-
-// Generates code for an `[argType, argValue]` argument spec,
-// ignoring the third element (keyword argument name) if present.
-//
-// The resulting code may be reactive (in the case of a PATH of
-// more than one element) and is not wrapped in a closure.
-var codeGenArgValue = function (arg) {
- var argType = arg[0];
- var argValue = arg[1];
-
- var argCode;
- switch (argType) {
- case 'STRING':
- case 'NUMBER':
- case 'BOOLEAN':
- case 'NULL':
- argCode = toJSLiteral(argValue);
- break;
- case 'PATH':
- argCode = codeGenPath(argValue);
- break;
- default:
- // can't get here
- throw new Error("Unexpected arg type: " + argType);
- }
-
- return argCode;
-};
-
-// Generates a call to `Spacebars.fooMustache` on evaluated arguments.
-// The resulting code has no function literals and must be wrapped in
-// one for fine-grained reactivity.
-var codeGenMustache = function (path, args, mustacheType) {
- var nameCode = codeGenPath(path);
- var argCode = codeGenMustacheArgs(args);
- var mustache = (mustacheType || 'mustache');
-
- return 'Spacebars.' + mustache + '(' + nameCode +
- (argCode ? ', ' + argCode.join(', ') : '') + ')';
-};
-
-// returns: array of source strings, or null if no
-// args at all.
-var codeGenMustacheArgs = function (tagArgs) {
- var kwArgs = null; // source -> source
- var args = null; // [source]
-
- // tagArgs may be null
- _.each(tagArgs, function (arg) {
- var argCode = codeGenArgValue(arg);
-
- if (arg.length > 2) {
- // keyword argument (represented as [type, value, name])
- kwArgs = (kwArgs || {});
- kwArgs[arg[2]] = argCode;
- } else {
- // positional argument
- args = (args || []);
- args.push(argCode);
- }
- });
-
- // put kwArgs in options dictionary at end of args
- if (kwArgs) {
- args = (args || []);
- args.push('Spacebars.kw(' + makeObjectLiteral(kwArgs) + ')');
- }
-
- return args;
-};
-
-// Takes an inclusion tag and returns an object containing these properties,
-// all optional, whose values are JS source code:
-//
-// - `dataFunc` - source code of a data function literal
-// - `content` - source code of a content block
-// - `elseContent` - source code of an elseContent block
-//
-// Implements the calling convention for inclusions.
-var codeGenInclusionParts = function (tag) {
- var ret = {};
-
- if ('content' in tag) {
- ret.content = (
- 'UI.block(' + Spacebars.codeGen(tag.content) + ')');
- }
- if ('elseContent' in tag) {
- ret.elseContent = (
- 'UI.block(' + Spacebars.codeGen(tag.elseContent) + ')');
- }
-
- var dataFuncCode = null;
-
- var args = tag.args;
- if (! args.length) {
- // e.g. `{{#foo}}`
- return ret;
- } else if (args[0].length === 3) {
- // keyword arguments only, e.g. `{{> point x=1 y=2}}`
- var dataProps = {};
- _.each(args, function (arg) {
- var argKey = arg[2];
- dataProps[argKey] = 'Spacebars.call(' + codeGenArgValue(arg) + ')';
- });
- dataFuncCode = makeObjectLiteral(dataProps);
- } else if (args[0][0] !== 'PATH') {
- // literal first argument, e.g. `{{> foo "blah"}}`
- //
- // tag validation has confirmed, in this case, that there is only
- // one argument (`args.length === 1`)
- dataFuncCode = codeGenArgValue(args[0]);
- } else if (args.length === 1) {
- // one argument, must be a PATH
- dataFuncCode = 'Spacebars.call(' + codeGenPath(args[0][1]) + ')';
- } else {
- dataFuncCode = codeGenMustache(args[0][1], args.slice(1),
- 'dataMustache');
- }
-
- ret.dataFunc = 'function () { return ' + dataFuncCode + '; }';
-
- return ret;
-};
-
-
-// ============================================================
-// Main compiler
-
-var replaceSpecials = function (node) {
- if (node instanceof HTML.Tag) {
- // potential optimization: don't always create a new tag
- var newChildren = _.map(node.children, replaceSpecials);
- var newTag = HTML.getTag(node.tagName).apply(null, newChildren);
- var oldAttrs = node.attrs;
- var newAttrs = null;
-
- if (oldAttrs) {
- _.each(oldAttrs, function (value, name) {
- if (name.charAt(0) !== '$') {
- newAttrs = (newAttrs || {});
- newAttrs[name] = replaceSpecials(value);
- }
- });
-
- if (oldAttrs.$specials && oldAttrs.$specials.length) {
- newAttrs = (newAttrs || {});
- newAttrs.$dynamic = _.map(oldAttrs.$specials, function (special) {
- return codeGenTemplateTag(special.value);
- });
- }
- }
-
- newTag.attrs = newAttrs;
- return newTag;
- } else if (node instanceof Array) {
- return _.map(node, replaceSpecials);
- } else if (node instanceof HTMLTools.Special) {
- return codeGenTemplateTag(node.value);
- } else {
- return node;
- }
-};
-
-Spacebars.compile = function (input, options) {
- var tree = Spacebars.parse(input);
- return Spacebars.codeGen(tree, options);
-};
-
-Spacebars.codeGen = function (parseTree, options) {
- // is this a template, rather than a block passed to
- // a block helper, say
- var isTemplate = (options && options.isTemplate);
-
- var tree = parseTree;
-
- // The flags `isTemplate` and `isBody` are kind of a hack.
- if (isTemplate || (options && options.isBody)) {
- // optimizing fragments would require being smarter about whether we are
- // in a TEXTAREA, say.
- tree = optimize(tree);
- }
-
- tree = replaceSpecials(tree);
-
- var code = '(function () { var self = this; ';
- if (isTemplate) {
- // support `{{> UI.contentBlock}}` and `{{> UI.elseBlock}}` with
- // lexical scope by creating a local variable in the
- // template's render function.
- code += 'var template = this; ';
- }
- code += 'return ';
- code += HTML.toJS(tree);
- code += '; })';
-
- code = beautify(code);
-
- return code;
-};
-
-var beautify = function (code) {
- if (Package.minifiers && Package.minifiers.UglifyJSMinify) {
- var result = UglifyJSMinify(code,
- { fromString: true,
- mangle: false,
- compress: false,
- output: { beautify: true,
- indent_level: 2,
- width: 80 } });
- var output = result.code;
- // Uglify interprets our expression as a statement and may add a semicolon.
- // Strip trailing semicolon.
- output = output.replace(/;$/, '');
- return output;
- } else {
- // don't actually beautify; no UglifyJS
- return code;
- }
-};
-
-// expose for compiler output tests
-Spacebars._beautify = beautify;
diff --git a/packages/spacebars-compiler/spacebars_tests.js b/packages/spacebars-compiler/spacebars_tests.js
index dd5ecfc1b9..f86fe9fab6 100644
--- a/packages/spacebars-compiler/spacebars_tests.js
+++ b/packages/spacebars-compiler/spacebars_tests.js
@@ -1,4 +1,4 @@
-Tinytest.add("spacebars - stache tags", function (test) {
+Tinytest.add("spacebars-compiler - stache tags", function (test) {
var run = function (input, expected) {
if (typeof expected === "string") {
@@ -6,7 +6,7 @@ Tinytest.add("spacebars - stache tags", function (test) {
var msg = '';
test.throws(function () {
try {
- Spacebars.TemplateTag.parse(input);
+ SpacebarsCompiler.TemplateTag.parse(input);
} catch (e) {
msg = e.message;
throw e;
@@ -14,7 +14,7 @@ Tinytest.add("spacebars - stache tags", function (test) {
});
test.equal(msg.slice(0, expected.length), expected);
} else {
- var result = Spacebars.TemplateTag.parse(input);
+ var result = SpacebarsCompiler.TemplateTag.parse(input);
test.equal(result, expected);
}
};
@@ -167,7 +167,7 @@ Tinytest.add("spacebars - stache tags", function (test) {
});
-Tinytest.add("spacebars - Spacebars.dot", function (test) {
+Tinytest.add("spacebars-compiler - Spacebars.dot", function (test) {
test.equal(Spacebars.dot(null, 'foo'), null);
test.equal(Spacebars.dot('foo', 'foo'), undefined);
test.equal(Spacebars.dot({x:1}, 'x'), 1);
@@ -229,61 +229,61 @@ Tinytest.add("spacebars - Spacebars.dot", function (test) {
//////////////////////////////////////////////////
-Tinytest.add("spacebars - parse", function (test) {
- test.equal(HTML.toJS(Spacebars.parse('{{foo}}')),
- 'HTMLTools.Special({type: "DOUBLE", path: ["foo"]})');
+Tinytest.add("spacebars-compiler - parse", function (test) {
+ test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('{{foo}}')),
+ 'SpacebarsCompiler.TemplateTag({type: "DOUBLE", path: ["foo"]})');
- test.equal(HTML.toJS(Spacebars.parse('{{!foo}}')), 'null');
- test.equal(HTML.toJS(Spacebars.parse('x{{!foo}}y')), '"xy"');
+ test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('{{!foo}}')), 'null');
+ test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('x{{!foo}}y')), '"xy"');
- test.equal(HTML.toJS(Spacebars.parse('{{!--foo--}}')), 'null');
- test.equal(HTML.toJS(Spacebars.parse('x{{!--foo--}}y')), '"xy"');
+ test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('{{!--foo--}}')), 'null');
+ test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('x{{!--foo--}}y')), '"xy"');
- test.equal(HTML.toJS(Spacebars.parse('{{#foo}}x{{/foo}}')),
- 'HTMLTools.Special({type: "BLOCKOPEN", path: ["foo"], content: "x"})');
+ test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('{{#foo}}x{{/foo}}')),
+ 'SpacebarsCompiler.TemplateTag({type: "BLOCKOPEN", path: ["foo"], content: "x"})');
- test.equal(HTML.toJS(Spacebars.parse('{{#foo}}{{#bar}}{{/bar}}{{/foo}}')),
- 'HTMLTools.Special({type: "BLOCKOPEN", path: ["foo"], content: HTMLTools.Special({type: "BLOCKOPEN", path: ["bar"]})})');
+ test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('{{#foo}}{{#bar}}{{/bar}}{{/foo}}')),
+ 'SpacebarsCompiler.TemplateTag({type: "BLOCKOPEN", path: ["foo"], content: SpacebarsCompiler.TemplateTag({type: "BLOCKOPEN", path: ["bar"]})})');
- test.equal(HTML.toJS(Spacebars.parse('
');
});
-Tinytest.add(
- "spacebars - template - jQuery.trigger extraParameters are passed to the event callback",
+Tinytest.add("spacebars-tests - template_tests - jQuery.trigger extraParameters are passed to the event callback",
function (test) {
var tmpl = Template.spacebars_test_jquery_events;
var captured = false;
@@ -1828,7 +1819,7 @@ Tinytest.add(
}
);
-Tinytest.add("spacebars - template - UI.toHTML", function (test) {
+Tinytest.add("spacebars-tests - template_tests - toHTML", function (test) {
// run once, verifying that autoruns are stopped
var once = function (tmplToRender, tmplForHelper, helper, val) {
var count = 0;
@@ -1840,7 +1831,7 @@ Tinytest.add("spacebars - template - UI.toHTML", function (test) {
R.set(val);
tmplForHelper[helper] = getR;
- test.equal(canonicalizeHtml(UI.toHTML(tmplToRender)), "bar");
+ test.equal(canonicalizeHtml(Blaze.toHTML(tmplToRender)), "bar");
test.equal(count, 1);
R.set("");
Deps.flush();
@@ -1862,8 +1853,7 @@ Tinytest.add("spacebars - template - UI.toHTML", function (test) {
Template.spacebars_test_tohtml_each, "foos", ["bar"]);
});
-Tinytest.add(
- "spacebars - template - block comments should not be displayed",
+Tinytest.add("spacebars-tests - template_tests - block comments should not be displayed",
function (test) {
var tmpl = Template.spacebars_test_block_comment;
var div = renderToDiv(tmpl);
@@ -1872,8 +1862,7 @@ Tinytest.add(
);
// Originally reported at https://github.com/meteor/meteor/issues/2046
-Tinytest.add(
- "spacebars - template - {{#with}} with mutated data context",
+Tinytest.add("spacebars-tests - template_tests - {{#with}} with mutated data context",
function (test) {
var tmpl = Template.spacebars_test_with_mutated_data_context;
var foo = {value: 0};
@@ -1892,8 +1881,7 @@ Tinytest.add(
test.equal(canonicalizeHtml(div.innerHTML), '1');
});
-Tinytest.add(
- "spacebars - template - javascript scheme urls",
+Tinytest.add("spacebars-tests - template_tests - javascript scheme urls",
function (test) {
var tmpl = Template.spacebars_test_url_attribute;
var sessionKey = "foo-" + Random.id();
@@ -1963,8 +1951,7 @@ Tinytest.add(
}
);
-Tinytest.add(
- "spacebars - template - event handlers get cleaned up with template is removed",
+Tinytest.add("spacebars-tests - template_tests - event handlers get cleaned up when template is removed",
function (test) {
var tmpl = Template.spacebars_test_event_handler_cleanup;
var subtmpl = Template.spacebars_test_event_handler_cleanup_sub;
@@ -1980,20 +1967,40 @@ Tinytest.add(
var div = renderToDiv(tmpl);
- test.equal(div.$_uievents["click"].handlers.length, 1);
- test.equal(div.$_uievents["mouseover"].handlers.length, 1);
+ test.equal(div.$blaze_events["click"].handlers.length, 1);
+ test.equal(div.$blaze_events["mouseover"].handlers.length, 1);
rv.set(false);
Deps.flush();
- test.equal(div.$_uievents["click"].handlers.length, 0);
- test.equal(div.$_uievents["mouseover"].handlers.length, 0);
+ test.equal(div.$blaze_events["click"].handlers.length, 0);
+ test.equal(div.$blaze_events["mouseover"].handlers.length, 0);
}
);
+// This test makes sure that Blaze correctly finds the controller
+// heirarchy surrounding an element that itself doesn't have a
+// controller.
+Tinytest.add(
+ "spacebars-tests - template_tests - data context in event handlers on elements inside {{#if}}",
+ function (test) {
+ var tmpl = Template.spacebars_test_data_context_for_event_handler_in_if;
+ var data = null;
+ tmpl.events({
+ 'click span': function () {
+ data = this;
+ }
+ });
+ var div = renderToDiv(tmpl);
+ document.body.appendChild(div);
+ clickIt(div.querySelector('span'));
+ test.equal(data, {foo: "bar"});
+ document.body.removeChild(div);
+ });
+
// https://github.com/meteor/meteor/issues/2156
Tinytest.add(
- "spacebars - template - each with inserts inside autorun",
+ "spacebars-tests - template_tests - each with inserts inside autorun",
function (test) {
var tmpl = Template.spacebars_test_each_with_autorun_insert;
var coll = new Meteor.Collection(null);
@@ -2027,7 +2034,7 @@ Tinytest.add(
);
Tinytest.add(
- "spacebars - ui hooks",
+ "spacebars-tests - template_tests - ui hooks",
function (test) {
var tmpl = Template.spacebars_test_ui_hooks;
var rv = new ReactiveVar([]);
@@ -2050,8 +2057,8 @@ Tinytest.add(
hooks.push("insert");
// check that the element hasn't actually been added yet
- test.isTrue(n.parentNode.nodeType === 11 /*DOCUMENT_FRAGMENT_NODE*/);
- test.isFalse(n.parentNode.parentNode);
+ test.isTrue((! n.parentNode) ||
+ n.parentNode.nodeType === 11 /*DOCUMENT_FRAGMENT_NODE*/);
},
removeElement: function (n) {
hooks.push("remove");
@@ -2094,7 +2101,7 @@ Tinytest.add(
);
Tinytest.add(
- "spacebars - ui hooks - nested domranges",
+ "spacebars-tests - template_tests - ui hooks - nested domranges",
function (test) {
var tmpl = Template.spacebars_test_ui_hooks_nested;
var rv = new ReactiveVar(true);
@@ -2128,7 +2135,7 @@ Tinytest.add(
);
Tinytest.add(
- "spacebars - access template instance from helper",
+ "spacebars-tests - template_tests - UI._templateInstance from helper",
function (test) {
// Set a property on the template instance; check that it's still
// there from a helper.
@@ -2149,19 +2156,8 @@ Tinytest.add(
}
);
-// XXX This is for traversing empty text nodes and should be removed
-// on blaze-refactor.
-var getSiblingText = function (node, siblingNum) {
- var sibling = node;
- for (var i = 0; i < siblingNum; i++) {
- if (sibling)
- sibling = sibling.nextSibling;
- }
- return $(sibling).text();
-};
-
Tinytest.add(
- "spacebars - access template instance from helper, " +
+ "spacebars-tests - template_tests - UI._templateInstance from helper, " +
"template instance is kept up-to-date",
function (test) {
var tmpl = Template.spacebars_test_template_instance_helper;
@@ -2169,21 +2165,16 @@ Tinytest.add(
var instanceFromHelper;
tmpl.foo = function () {
- instanceFromHelper = UI._templateInstance();
- return rv.get();
+ return UI._templateInstance().data;
};
- var div = renderToDiv(tmpl);
+ var div = renderToDiv(tmpl, function () { return rv.get(); });
rv.set("first");
- Deps.flush();
- // `nextSibling` because the first node is an empty text node.
- test.equal(getSiblingText(instanceFromHelper.firstNode, 4),
- "first");
+ divRendersTo(test, div, "first");
rv.set("second");
Deps.flush();
- test.equal(getSiblingText(instanceFromHelper.firstNode, 4),
- "second");
+ divRendersTo(test, div, "second");
// UI._templateInstance() should throw when called from not within a
// helper.
@@ -2194,7 +2185,7 @@ Tinytest.add(
);
Tinytest.add(
- "spacebars - {{#with}} autorun is cleaned up",
+ "spacebars-tests - template_tests - {{#with}} autorun is cleaned up",
function (test) {
var tmpl = Template.spacebars_test_with_cleanup;
var rv = new ReactiveVar("");
@@ -2219,39 +2210,40 @@ Tinytest.add(
);
Tinytest.add(
- "spacebars - access parent data contexts from helper",
+ "spacebars-tests - template_tests - UI._parentData from helpers",
function (test) {
var childTmpl = Template.spacebars_test_template_parent_data_helper_child;
var parentTmpl = Template.spacebars_test_template_parent_data_helper;
- var rv = new ReactiveVar(0);
+
+ var height = new ReactiveVar(0);
+ var bar = new ReactiveVar("bar");
childTmpl.a = ["a"];
- childTmpl.b = new ReactiveVar("b");
+ childTmpl.b = function () { return bar.get(); };
childTmpl.c = ["c"];
childTmpl.foo = function () {
- var data = UI._parentData(rv.get());
- return data.get === undefined ? data : data.get();
+ return UI._parentData(height.get());
};
var div = renderToDiv(parentTmpl);
test.equal(canonicalizeHtml(div.innerHTML), "d");
- rv.set(1);
+ height.set(1);
Deps.flush();
- test.equal(canonicalizeHtml(div.innerHTML), "b");
+ test.equal(canonicalizeHtml(div.innerHTML), "bar");
// Test UI._parentData() reactivity
- childTmpl.b.set("bNew");
+ bar.set("baz");
Deps.flush();
- test.equal(canonicalizeHtml(div.innerHTML), "bNew");
+ test.equal(canonicalizeHtml(div.innerHTML), "baz");
- rv.set(2);
+ height.set(2);
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML), "a");
- rv.set(3);
+ height.set(3);
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML), "parent");
}
@@ -2272,3 +2264,266 @@ Tinytest.add(
test.equal(anchNamespace, "http://www.w3.org/2000/svg");
}
);
+
+Tinytest.add(
+ "spacebars-tests - template_tests - created/rendered/destroyed by each",
+ function (test) {
+ var outerTmpl =
+ Template.spacebars_test_template_created_rendered_destroyed_each;
+ var innerTmpl =
+ Template.spacebars_test_template_created_rendered_destroyed_each_sub;
+
+ var buf = '';
+
+ innerTmpl.created = function () { buf += 'C' + String(this.data).toLowerCase(); };
+ innerTmpl.rendered = function () { buf += 'R' + String(this.data).toLowerCase(); };
+ innerTmpl.destroyed = function () { buf += 'D' + String(this.data).toLowerCase(); };
+
+ var R = ReactiveVar([{_id: 'A'}]);
+
+ outerTmpl.items = function () {
+ return R.get();
+ };
+
+ var div = renderToDiv(outerTmpl);
+ divRendersTo(test, div, 'Foo
";
@@ -180,7 +174,7 @@ Tinytest.add("templating - safestring", function(test) {
var obj = {fooprop: "
",
barprop: new Spacebars.SafeString("
")};
var html = canonicalizeHtml(
- renderToDiv(Template.test_safestring_a.extend({data: obj})).innerHTML);
+ renderToDiv(Template.test_safestring_a, obj).innerHTML);
test.equal(html,
"<br>
"+
@@ -188,7 +182,7 @@ Tinytest.add("templating - safestring", function(test) {
});
-Tinytest.add("templating - helpers and dots", function(test) {
+Tinytest.add("spacebars-tests - templating_tests - helpers and dots", function(test) {
UI.registerHelper("platypus", function() {
return "eggs";
});
@@ -258,7 +252,7 @@ Tinytest.add("templating - helpers and dots", function(test) {
var html;
html = canonicalizeHtml(
- renderToDiv(Template.test_helpers_a.extend({data: dataObj})).innerHTML);
+ renderToDiv(Template.test_helpers_a, dataObj).innerHTML);
test.equal(html.match(/\S+/g), [
'platypus=bill', // helpers on Template object take first priority
'watermelon=seeds', // global helpers take second priority
@@ -268,7 +262,7 @@ Tinytest.add("templating - helpers and dots", function(test) {
]);
html = canonicalizeHtml(
- renderToDiv(Template.test_helpers_b.extend({data: dataObj})).innerHTML);
+ renderToDiv(Template.test_helpers_b, dataObj).innerHTML);
test.equal(html.match(/\S+/g), [
// unknown properties silently fail
'unknown=',
@@ -277,7 +271,7 @@ Tinytest.add("templating - helpers and dots", function(test) {
]);
html = canonicalizeHtml(
- renderToDiv(Template.test_helpers_c.extend({data: dataObj})).innerHTML);
+ renderToDiv(Template.test_helpers_c, dataObj).innerHTML);
test.equal(html.match(/\S+/g), [
// property gets are supposed to silently fail
'platypus.X=',
@@ -291,7 +285,7 @@ Tinytest.add("templating - helpers and dots", function(test) {
]);
html = canonicalizeHtml(
- renderToDiv(Template.test_helpers_d.extend({data: dataObj})).innerHTML);
+ renderToDiv(Template.test_helpers_d, dataObj).innerHTML);
test.equal(html.match(/\S+/g), [
// helpers should get current data context in `this`
'daisygetter=petal',
@@ -305,7 +299,7 @@ Tinytest.add("templating - helpers and dots", function(test) {
]);
html = canonicalizeHtml(
- renderToDiv(Template.test_helpers_e.extend({data: dataObj})).innerHTML);
+ renderToDiv(Template.test_helpers_e, dataObj).innerHTML);
test.equal(html.match(/\S+/g), [
'fancy.foo=bar',
'fancy.apple.banana=smoothie',
@@ -316,7 +310,7 @@ Tinytest.add("templating - helpers and dots", function(test) {
]);
html = canonicalizeHtml(
- renderToDiv(Template.test_helpers_f.extend({data: dataObj})).innerHTML);
+ renderToDiv(Template.test_helpers_f, dataObj).innerHTML);
test.equal(html.match(/\S+/g), [
'fancyhelper.foo=bar',
'fancyhelper.apple.banana=smoothie',
@@ -329,7 +323,7 @@ Tinytest.add("templating - helpers and dots", function(test) {
// test significance of 'this', which prevents helper from
// shadowing property
html = canonicalizeHtml(
- renderToDiv(Template.test_helpers_g.extend({data: dataObj})).innerHTML);
+ renderToDiv(Template.test_helpers_g, dataObj).innerHTML);
test.equal(html.match(/\S+/g), [
'platypus=eggs',
'this.platypus=weird'
@@ -340,7 +334,7 @@ Tinytest.add("templating - helpers and dots", function(test) {
Template.test_helpers_h.helperListFour = listFour;
html = canonicalizeHtml(
- renderToDiv(Template.test_helpers_h.extend({data: dataObj})).innerHTML);
+ renderToDiv(Template.test_helpers_h, dataObj).innerHTML);
var trials =
html.match(/\(.*?\)/g);
test.equal(trials[0],
@@ -358,14 +352,14 @@ Tinytest.add("templating - helpers and dots", function(test) {
});
-Tinytest.add("templating - rendered template", function(test) {
+Tinytest.add("spacebars-tests - templating_tests - rendered template", function(test) {
var R = ReactiveVar('foo');
Template.test_render_a.foo = function() {
R.get();
return this.x + 1;
};
- var div = renderToDiv(Template.test_render_a.extend({data: {x: 123}}));
+ var div = renderToDiv(Template.test_render_a, {x: 123});
test.equal($(div).text().match(/\S+/)[0], "124");
var br1 = div.getElementsByTagName('br')[0];
@@ -393,7 +387,7 @@ Tinytest.add("templating - rendered template", function(test) {
return (+this) + 1;
};
- div = renderToDiv(Template.test_render_b.extend({data: {x: 123}}));
+ div = renderToDiv(Template.test_render_b, {x: 123});
test.equal($(div).text().match(/\S+/)[0], "201");
var br1 = div.getElementsByTagName('br')[0];
@@ -414,7 +408,7 @@ Tinytest.add("templating - rendered template", function(test) {
});
-Tinytest.add("templating - template arg", function (test) {
+Tinytest.add("spacebars-tests - templating_tests - template arg", function (test) {
Template.test_template_arg_a.events({
click: function (event, template) {
template.firstNode.innerHTML = 'Hello';
@@ -451,7 +445,7 @@ Tinytest.add("templating - template arg", function (test) {
test.throws(function () { return self.findAll("*"); });
};
- var div = renderToDiv(Template.test_template_arg_a.extend({data: {food: "pie"}}));
+ var div = renderToDiv(Template.test_template_arg_a, {food: "pie"});
var cleanupDiv = addToBody(div);
Deps.flush(); // cause `rendered` to be called
test.equal($(div).text(), "Greetings 1-bold Line");
@@ -462,7 +456,7 @@ Tinytest.add("templating - template arg", function (test) {
Deps.flush();
});
-Tinytest.add("templating - helpers", function (test) {
+Tinytest.add("spacebars-tests - templating_tests - helpers", function (test) {
var tmpl = Template.test_template_helpers_a;
tmpl.foo = 'z';
@@ -501,7 +495,7 @@ Tinytest.add("templating - helpers", function (test) {
Deps.flush();
});
-Tinytest.add("templating - events", function (test) {
+Tinytest.add("spacebars-tests - templating_tests - events", function (test) {
var tmpl = Template.test_template_events_a;
var buf = [];
@@ -559,7 +553,7 @@ Tinytest.add("templating - events", function (test) {
});
-Tinytest.add('templating - helper typecast Issue #617', function (test) {
+Tinytest.add('spacebars-tests - templating_tests - helper typecast Issue #617', function (test) {
UI.registerHelper('testTypeCasting', function (/*arguments*/) {
// Return a string representing the arguments passed to this
@@ -586,14 +580,14 @@ Tinytest.add('templating - helper typecast Issue #617', function (test) {
"[object]");
});
-Tinytest.add('templating - each falsy Issue #801', function (test) {
+Tinytest.add('spacebars-tests - templating_tests - each falsy Issue #801', function (test) {
//Minor test for issue #801 (#each over array containing nulls)
Template.test_template_issue801.values = function() { return [0,1,2,null,undefined,false]; };
var div = renderToDiv(Template.test_template_issue801);
test.equal(canonicalizeHtml(div.innerHTML), "012");
});
-Tinytest.add('templating - duplicate template error', function (test) {
+Tinytest.add('spacebars-tests - templating_tests - duplicate template error', function (test) {
Template.__define__("test_duplicate_template", function () {});
test.throws(function () {
Template.__define__("test_duplicate_template", function () {});
diff --git a/packages/spacebars/dynamic.html b/packages/spacebars/dynamic.html
index 00dedc7b8f..3e93b50538 100644
--- a/packages/spacebars/dynamic.html
+++ b/packages/spacebars/dynamic.html
@@ -20,7 +20,7 @@ the template to render) and a `data` property, which can be falsey. -->
{{#with ../data}} {{! original 'dataContext' argument to __dynamic}}
{{> ..}} {{! return value from chooseTemplate(template) }}
{{else}} {{! if the 'dataContext' argument was falsey }}
- {{> .. ../data}} {{! return value from chooseTemplate(template) }}
+ {{> .. ../data}} {{! return value from chooseTemplate(template) }}
{{/with}}
{{/with}}
diff --git a/packages/spacebars/dynamic_tests.js b/packages/spacebars/dynamic_tests.js
index a8b62f5275..503c22011b 100644
--- a/packages/spacebars/dynamic_tests.js
+++ b/packages/spacebars/dynamic_tests.js
@@ -1,5 +1,5 @@
Tinytest.add(
- "ui-dynamic-template - render template dynamically", function (test, expect) {
+ "spacebars - ui-dynamic-template - render template dynamically", function (test, expect) {
var tmpl = Template.ui_dynamic_test;
var nameVar = new ReactiveVar;
@@ -30,7 +30,7 @@ Tinytest.add(
// Same test as above, but the {{> UI.dynamic}} inclusion has no
// `dataContext` argument.
Tinytest.add(
- "ui-dynamic-template - render template dynamically, no data context",
+ "spacebars - ui-dynamic-template - render template dynamically, no data context",
function (test, expect) {
var tmpl = Template.ui_dynamic_test_no_data;
@@ -49,7 +49,7 @@ Tinytest.add(
Tinytest.add(
- "ui-dynamic-template - render template " +
+ "spacebars - ui-dynamic-template - render template " +
"dynamically, data context gets inherited",
function (test, expect) {
var tmpl = Template.ui_dynamic_test_inherited_data;
@@ -80,7 +80,7 @@ Tinytest.add(
);
Tinytest.add(
- "ui-dynamic-template - render template " +
+ "spacebars - ui-dynamic-template - render template " +
"dynamically, data context does not get inherited if " +
"falsey context is passed in",
function (test, expect) {
@@ -107,7 +107,7 @@ Tinytest.add(
);
Tinytest.add(
- "ui-dynamic-template - render template " +
+ "spacebars - ui-dynamic-template - render template " +
"dynamically, bad arguments",
function (test, expect) {
var tmplPrefix = "ui_dynamic_test_bad_args";
@@ -120,6 +120,7 @@ Tinytest.add(
for (var i = 0; i < 3; i++) {
var tmpl = Template[tmplPrefix + i];
test.throws(function () {
+ Blaze._throwNextException = true;
var div = renderToDiv(tmpl);
});
}
@@ -127,7 +128,7 @@ Tinytest.add(
);
Tinytest.add(
- "ui-dynamic-template - render template " +
+ "spacebars - ui-dynamic-template - render template " +
"dynamically, falsey context",
function (test, expect) {
var tmpl = Template.ui_dynamic_test_falsey_context;
diff --git a/packages/spacebars/package.js b/packages/spacebars/package.js
index 4c20f2ee2b..1b38fae2b0 100644
--- a/packages/spacebars/package.js
+++ b/packages/spacebars/package.js
@@ -12,11 +12,11 @@ Package.describe({
// Additional tests are in `spacebars-tests`.
Package.on_use(function (api) {
- api.use('spacebars-common');
- api.imply('spacebars-common');
+ api.export('Spacebars');
api.use('htmljs');
api.use('ui');
+ api.use('observe-sequence');
api.use('templating');
api.add_files(['spacebars-runtime.js']);
api.add_files(['dynamic.html', 'dynamic.js'], 'client');
diff --git a/packages/spacebars/spacebars-runtime.js b/packages/spacebars/spacebars-runtime.js
index c79b1fc66b..7d02d7f9b9 100644
--- a/packages/spacebars/spacebars-runtime.js
+++ b/packages/spacebars/spacebars-runtime.js
@@ -1,42 +1,37 @@
-// * `templateOrFunction` - template (component) or function returning a template
-// or null
-Spacebars.include = function (templateOrFunction, contentBlock, elseContentBlock) {
- if (contentBlock && ! UI.isComponent(contentBlock))
- throw new Error('Second argument to Spacebars.include must be a template or UI.block if present');
- if (elseContentBlock && ! UI.isComponent(elseContentBlock))
- throw new Error('Third argument to Spacebars.include must be a template or UI.block if present');
+Spacebars = {};
- var props = null;
- if (contentBlock) {
- props = (props || {});
- props.__content = contentBlock;
- }
- if (elseContentBlock) {
- props = (props || {});
- props.__elseContent = elseContentBlock;
+var tripleEquals = function (a, b) { return a === b; };
+
+Spacebars.include = function (templateOrFunction, contentFunc, elseFunc) {
+ if (! templateOrFunction)
+ return null;
+
+ if (typeof templateOrFunction !== 'function') {
+ var template = templateOrFunction;
+ if (! Blaze.isTemplate(template))
+ throw new Error("Expected template or null, found: " + template);
+ return Blaze.runTemplate(templateOrFunction, contentFunc, elseFunc);
}
- if (UI.isComponent(templateOrFunction))
- return templateOrFunction.extend(props);
-
- var func = templateOrFunction;
-
- var f = function () {
- var emboxedFunc = UI.namedEmboxValue('Spacebars.include', func);
- f.stop = function () {
- emboxedFunc.stop();
- };
- var tmpl = emboxedFunc();
-
- if (tmpl === null)
+ var templateVar = Blaze.ReactiveVar(null, tripleEquals);
+ var view = Blaze.View('Spacebars.include', function () {
+ var template = templateVar.get();
+ if (template === null)
return null;
- if (! UI.isComponent(tmpl))
- throw new Error("Expected null or template in return value from inclusion function, found: " + tmpl);
- return tmpl.extend(props);
- };
+ if (! Template.__isTemplate__(template))
+ throw new Error("Expected template or null, found: " + template);
- return f;
+ return Blaze.runTemplate(template, contentFunc, elseFunc);
+ });
+ view.__templateVar = templateVar;
+ view.onCreated(function () {
+ this.autorun(function () {
+ templateVar.set(templateOrFunction());
+ });
+ });
+
+ return view;
};
// Executes `{{foo bar baz}}` when called on `(foo, bar, baz)`.
@@ -209,38 +204,77 @@ Spacebars.dot = function (value, id1/*, id2, ...*/) {
};
};
-// Implement Spacebars's #with, which renders its else case (or nothing)
-// if the argument is falsy.
-Spacebars.With = function (argFunc, contentBlock, elseContentBlock) {
- return UI.Component.extend({
- init: function () {
- this.v = UI.emboxValue(argFunc, UI.safeEquals);
- },
- render: function () {
- return UI.If(this.v, UI.With(this.v, contentBlock), elseContentBlock);
- },
- materialized: (function () {
- var f = function (range) {
- var self = this;
- if (Deps.active) {
- Deps.onInvalidate(function () {
- self.v.stop();
- });
- }
- if (range) {
- range.removed = function () {
- self.v.stop();
- };
- }
- };
- f.isWith = true;
- return f;
- })()
- });
-};
-
Spacebars.TemplateWith = function (argFunc, contentBlock) {
- var w = UI.With(argFunc, contentBlock);
+ var w = Blaze.With(argFunc, contentBlock);
w.__isTemplateWith = true;
return w;
};
+
+// Spacebars.With implements the conditional logic of rendering
+// the `{{else}}` block if the argument is falsy. It combines
+// a Blaze.If with a Blaze.With (the latter only in the truthy
+// case, since the else block is evaluated without entering
+// a new data context).
+Spacebars.With = function (argFunc, contentFunc, elseFunc) {
+ var argVar = new Blaze.ReactiveVar;
+ var view = Blaze.View('Spacebars_with', function () {
+ return Blaze.If(function () { return argVar.get(); },
+ function () { return Blaze.With(function () {
+ return argVar.get(); }, contentFunc); },
+ elseFunc);
+ });
+ view.onCreated(function () {
+ this.autorun(function () {
+ argVar.set(argFunc());
+
+ // This is a hack so that autoruns inside the body
+ // of the #with get stopped sooner. It reaches inside
+ // our ReactiveVar to access its dep.
+
+ Deps.onInvalidate(function () {
+ argVar.dep.changed();
+ });
+
+ // Take the case of `{{#with A}}{{B}}{{/with}}`. The goal
+ // is to not re-render `B` if `A` changes to become falsy
+ // and `B` is simultaneously invalidated.
+ //
+ // A series of autoruns are involved:
+ //
+ // 1. This autorun (argument to Spacebars.With)
+ // 2. Argument to Blaze.If
+ // 3. Blaze.If view re-render
+ // 4. Argument to Blaze.With
+ // 5. The template tag `{{B}}`
+ //
+ // When (3) is invalidated, it immediately stops (4) and (5)
+ // because of a Deps.onInvalidate built into materializeView.
+ // (When a View's render method is invalidated, it immediately
+ // tears down all the subviews, via a Deps.onInvalidate much
+ // like this one.
+ //
+ // Suppose `A` changes to become falsy, and `B` changes at the
+ // same time (i.e. without an intervening flush).
+ // Without the code above, this happens:
+ //
+ // - (1) and (5) are invalidated.
+ // - (1) runs, invalidating (2) and (4).
+ // - (5) runs.
+ // - (2) runs, invalidating (3), stopping (4) and (5).
+ //
+ // With the code above:
+ //
+ // - (1) and (5) are invalidated, invalidating (2) and (4).
+ // - (1) runs.
+ // - (2) runs, invalidating (3), stopping (4) and (5).
+ //
+ // If the re-run of (5) is originally enqueued before (1), all
+ // bets are off, but typically that doesn't seem to be the
+ // case. Anyway, doing this is always better than not doing it,
+ // because it might save a bunch of DOM from being updated
+ // needlessly.
+ });
+ });
+
+ return view;
+};
diff --git a/packages/stylus/stylus_tests.js b/packages/stylus/stylus_tests.js
index 916f3a3ad7..e447e121bd 100644
--- a/packages/stylus/stylus_tests.js
+++ b/packages/stylus/stylus_tests.js
@@ -2,7 +2,7 @@
Tinytest.add("stylus - presence", function(test) {
var div = document.createElement('div');
- UI.materialize(Template.stylus_test_presence, div);
+ Blaze.render(Template.stylus_test_presence).attach(div);
div.style.display = 'block';
document.body.appendChild(div);
@@ -15,7 +15,7 @@ Tinytest.add("stylus - presence", function(test) {
Tinytest.add("stylus - @import", function(test) {
var div = document.createElement('div');
- UI.materialize(Template.stylus_test_import, div);
+ Blaze.render(Template.stylus_test_import).attach(div);
div.style.display = 'block';
document.body.appendChild(div);
diff --git a/packages/templating/global_template_object.js b/packages/templating/global_template_object.js
deleted file mode 100644
index 3e22e9e383..0000000000
--- a/packages/templating/global_template_object.js
+++ /dev/null
@@ -1,14 +0,0 @@
-// Create an empty template object. Packages and apps add templates on
-// to this object.
-Template = {};
-
-Template.__define__ = function (templateName, renderFunc) {
- if (Template.hasOwnProperty(templateName))
- throw new Error("There are multiple templates named '" + templateName + "'. Each template needs a unique name.");
-
- Template[templateName] = UI.Component.extend({
- kind: "Template_" + templateName,
- render: renderFunc,
- __helperHost: true
- });
-};
diff --git a/packages/templating/package.js b/packages/templating/package.js
index 0250efac55..2106295e2c 100644
--- a/packages/templating/package.js
+++ b/packages/templating/package.js
@@ -22,7 +22,7 @@ Package.on_use(function (api) {
// XXX would like to do the following only when the first html file
// is encountered
- api.add_files('global_template_object.js', 'client');
+ api.add_files('templating.js', 'client');
api.export('Template', 'client');
// html_scanner.js emits client code that calls Meteor.startup and
@@ -41,10 +41,6 @@ Package.on_test(function (api) {
'minimongo'], 'client');
api.use('spacebars-compiler');
- api.add_files([
- 'templating_tests.js',
- 'templating_tests.html'
- ], 'client');
api.add_files([
'plugin/html_scanner.js',
'scanner_tests.js'
diff --git a/packages/templating/plugin/html_scanner.js b/packages/templating/plugin/html_scanner.js
index 56331f28a9..9be7de3f35 100644
--- a/packages/templating/plugin/html_scanner.js
+++ b/packages/templating/plugin/html_scanner.js
@@ -152,10 +152,10 @@ html_scanner = {
if (! name)
throwParseError("Template has no 'name' attribute");
- if (Spacebars.isReservedName(name))
+ if (SpacebarsCompiler.isReservedName(name))
throwParseError("Template can't be named \"" + name + "\"");
- var renderFuncCode = Spacebars.compile(
+ var renderFuncCode = SpacebarsCompiler.compile(
contents, {
isTemplate: true,
sourceName: 'Template "' + name + '"'
@@ -168,14 +168,14 @@ html_scanner = {
if (hasAttribs)
throwParseError("Attributes on not supported");
- var renderFuncCode = Spacebars.compile(
+ var renderFuncCode = SpacebarsCompiler.compile(
contents, {
isBody: true,
sourceName: ""
});
// We may be one of many `` tags.
- results.js += "\nUI.body.contentParts.push(UI.Component.extend({render: " + renderFuncCode + "}));\nMeteor.startup(function () { if (! UI.body.INSTANTIATED) { UI.body.INSTANTIATED = true; UI.DomRange.insert(UI.render(UI.body).dom, document.body); } });\n";
+ results.js += "\nTemplate.__body__.__contentParts.push(Blaze.View('body_content_'+Template.__body__.__contentParts.length, " + renderFuncCode + "));\nMeteor.startup(Template.__body__.__instantiate);\n";
}
} catch (e) {
if (e.scanner) {
diff --git a/packages/templating/scanner_tests.js b/packages/templating/scanner_tests.js
index c03e6c07e7..40ad203c7a 100644
--- a/packages/templating/scanner_tests.js
+++ b/packages/templating/scanner_tests.js
@@ -26,12 +26,12 @@ Tinytest.add("templating - html scanner", function (test) {
// where content is something simple like the string "Hello"
// (passed in as a source string including the quotes).
var simpleBody = function (content) {
- return "\nUI.body.contentParts.push(UI.Component.extend({render: (function() {\n var self = this;\n return " + content + ";\n})}));\nMeteor.startup(function () { if (! UI.body.INSTANTIATED) { UI.body.INSTANTIATED = true; UI.DomRange.insert(UI.render(UI.body).dom, document.body); } });\n";
+ return "\nTemplate.__body__.__contentParts.push(Blaze.View('body_content_'+Template.__body__.__contentParts.length, (function() {\n var view = this;\n return " + content + ";\n})));\nMeteor.startup(Template.__body__.__instantiate);\n";
};
// arguments are quoted strings like '"hello"'
var simpleTemplate = function (templateName, content) {
- return '\nTemplate.__define__(' + templateName + ', (function() {\n var self = this;\n var template = this;\n return ' + content + ';\n}));\n';
+ return '\nTemplate.__define__(' + templateName + ', (function() {\n var view = this;\n return ' + content + ';\n}));\n';
};
var checkResults = function(results, expectJs, expectHead) {
diff --git a/packages/templating/templating.js b/packages/templating/templating.js
new file mode 100644
index 0000000000..b871b9c180
--- /dev/null
+++ b/packages/templating/templating.js
@@ -0,0 +1,261 @@
+// Create an empty template object. Packages and apps add templates on
+// to this object.
+Template = {};
+
+// `Template` is not a function so this is not a real function prototype,
+// but it is used as the prototype of all `Template.foo` objects.
+// Naming a template "prototype" will cause an error.
+Template.prototype = (function () {
+ // IE 8 exposes function names in the enclosing scope, so
+ // use this IIFE to catch it.
+ return (function Template() {}).prototype;
+})();
+
+Template.prototype.helpers = function (dict) {
+ for (var k in dict)
+ this[k] = dict[k];
+};
+
+Template.__updateTemplateInstance = function (view) {
+ // Populate `view.templateInstance.{firstNode,lastNode,data}`
+ // on demand.
+ var tmpl = view._templateInstance;
+ if (! tmpl) {
+ tmpl = view._templateInstance = {
+ $: function (selector) {
+ if (! view.domrange)
+ throw new Error("Can't use $ on component with no DOM");
+ return view.domrange.$(selector);
+ },
+ findAll: function (selector) {
+ return Array.prototype.slice.call(this.$(selector));
+ },
+ find: function (selector) {
+ var result = this.$(selector);
+ return result[0] || null;
+ },
+ data: null,
+ firstNode: null,
+ lastNode: null,
+ __view__: view
+ };
+ }
+
+ tmpl.data = Blaze.getViewData(view);
+
+ if (view.domrange && !view.isDestroyed) {
+ tmpl.firstNode = view.domrange.firstNode();
+ tmpl.lastNode = view.domrange.lastNode();
+ } else {
+ // on 'created' or 'destroyed' callbacks we don't have a DomRange
+ tmpl.firstNode = null;
+ tmpl.lastNode = null;
+ }
+
+ return tmpl;
+};
+
+UI._templateInstance = function () {
+ var templateView = Blaze.getCurrentTemplateView();
+ if (! templateView)
+ throw new Error("No current template");
+
+ return Template.__updateTemplateInstance(templateView);
+};
+
+Template.prototype.events = function (eventMap) {
+ var template = this;
+ template.__eventMaps = (template.__eventMaps || []);
+ var eventMap2 = {};
+ for (var k in eventMap) {
+ eventMap2[k] = (function (k, v) {
+ return function (event/*, ...*/) {
+ var view = this; // passed by EventAugmenter
+ var data = Blaze.getElementData(event.currentTarget);
+ if (data == null)
+ data = {};
+ var args = Array.prototype.slice.call(arguments);
+ var tmplInstance = Template.__updateTemplateInstance(view);
+ args.splice(1, 0, tmplInstance);
+ return v.apply(data, args);
+ };
+ })(k, eventMap[k]);
+ }
+
+ template.__eventMaps.push(eventMap2);
+};
+
+Template.prototype.__makeView = function (contentFunc, elseFunc) {
+ var template = this;
+ var view = Blaze.View(this.__viewName, this.__render);
+ view.template = template;
+
+ view.templateContentBlock = (
+ contentFunc ? Template.__create__('(contentBlock)', contentFunc) : null);
+ view.templateElseBlock = (
+ elseFunc ? Template.__create__('(elseBlock)', elseFunc) : null);
+
+ if (template.__eventMaps ||
+ typeof template.events === 'object') {
+ view.onMaterialized(function () {
+ if (! template.__eventMaps &&
+ typeof template.events === "object") {
+ // Provide limited back-compat support for `.events = {...}`
+ // syntax. Pass `template.events` to the original `.events(...)`
+ // function. This code must run only once per template, in
+ // order to not bind the handlers more than once, which is
+ // ensured by the fact that we only do this when `__eventMaps`
+ // is falsy, and we cause it to be set now.
+ Template.prototype.events.call(template, template.events);
+ }
+
+ _.each(template.__eventMaps, function (m) {
+ Blaze._addEventMap(view, m, view);
+ });
+ });
+ }
+
+ if (template.__initView)
+ template.__initView(view);
+
+ if (template.created) {
+ view.onCreated(function () {
+ var inst = Template.__updateTemplateInstance(view);
+ template.created.call(inst);
+ });
+ }
+
+ if (template.rendered) {
+ view.onRendered(function () {
+ var inst = Template.__updateTemplateInstance(view);
+ template.rendered.call(inst);
+ });
+ }
+
+ if (template.destroyed) {
+ view.onDestroyed(function () {
+ var inst = Template.__updateTemplateInstance(view);
+ template.destroyed.call(inst);
+ });
+ }
+
+ return view;
+};
+
+var _hasOwnProperty = Object.prototype.hasOwnProperty;
+
+Template.__lookup__ = function (templateName) {
+ if (! _hasOwnProperty.call(Template, templateName))
+ return null;
+ var tmpl = Template[templateName];
+ if (Template.__isTemplate__(tmpl))
+ return tmpl;
+ return null;
+};
+
+Template.__create__ = function (viewName, templateFunc, initView) {
+ var tmpl = new Template.prototype.constructor;
+ tmpl.__viewName = viewName;
+ tmpl.__render = templateFunc;
+ if (initView)
+ tmpl.__initView = initView;
+
+ return tmpl;
+};
+
+Template.__define__ = function (templateName, templateFunc) {
+ if (_hasOwnProperty.call(Template, templateName)) {
+ if (Template[templateName].__makeView)
+ throw new Error("There are multiple templates named '" + templateName + "'. Each template needs a unique name.");
+ throw new Error("This template name is reserved: " + templateName);
+ }
+
+ var tmpl = Template.__create__('Template.' + templateName, templateFunc);
+ tmpl.__templateName = templateName;
+
+ Template[templateName] = tmpl;
+ return tmpl;
+};
+
+Template.__isTemplate__ = function (x) {
+ return x && x.__makeView;
+};
+
+// Define a template `Template.__body__` that renders its
+// `__contentParts`.
+Template.__define__('__body__', function () {
+ var parts = Template.__body__.__contentParts;
+ // enable lookup by setting `view.template`
+ for (var i = 0; i < parts.length; i++)
+ parts[i].template = Template.__body__;
+ return parts;
+});
+Template.__body__.__contentParts = []; // array of Blaze.Views
+
+// Define `Template.__body__.__instantiate()` as a function that
+// renders `Template.__body__` into `document.body`, at most once
+// (calling it a second time does nothing). This function does
+// not use `this`, so you can safely call:
+// `Meteor.startup(Template.__body__.__instantiate)`.
+Template.__body__.__isInstantiated = false;
+var instantiateBody = function () {
+ if (Template.__body__.__isInstantiated)
+ return;
+ Template.__body__.__isInstantiated = true;
+ var range = Blaze.render(Template.__body__);
+ Template.__body__.__view = range.view;
+ range.attach(document.body);
+};
+Template.__body__.__instantiate = instantiateBody;
+
+
+// Renders a template (eg `Template.foo`), returning a DOMRange. The
+// range will keep updating reactively.
+UI.render = function (tmpl) {
+ if (! Template.__isTemplate__(tmpl))
+ throw new Error("Template required here");
+
+ return Blaze.render(tmpl);
+};
+
+// Same as `UI.render` with a data context passed in.
+UI.renderWithData = function (tmpl, data) {
+ if (! Template.__isTemplate__(tmpl))
+ throw new Error("Template required here");
+ if (typeof data === 'function')
+ throw new Error("Data argument can't be a function"); // XXX or can it?
+
+ return Blaze.render(Blaze.With(data, function () {
+ return tmpl;
+ }));
+};
+
+// The publicly documented API for inserting a DOMRange returned from
+// `UI.render` or `UI.renderWithData` into the DOM. If you then remove
+// `parentElement` using jQuery, all reactive updates on the rendered
+// template will stop.
+UI.insert = function (range, parentElement, nextNode) {
+ // parentElement must be a DOM node. in particular, can't be the
+ // result of a call to `$`. Can't check if `parentElement instanceof
+ // Node` since 'Node' is undefined in IE8.
+ if (! parentElement || typeof parentElement.nodeType !== 'number')
+ throw new Error("'parentElement' must be a DOM node");
+ if (nextNode && typeof nextNode.nodeType !== 'number') // 'nextNode' is optional
+ throw new Error("'nextNode' must be a DOM node");
+ if (! range instanceof Blaze.DOMRange)
+ throw new Error("Expected template rendered with UI.render");
+
+ range.attach(parentElement, nextNode);
+};
+
+// XXX test and document
+UI.remove = function (range) {
+ if (! range instanceof Blaze.DOMRange)
+ throw new Error("Expected template rendered with UI.render");
+
+ if (range.attached)
+ range.detach();
+ range.destroy();
+};
+
+UI.body = Template.__body__;
diff --git a/packages/test-helpers/canonicalize_html.js b/packages/test-helpers/canonicalize_html.js
index 6a5ed5f4a9..5ac2c0e277 100644
--- a/packages/test-helpers/canonicalize_html.js
+++ b/packages/test-helpers/canonicalize_html.js
@@ -21,7 +21,7 @@ canonicalizeHtml = function(html) {
attrs = attrs.replace(/sizcache[0-9]+="[^"]*"/g, ' ');
// Similarly for expando properties used by jQuery to track data.
attrs = attrs.replace(/jQuery[0-9]+="[0-9]+"/g, ' ');
- // Similarly for expando properties used to DomBackend to keep
+ // Similarly for expando properties used to DOMBackend to keep
// track of callbacks to fire when an element is removed
attrs = attrs.replace(/\$meteor_ui_removal_callbacks="[^"]*"/g, ' ');
diff --git a/packages/test-helpers/render_div.js b/packages/test-helpers/render_div.js
index 451afea535..2976d5c18c 100644
--- a/packages/test-helpers/render_div.js
+++ b/packages/test-helpers/render_div.js
@@ -1,5 +1,13 @@
-renderToDiv = function (comp) {
+renderToDiv = function (template, optData) {
var div = document.createElement("DIV");
- UI.materialize(comp, div);
+ if (optData == null) {
+ Blaze.render(template).attach(div);
+ } else {
+ Blaze.render(function () {
+ return Blaze.With(optData, function () {
+ return template;
+ });
+ }).attach(div);
+ }
return div;
};
diff --git a/packages/test-helpers/seeded_random_test.js b/packages/test-helpers/seeded_random_test.js
index 9d3ef18509..841f62c35a 100644
--- a/packages/test-helpers/seeded_random_test.js
+++ b/packages/test-helpers/seeded_random_test.js
@@ -1,6 +1,6 @@
// XXX SECTION: Meta tests
-Tinytest.add("seeded random", function (test) {
+Tinytest.add("test-helpers - seeded_random", function (test) {
// Test that two seeded PRNGs with the same seed produce the same values.
var seed = "I'm a seed";
var sr1 = new SeededRandom(seed);
diff --git a/packages/test-helpers/try_all_permutations_test.js b/packages/test-helpers/try_all_permutations_test.js
index f3e2f0cb04..f9bebffea9 100644
--- a/packages/test-helpers/try_all_permutations_test.js
+++ b/packages/test-helpers/try_all_permutations_test.js
@@ -1,6 +1,6 @@
// XXX SECTION: Meta tests
-Tinytest.add("try_all_permutations", function (test) {
+Tinytest.add("test-helpers - try_all_permutations", function (test) {
// Have a good test of try_all_permutations, because it would suck
// if try_all_permutations didn't actually run anything and so none
// of our other tests actually did any testing.
diff --git a/packages/ui/README-old.md b/packages/ui/README-old.md
deleted file mode 100644
index 8e44afe656..0000000000
--- a/packages/ui/README-old.md
+++ /dev/null
@@ -1,87 +0,0 @@
-# Meteor UI
-
-XXX This README just talks about DomRange, and the information is out of date or may change before release.
-
-## DomRange
-
-- - -
-**What users need to know:** DomRange is the type of object found at `component.dom` (sometimes `this.dom`). It provides useful methods like `dom.$(selector)` and `dom.elements()`. A DomRange represents the DOM extent of a rendered component, sort of like virtual wrapper element.
-- - -
-
-A DomRange can be conceptualized as an invisible element in the DOM tree which sits under some parent element and contains some of the element's children as members. There's one DomRange for every template, component, or block helper in a Meteor application. The members of a DomRange may be either nodes or other DomRanges, forming a miniature tree. Since all the nodes in a DomRange tree are siblings in the DOM, this miniature tree occurs entirely at one DOM tree level.
-
-For example, take the following template code:
-
-```
-
- {{#each posts}}
- {{#if recent}}
- "), function (r) {
- var table = r.elements()[0];
- var tableContent = new DomRange;
- var buf = [];
- DomRange.insert(tableContent, table);
- var trRange = htmlRange("
");
- tableContent.add(trRange);
- test.isTrue(tableContent.contains(trRange));
- });
-
- inDocument(htmlRange("Hello "), function (r) {
- var table = r.elements()[0];
- var tableContent = new DomRange;
- var buf = [];
- DomRange.insert(tableContent, table);
- var trRange = htmlRange("
");
- var tr = trRange.elements()[0];
- tableContent.add('tr', tr);
- test.equal(_.keys(tableContent.members).length, 1);
- test.isTrue(tableContent.contains(tr));
- tableContent.remove('tr');
- // bizarrely, in IE 8, the `tr` still has some
- // DocumentFragment as its parent even though `removeChild`
- // has been called on it directly.
- test.isFalse(tr.parentNode && tr.parentNode.nodeType === 1);
- });
-});
-
-Tinytest.add("ui - DomRange - events in tables", function (test) {
- inDocument(htmlRange("Hello "), function (r) {
- var table = r.elements()[0];
- var tableContent = new DomRange;
- var buf = [];
- DomRange.insert(tableContent, table);
- tableContent.on('click', 'tr', function (evt) {
- buf.push('click ' + evt.currentTarget.nodeName);
- });
- var trRange = htmlRange("
");
- tableContent.add(trRange);
- var tr = trRange.elements()[0];
- test.equal(buf, []);
- clickElement(tr);
- test.equal(buf, ['click TR']);
- // XXX test something that would break if the event data
- // is on the TABLE rather than the TBODY (the new
- // parentNode of `tableContent`).
- });
-});
-
-Tinytest.add("ui - DomRange - nested event order", function (test) {
- inDocument(new DomRange, function (r) {
- var a = new DomRange;
- var b = new DomRange;
- var c = new DomRange;
- var d = new DomRange;
- r.add(a);
- a.add(b);
- b.add(c);
- c.add(d);
- var div = document.createElement("DIV");
- d.add(div);
-
- var buf = [];
- var appender = function (str) {
- return function (evt) {
- buf.push(str);
- };
- };
-
- b.on('click', 'div', appender("B"));
- a.on('click', 'div', appender("A"));
- d.on('click', appender("D"));
- c.on('click', 'div', appender("C"));
- test.equal(buf, []);
- clickElement(div);
- test.equal(buf, ['D', 'C', 'B', 'A']);
- buf.length = 0;
-
- b.on('click', appender("B2"));
- d.on('click', 'div', appender("D2"));
- clickElement(div);
- test.equal(buf, ['D', 'D2', 'C', 'B', 'B2', 'A']);
- });
-});
-
-Tinytest.add("ui - DomRange - isParented", function (test) {
- inDocument(new DomRange, function (r) {
- test.equal(r.isParented, true);
- var a = new DomRange;
- var b = new DomRange;
- var c = new DomRange;
- var d = new DomRange;
- var e = new DomRange;
- var abcde = function (ap, bp, cp, dp, ep) {
- test.equal(!! a.isParented, !! ap);
- test.equal(!! b.isParented, !! bp);
- test.equal(!! c.isParented, !! cp);
- test.equal(!! d.isParented, !! dp);
- test.equal(!! e.isParented, !! ep);
- };
- var div = document.createElement("DIV");
- c.add(div);
- abcde(0, 0, 0, 0, 0);
- d.add(e);
- abcde(0, 0, 0, 0, 0);
- DomRange.insert(d, div);
- abcde(0, 0, 0, 1, 1);
- a.add(b);
- abcde(0, 0, 0, 1, 1);
- r.add(a);
- abcde(1, 1, 0, 1, 1);
- b.add(c);
- abcde(1, 1, 1, 1, 1);
-
- var container = r.parentNode();
- test.equal(_.keys(container.$_uiranges).length, 1);
- test.equal(_.keys(div.$_uiranges).length, 1);
- d.remove();
- test.equal(_.keys(div.$_uiranges).length, 0);
- r.remove();
- test.equal(_.keys(container.$_uiranges).length, 0);
- });
-});
-
-Tinytest.add("ui - DomRange - structural removal", function (test) {
- inDocument(new DomRange, function (r) {
- var a = new DomRange;
- test.isFalse(a.isRemoved);
- r.add('a', a);
- test.isFalse(a.isRemoved);
- r.remove('a');
- test.isTrue(a.isRemoved);
-
-
- var b = new DomRange;
- test.isFalse(b.isRemoved);
- r.add(b);
- test.isFalse(b.isRemoved);
- r.removeAll();
- test.isTrue(b.isRemoved);
-
-
- var c = new DomRange;
- var d = new DomRange;
- var e = new DomRange;
- c.add(d);
- d.add(e);
- r.add('c', c);
- test.isFalse(c.isRemoved);
- test.isFalse(d.isRemoved);
- test.isFalse(e.isRemoved);
- r.remove('c');
- test.isTrue(c.isRemoved);
- test.isTrue(d.isRemoved);
- test.isTrue(e.isRemoved);
-
-
- for (var scenario = 0; scenario < 3; scenario++) {
- var f = new DomRange;
- var g = document.createElement("DIV");
- var h = new DomRange;
- var i = document.createElement("DIV");
- var j = document.createElement("DIV");
- var k = new DomRange;
- r.add('f', f);
- f.add(g);
- DomRange.insert(h, g);
- h.add(i);
- DomRange.insert(k, j);
- i.appendChild(j);
- test.isFalse(f.isRemoved);
- test.isFalse(h.isRemoved);
- test.isFalse(k.isRemoved);
- if (scenario === 0)
- r.removeAll();
- else if (scenario === 1)
- r.remove('f');
- else if (scenario === 2)
- $(r.parentNode()).remove();
- test.isTrue(f.isRemoved);
- test.isTrue(h.isRemoved);
- test.isTrue(k.isRemoved);
-
- r.removeAll();
- }
- });
-});
-
-Tinytest.add("ui - DomRange - noticed removal", function (test) {
- // TODO
- //
- // e.g. noticed via `eachMember` or `add`
-});
-
-Tinytest.add("ui - DomRange - jQuery removal", function (test) {
- inDocument(htmlRange(""), function (r) {
- for (var scenario = 0; scenario < 3; scenario++) {
- var f = document.createElement("DIV");
- var g = document.createElement("DIV");
- var h = new DomRange;
- var i = document.createElement("DIV");
- var j = document.createElement("DIV");
- var k = new DomRange;
- r.add(f);
- f.appendChild(g);
- DomRange.insert(h, g);
- h.add(i);
- DomRange.insert(k, j);
- i.appendChild(j);
- test.isFalse(h.isRemoved);
- test.isFalse(k.isRemoved);
-
- $(g).removeData();
- test.isFalse(h.isRemoved);
- test.isFalse(k.isRemoved);
-
- if (scenario === 0)
- $(g).remove();
- else if (scenario === 1)
- $(f).empty();
- else if (scenario === 2)
- $(f).html("Hello
");
- else if (scenario === 3)
- $(g).detach();
-
- if (scenario !== 3) {
- test.isTrue(h.isRemoved);
- test.isTrue(k.isRemoved);
- } else {
- // `detach` doesn't remove
- test.isFalse(h.isRemoved);
- test.isFalse(k.isRemoved);
- }
-
- r.removeAll();
- }
- });
-});
-
-// TO TEST STILL:
-// - external remove element
-// - double-add, double-remove
-// - external entire remove
-// - element adoption during move/remove/refresh
-// - first arg of add must be string, errors on `0` for example.
-// same with remove and move `id` arguments.
-// - can't add multiple members with id, but can add array of 1.
-// can add 0 with no id.
-// - add a node or range with the same id as an old member
-// works if that member is gone.
-// - events (and other stuff) get moved when wrapping in TBODY
-// - event unbinding
-// - "noticed" removal due to `eachMembers`, `add`, etc.
diff --git a/packages/ui/each.js b/packages/ui/each.js
deleted file mode 100644
index ea3ab262c3..0000000000
--- a/packages/ui/each.js
+++ /dev/null
@@ -1,119 +0,0 @@
-UI.EachImpl = Component.extend({
- typeName: 'Each',
- render: function (modeHint) {
- var self = this;
- var content = self.__content;
- var elseContent = self.__elseContent;
-
- if (modeHint === 'STATIC') {
- // This is a hack. The caller gives us a hint if the
- // value we return will be static (in HTML or text)
- // or dynamic (materialized DOM). The dynamic path
- // returns `null` and then we populate the DOM from
- // the `materialized` callback.
- //
- // It would be much cleaner to always return the same
- // value here, and to have that value be some special
- // object that encapsulates the logic for populating
- // the #each using a mode-agnostic interface that
- // works for HTML, text, and DOM. Alternatively, we
- // could formalize the current pattern, e.g. defining
- // a method like component.populate(domRange) and one
- // like renderStatic() or even renderHTML / renderText.
- var parts = _.map(
- ObserveSequence.fetch(self.__sequence()),
- function (item) {
- return content.extend({data: function () {
- return item;
- }});
- });
-
- if (parts.length) {
- return parts;
- } else {
- return elseContent;
- }
- return parts;
- } else {
- return null;
- }
- },
- materialized: function () {
- var self = this;
-
- var range = self.dom;
-
- var content = self.__content;
- var elseContent = self.__elseContent;
-
- // if there is an else clause, keep track of the number of
- // rendered items. use this to display the else clause when count
- // becomes zero, and remove it when count becomes positive.
- var itemCount = 0;
- var addToCount = function(delta) {
- if (!elseContent) // if no else, no need to keep track of count
- return;
-
- if (itemCount + delta < 0)
- throw new Error("count should never become negative");
-
- if (itemCount === 0) {
- // remove else clause
- range.removeAll();
- }
- itemCount += delta;
- if (itemCount === 0) {
- UI.materialize(elseContent, range, null, self);
- }
- };
-
- this.observeHandle = ObserveSequence.observe(function () {
- return self.__sequence();
- }, {
- addedAt: function (id, item, i, beforeId) {
- addToCount(1);
- id = LocalCollection._idStringify(id);
-
- var data = item;
- var dep = new Deps.Dependency;
-
- // function to become `comp.data`
- var dataFunc = function () {
- dep.depend();
- return data;
- };
- // Storing `$set` on `comp.data` lets us
- // access it from `changed`.
- dataFunc.$set = function (v) {
- data = v;
- dep.changed();
- };
-
- if (beforeId)
- beforeId = LocalCollection._idStringify(beforeId);
-
- var renderedItem = UI.render(content.extend({data: dataFunc}), self);
- range.add(id, renderedItem.dom, beforeId);
- },
- removedAt: function (id, item) {
- addToCount(-1);
- range.remove(LocalCollection._idStringify(id));
- },
- movedTo: function (id, item, i, j, beforeId) {
- range.moveBefore(
- LocalCollection._idStringify(id),
- beforeId && LocalCollection._idStringify(beforeId));
- },
- changedAt: function (id, newItem, oldItem, atIndex) {
- range.get(LocalCollection._idStringify(id)).component.data.$set(newItem);
- }
- });
-
- // on initial render, display the else clause if no items
- addToCount(0);
- },
- destroyed: function () {
- if (this.__component__.observeHandle)
- this.__component__.observeHandle.stop();
- }
-});
diff --git a/packages/ui/fields.js b/packages/ui/fields.js
deleted file mode 100644
index 71341ded99..0000000000
--- a/packages/ui/fields.js
+++ /dev/null
@@ -1,143 +0,0 @@
-
-var global = (function () { return this; })();
-
-currentComponent = new Meteor.EnvironmentVariable();
-
-// Searches for the given property in `comp` or a parent,
-// and returns it as is (without call it if it's a function).
-var lookupComponentProp = function (comp, prop) {
- comp = findComponentWithProp(prop, comp);
- var result = (comp ? comp.data : null);
- if (typeof result === 'function')
- result = _.bind(result, comp);
- return result;
-};
-
-// Component that's a no-op when used as a block helper like
-// `{{#foo}}...{{/foo}}`. Prints a warning that it is deprecated.
-var noOpComponent = function (name) {
- return Component.extend({
- kind: 'NoOp',
- render: function () {
- Meteor._debug("{{#" + name + "}} is now unnecessary and deprecated.");
- return this.__content;
- }
- });
-};
-
-// This map is searched first when you do something like `{{#foo}}` in
-// a template.
-var builtInComponents = {
- // for past compat:
- 'constant': noOpComponent("constant"),
- 'isolate': noOpComponent("isolate")
-};
-
-_extend(UI.Component, {
- // Options:
- //
- // - template {Boolean} If true, look at the list of templates after
- // helpers and before data context.
- lookup: function (id, opts) {
- var self = this;
- var template = opts && opts.template;
- var result;
- var comp;
-
- if (!id)
- throw new Error("must pass id to lookup");
-
- if (/^\./.test(id)) {
- // starts with a dot. must be a series of dots which maps to an
- // ancestor of the appropriate height.
- if (!/^(\.)+$/.test(id)) {
- throw new Error("id starting with dot must be a series of dots");
- }
-
- var compWithData = findComponentWithProp('data', self);
- for (var i = 1; i < id.length; i++) {
- compWithData = compWithData ? findComponentWithProp('data', compWithData.parent) : null;
- }
-
- return (compWithData ? compWithData.data : null);
-
- } else if ((comp = findComponentWithHelper(id, self))) {
- // found a property or method of a component
- // (`self` or one of its ancestors)
- var result = comp[id];
-
- } else if (_.has(builtInComponents, id)) {
- return builtInComponents[id];
-
- // Code to search the global namespace for capitalized names
- // like component classes, `Template`, `StringUtils.foo`,
- // etc.
- //
- // } else if (/^[A-Z]/.test(id) && (id in global)) {
- // // Only look for a global identifier if `id` is
- // // capitalized. This avoids having `{{name}}` mean
- // // `window.name`.
- // result = global[id];
- // return function (/*arguments*/) {
- // var data = getComponentData(self);
- // if (typeof result === 'function')
- // return result.apply(data, arguments);
- // return result;
- // };
- } else if (template && _.has(Template, id)) {
- return Template[id];
-
- } else if ((result = UI._globalHelper(id))) {
-
- } else {
- // Resolve id `foo` as `data.foo` (with a "soft dot").
- return function (/* arguments */) {
- var data = getComponentData(self);
- if (template && !(data && _.has(data, id)))
- throw new Error("Can't find template, helper or data context " +
- "key: " + id);
- if (! data)
- return data;
- var result = data[id];
- if (typeof result === 'function')
- return result.apply(data, arguments);
- return result;
- };
- }
-
- if (typeof result === 'function' && ! result._isEmboxedConstant) {
- // Wrap the function `result`, binding `this` to `getComponentData(self)`.
- // This creates a dependency when the result function is called.
- // Don't do this if the function is really just an emboxed constant.
- return function (/*arguments*/) {
- var args = arguments;
- return currentComponent.withValue(self, function () {
- currentTemplateInstance = null; // lazily computed, since `updateTemplateInstance` is a little slow
- var data = getComponentData(self);
- return result.apply(data === null ? {} : data, args);
- });
- };
- } else {
- return result;
- };
- },
- lookupTemplate: function (id) {
- return this.lookup(id, {template: true});
- },
- get: function (id) {
- // support `this.get()` to get the data context.
- if (id === undefined)
- id = ".";
-
- var result = this.lookup(id);
- return (typeof result === 'function' ? result() : result);
- },
- set: function (id, value) {
- var comp = findComponentWithProp(id, this);
- if (! comp || ! comp[id])
- throw new Error("Can't find field: " + id);
- if (typeof comp[id] !== 'function')
- throw new Error("Not a settable field: " + id);
- comp[id](value);
- }
-});
diff --git a/packages/ui/handlebars_backcompat.js b/packages/ui/handlebars_backcompat.js
index b089dfd7c3..78c2aedd2a 100644
--- a/packages/ui/handlebars_backcompat.js
+++ b/packages/ui/handlebars_backcompat.js
@@ -1,36 +1,7 @@
-// XXX this file no longer makes sense in isolation. take it apart as
-// part file reorg on the 'ui' package
-var globalHelpers = {};
-
-UI.registerHelper = function (name, func) {
- globalHelpers[name] = func;
-};
-
-UI._globalHelper = function (name) {
- return globalHelpers[name];
-};
-
Handlebars = {};
Handlebars.registerHelper = UI.registerHelper;
-// Utility to HTML-escape a string.
-UI._escape = Handlebars._escape = (function() {
- var escape_map = {
- "<": "<",
- ">": ">",
- '"': """,
- "'": "'",
- "`": "`", /* IE allows backtick-delimited attributes?? */
- "&": "&"
- };
- var escape_one = function(c) {
- return escape_map[c];
- };
-
- return function (x) {
- return x.replace(/[&<>"'`]/g, escape_one);
- };
-})();
+Handlebars._escape = UI._escape;
// Return these from {{...}} helpers to achieve the same as returning
// strings from {{{...}}} helpers
diff --git a/packages/ui/package.js b/packages/ui/package.js
index 2f50cf3942..96cc37c64f 100644
--- a/packages/ui/package.js
+++ b/packages/ui/package.js
@@ -20,18 +20,10 @@ Package.on_use(function (api) {
api.use('htmljs');
api.imply('htmljs');
- api.add_files(['exceptions.js', 'base.js']);
-
- api.add_files(['dombackend.js',
- 'domrange.js'], 'client');
-
- api.add_files(['attrs.js',
- 'render.js',
- 'builtins.js',
- 'each.js',
- 'fields.js'
- ]);
+ api.use('blaze');
+ api.imply('blaze');
+ api.add_files(['ui.js']);
api.add_files(['handlebars_backcompat.js']);
});
@@ -40,14 +32,11 @@ Package.on_test(function (api) {
api.use('jquery'); // strong dependency, for testing jQuery backend
api.use('ui');
api.use(['test-helpers', 'underscore'], 'client');
- api.use('spacebars-compiler'); // for `HTML.toJS`
+ api.use('blaze-tools'); // for `HTML.toJS`
api.use('html-tools');
api.add_files([
- 'base_tests.js',
- 'domrange_tests.js',
- 'render_tests.js',
- 'dombackend_tests.js'
+ 'render_tests.js'
], 'client');
});
diff --git a/packages/ui/render.js b/packages/ui/render.js
deleted file mode 100644
index 95c7ab305c..0000000000
--- a/packages/ui/render.js
+++ /dev/null
@@ -1,479 +0,0 @@
-
-UI.Component.instantiate = function (parent) {
- var kind = this;
-
- // check arguments
- if (UI.isComponent(kind)) {
- if (kind.isInited)
- throw new Error("A component kind is required, not an instance");
- } else {
- throw new Error("Expected Component kind");
- }
-
- var inst = kind.extend(); // XXX args go here
- inst.isInited = true;
-
- // XXX messy to define this here
- inst.templateInstance = {
- $: function(selector) {
- // XXX check that `.dom` exists here?
- return inst.dom.$(selector);
- },
- findAll: function (selector) {
- return $.makeArray(this.$(selector));
- },
- find: function (selector) {
- var result = this.$(selector);
- return result[0] || null;
- },
- firstNode: null,
- lastNode: null,
- data: null,
- __component__: inst
- };
-
- inst.parent = (parent || null);
-
- if (inst.init)
- inst.init();
-
- if (inst.created) {
- updateTemplateInstance(inst);
- inst.created.call(inst.templateInstance);
- }
-
- return inst;
-};
-
-UI.Component.render = function () {
- return null;
-};
-
-var Box = function (func, equals) {
- var self = this;
-
- self.func = func;
- self.equals = equals;
-
- self.curResult = null;
-
- self.dep = new Deps.Dependency;
-
- self.resultComputation = Deps.nonreactive(function () {
- return Deps.autorun(function (c) {
- var func = self.func;
-
- var newResult = func();
-
- if (! c.firstRun) {
- var equals = self.equals;
- var oldResult = self.curResult;
-
- if (equals ? equals(newResult, oldResult) :
- newResult === oldResult) {
- // same as last time
- return;
- }
- }
-
- self.curResult = newResult;
- self.dep.changed();
- });
- });
-};
-
-Box.prototype.stop = function () {
- this.resultComputation.stop();
-};
-
-Box.prototype.get = function () {
- if (Deps.active && ! this.resultComputation.stopped)
- this.dep.depend();
-
- return this.curResult;
-};
-
-// Takes a reactive function (call it `inner`) and returns a reactive function
-// `outer` which is equivalent except in its reactive behavior. Specifically,
-// `outer` has the following two special properties:
-//
-// 1. Isolation: An invocation of `outer()` only invalidates its context
-// when the value of `inner()` changes. For example, `inner` may be a
-// function that gets one or more Session variables and calculates a
-// true/false value. `outer` blocks invalidation signals caused by the
-// Session variables changing and sends a signal out only when the value
-// changes between true and false (in this example). The value can be
-// of any type, and it is compared with `===` unless an `equals` function
-// is provided.
-//
-// 2. Value Sharing: The `outer` function returned by `emboxValue` can be
-// shared between different contexts, for example by assigning it to an
-// object as a method that can be accessed at any time, such as by
-// different templates or different parts of a template. No matter
-// how many times `outer` is called, `inner` is only called once until
-// it changes. The most recent value is stored internally.
-//
-// Conceptually, an emboxed value is much like a Session variable which is
-// kept up to date by an autorun. Session variables provide storage
-// (value sharing) and they don't notify their listeners unless a value
-// actually changes (isolation). The biggest difference is that such an
-// autorun would never be stopped, and the Session variable would never be
-// deleted even if it wasn't used any more. An emboxed value, on the other
-// hand, automatically stops computing when it's not being used, and starts
-// again when called from a reactive context. This means that when it stops
-// being used, it can be completely garbage-collected.
-//
-// If a non-function value is supplied to `emboxValue` instead of a reactive
-// function, then `outer` is still a function but it simply returns the value.
-//
-UI.emboxValue = function (funcOrValue, equals) {
- if (typeof funcOrValue === 'function') {
-
- var func = funcOrValue;
- var box = new Box(func, equals);
-
- var f = function () {
- return box.get();
- };
-
- f.stop = function () {
- box.stop();
- };
-
- return f;
-
- } else {
- var value = funcOrValue;
- var result = function () {
- return value;
- };
- result._isEmboxedConstant = true;
- return result;
- }
-};
-
-
-UI.namedEmboxValue = function (name, funcOrValue, equals) {
- if (! Deps.active) {
- var f = UI.emboxValue(funcOrValue, equals);
- f.stop();
- return f;
- }
-
- var c = Deps.currentComputation;
- if (! c[name])
- c[name] = UI.emboxValue(funcOrValue, equals);
-
- return c[name];
-};
-
-////////////////////////////////////////
-
-UI.insert = function (renderedTemplate, parentElement, nextNode) {
- // parentElement must be a DOM node. in particular, can't be the
- // result of a call to `$`. Can't check if `parentElement instanceof
- // Node` since 'Node' is undefined in IE8.
- if (! parentElement || typeof parentElement.nodeType !== 'number')
- throw new Error("'parentElement' must be a DOM node");
- if (nextNode && typeof nextNode.nodeType !== 'number') // 'nextNode' is optional
- throw new Error("'nextNode' must be a DOM node");
-
- if (! renderedTemplate.dom)
- throw new Error("Expected template rendered with UI.render");
-
- UI.DomRange.insert(renderedTemplate.dom, parentElement, nextNode);
-};
-
-// Insert a DOM node or DomRange into a DOM element or DomRange.
-//
-// One of three things happens depending on what needs to be inserted into what:
-// - `range.add` (anything into DomRange)
-// - `UI.DomRange.insert` (DomRange into element)
-// - `elem.insertBefore` (node into element)
-//
-// The optional `before` argument is an existing node or id to insert before in
-// the parent element or DomRange.
-var insert = function (nodeOrRange, parent, before) {
- if (! parent)
- throw new Error("Materialization parent required");
-
- if (parent instanceof UI.DomRange) {
- parent.add(nodeOrRange, before);
- } else if (nodeOrRange instanceof UI.DomRange) {
- // parent is an element; inserting a range
- UI.DomRange.insert(nodeOrRange, parent, before);
- } else {
- // parent is an element; inserting an element
- parent.insertBefore(nodeOrRange, before || null); // `null` for IE
- }
-};
-
-// options include:
-// - _nestInCurrentComputation: defaults to false. If true, then
-// `render`'s autoruns will be nested inside the current
-// computation, so if the current computation is invalidated, then
-// the autoruns set up inside `render` will be stopped. If false,
-// the autoruns will be set up in a fresh Deps context, so
-// invalidating the current computation will have no effect on them.
-UI.render = function (kind, parentComponent, options) {
- options = options || {};
-
- if (kind.isInited)
- throw new Error("Can't render component instance, only component kind");
-
- var inst, content, range;
-
- Deps.nonreactive(function () {
-
- inst = kind.instantiate(parentComponent);
-
- content = (inst.render && inst.render());
-
- range = new UI.DomRange;
- inst.dom = range;
- range.component = inst;
-
- if (! options._nestInCurrentComputation) {
- materialize(content, range, null, inst);
- }
-
- });
-
- if (options._nestInCurrentComputation) {
- materialize(content, range, null, inst);
- }
-
- range.removed = function () {
- inst.isDestroyed = true;
- if (inst.destroyed) {
- Deps.nonreactive(function () {
- updateTemplateInstance(inst);
- inst.destroyed.call(inst.templateInstance);
- });
- }
- };
-
- return inst;
-};
-
-// options are the same as for UI.render.
-UI.renderWithData = function (kind, data, parentComponent, options) {
- if (! UI.isComponent(kind))
- throw new Error("Component required here");
- if (kind.isInited)
- throw new Error("Can't render component instance, only component kind");
- if (typeof data === 'function')
- throw new Error("Data argument can't be a function");
-
- return UI.render(kind.extend({data: function () { return data; }}),
- parentComponent, options);
-};
-
-var contentEquals = function (a, b) {
- if (a instanceof HTML.Raw) {
- return (b instanceof HTML.Raw) && (a.value === b.value);
- } else if (a == null) {
- return (b == null);
- } else {
- return (a === b) &&
- ((typeof a === 'number') || (typeof a === 'boolean') ||
- (typeof a === 'string'));
- }
-};
-
-UI.InTemplateScope = function (tmplInstance, content) {
- if (! (this instanceof UI.InTemplateScope))
- // called without `new`
- return new UI.InTemplateScope(tmplInstance, content);
-
- var parentPtr = tmplInstance.parent;
- if (parentPtr.__isTemplateWith)
- parentPtr = parentPtr.parent;
-
- this.parentPtr = parentPtr;
- this.content = content;
-};
-
-UI.InTemplateScope.prototype.toHTML = function (parentComponent) {
- return HTML.toHTML(this.content, this.parentPtr);
-};
-
-UI.InTemplateScope.prototype.toText = function (textMode, parentComponent) {
- return HTML.toText(this.content, textMode, this.parentPtr);
-};
-
-var isSVGAnchor = function (node) {
- // We generally aren't able to detect SVG elements because
- // if "A" were in our list of known svg element names, then all
- // nodes would be created using
- // `document.createElementNS`. But in the special case of , we can at least detect that attribute and
- // create an SVG tag in that case.
- //
- // However, we still have a general problem of knowing when to
- // use document.createElementNS and when to use
- // document.createElement; for example, font tags will always
- // be created as SVG elements which can cause other
- // problems. #1977
- return (node.tagName === "a" &&
- node.attrs &&
- node.attrs["xlink:href"] !== undefined);
-};
-
-// Convert the pseudoDOM `node` into reactive DOM nodes and insert them
-// into the element or DomRange `parent`, before the node or id `before`.
-var materialize = function (node, parent, before, parentComponent) {
- // XXX should do more error-checking for the case where user is supplying the tags.
- // For example, check that CharRef has `html` and `str` properties and no content.
- // Check that Comment has a single string child and no attributes. Etc.
-
- if (node == null) {
- // null or undefined.
- // do nothinge.
- } else if ((typeof node === 'string') || (typeof node === 'boolean') || (typeof node === 'number')) {
- node = String(node);
- insert(document.createTextNode(node), parent, before);
- } else if (node instanceof Array) {
- for (var i = 0; i < node.length; i++)
- materialize(node[i], parent, before, parentComponent);
- } else if (typeof node === 'function') {
-
- var range = new UI.DomRange;
- var lastContent = null;
- var rangeUpdater = Deps.autorun(function (c) {
- var content = node();
- // normalize content a little, for easier comparison
- if (HTML.isNully(content))
- content = null;
- else if ((content instanceof Array) && content.length === 1)
- content = content[0];
-
- // update if content is different from last time
- if (! contentEquals(content, lastContent)) {
- lastContent = content;
-
- if (! c.firstRun)
- range.removeAll();
-
- materialize(content, range, null, parentComponent);
- }
- });
- range.removed = function () {
- rangeUpdater.stop();
- if (node.stop)
- node.stop();
- };
- // XXXX HACK
- if (Deps.active && node.stop) {
- Deps.onInvalidate(function () {
- node.stop();
- });
- }
- insert(range, parent, before);
- } else if (node instanceof HTML.Tag) {
- var tagName = node.tagName;
- var elem;
- if ((HTML.isKnownSVGElement(tagName) ||
- isSVGAnchor(node)) &&
- document.createElementNS) {
- elem = document.createElementNS('http://www.w3.org/2000/svg', tagName);
- } else {
- elem = document.createElement(node.tagName);
- }
-
- var rawAttrs = node.attrs;
- var children = node.children;
- if (node.tagName === 'textarea') {
- rawAttrs = (rawAttrs || {});
- rawAttrs.value = children;
- children = [];
- };
-
- if (rawAttrs) {
- var attrComp = Deps.autorun(function (c) {
- var attrUpdater = c.updater;
- if (! attrUpdater) {
- attrUpdater = c.updater = new ElementAttributesUpdater(elem);
- }
-
- try {
- var attrs = HTML.evaluateAttributes(rawAttrs, parentComponent);
- var stringAttrs = {};
- if (attrs) {
- for (var k in attrs) {
- stringAttrs[k] = HTML.toText(attrs[k], HTML.TEXTMODE.STRING,
- parentComponent);
- }
- attrUpdater.update(stringAttrs);
- }
- } catch (e) {
- reportUIException(e);
- }
- });
- UI.DomBackend.onElementTeardown(elem, function () {
- attrComp.stop();
- });
- }
- materialize(children, elem, null, parentComponent);
-
- insert(elem, parent, before);
- } else if (typeof node.instantiate === 'function') {
- // component
- var instance = UI.render(node, parentComponent, {
- _nestInCurrentComputation: true
- });
-
- // Call internal callback, which may take advantage of the current
- // Deps computation.
- if (instance.materialized)
- instance.materialized(instance.dom);
-
- insert(instance.dom, parent, before);
- } else if (node instanceof HTML.CharRef) {
- insert(document.createTextNode(node.str), parent, before);
- } else if (node instanceof HTML.Comment) {
- insert(document.createComment(node.sanitizedValue), parent, before);
- } else if (node instanceof HTML.Raw) {
- // Get an array of DOM nodes by using the browser's HTML parser
- // (like innerHTML).
- var htmlNodes = UI.DomBackend.parseHTML(node.value);
- for (var i = 0; i < htmlNodes.length; i++)
- insert(htmlNodes[i], parent, before);
- } else if (Package['html-tools'] && (node instanceof Package['html-tools'].HTMLTools.Special)) {
- throw new Error("Can't materialize Special tag, it's just an intermediate rep");
- } else if (node instanceof UI.InTemplateScope) {
- materialize(node.content, parent, before, node.parentPtr);
- } else {
- // can't get here
- throw new Error("Unexpected node in htmljs: " + node);
- }
-};
-
-
-
-// XXX figure out the right names, and namespace, for these.
-// for example, maybe some of them go in the HTML package.
-UI.materialize = materialize;
-
-UI.body = UI.Component.extend({
- kind: 'body',
- contentParts: [],
- render: function () {
- return this.contentParts;
- },
- // XXX revisit how body works.
- INSTANTIATED: false,
- __helperHost: true
-});
-
-UI.block = function (renderFunc) {
- return UI.Component.extend({ render: renderFunc });
-};
-
-UI.toHTML = function (content, parentComponent) {
- return HTML.toHTML(content, parentComponent);
-};
-
-UI.toRawText = function (content, parentComponent) {
- return HTML.toText(content, HTML.TEXTMODE.STRING, parentComponent);
-};
diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js
index 1df8038ca5..d724a65dda 100644
--- a/packages/ui/render_tests.js
+++ b/packages/ui/render_tests.js
@@ -1,6 +1,4 @@
-var materialize = UI.materialize;
-var toHTML = HTML.toHTML;
-var toCode = HTML.toJS;
+var toCode = BlazeTools.toJS;
var P = HTML.P;
var CharRef = HTML.CharRef;
@@ -15,6 +13,18 @@ var HR = HTML.HR;
var TEXTAREA = HTML.TEXTAREA;
var INPUT = HTML.INPUT;
+var materialize = function (content, parent) {
+ var func = content;
+ if (typeof content !== 'function') {
+ func = function () {
+ return content;
+ };
+ }
+ Blaze.render(func).attach(parent);
+};
+
+var toHTML = Blaze.toHTML;
+
Tinytest.add("ui - render - basic", function (test) {
var run = function (input, expectedInnerHTML, expectedHTML, expectedCode) {
var div = document.createElement("DIV");
@@ -84,8 +94,8 @@ Tinytest.add("ui - render - basic", function (test) {
'HTML.BR({a: [[""]]})');
run(BR({
- x: function () { return function () { return []; }; },
- a: function () { return function () { return ''; }; }}),
+ x: function () { return Blaze.View(function () { return Blaze.View(function () { return []; }); }); },
+ a: function () { return Blaze.View(function () { return Blaze.View(function () { return ''; }); }); }}),
'
',
'
');
});
@@ -133,7 +143,7 @@ Tinytest.add("ui - render - textarea", function (test) {
optNode = null;
}
var div = document.createElement("DIV");
- var node = TEXTAREA(optNode || text);
+ var node = TEXTAREA({value: optNode || text});
materialize(node, div);
var value = div.querySelector('textarea').value;
@@ -147,35 +157,38 @@ Tinytest.add("ui - render - textarea", function (test) {
run('Hello',
'
');
@@ -485,13 +507,16 @@ Tinytest.add("ui - render - components", function (test) {
buf.length = 0;
counter = 1;
- var html = toHTML(myComponent);
+ var html = Blaze.toHTML(makeView());
test.equal(buf, ['created 1',
'parent of 2 is 1',
'created 2',
'parent of 3 is 2',
- 'created 3']);
+ 'created 3',
+ 'destroyed 3',
+ 'destroyed 2',
+ 'destroyed 1']);
test.equal(html, '123
');
})();
@@ -501,19 +526,19 @@ Tinytest.add("ui - render - findAll", function (test) {
var found = null;
var $found = null;
- var myComponent = UI.Component.extend({
- render: function() {
+ var myTemplate = Template.__create__(
+ 'findAllTest',
+ function() {
return DIV([P('first'), P('second')]);
- },
- rendered: function() {
- found = this.findAll('p');
- $found = this.$('p');
- },
- });
+ });
+ myTemplate.rendered = function() {
+ found = this.findAll('p');
+ $found = this.$('p');
+ };
var div = document.createElement("DIV");
- materialize(myComponent, div);
+ Blaze.render(myTemplate).attach(div);
Deps.flush();
test.equal(_.isArray(found), true);
@@ -526,15 +551,16 @@ Tinytest.add("ui - render - reactive attributes 2", function (test) {
var R1 = ReactiveVar(['foo']);
var R2 = ReactiveVar(['bar']);
- var spanCode = SPAN({
- blah: function () { return R1.get(); },
- $dynamic: [function () { return { blah: [function () { return R2.get(); }] }; }]
- });
+ var spanFunc = function () {
+ return SPAN(HTML.Attrs(
+ { blah: function () { return R1.get(); } },
+ function () { return { blah: R2.get() }; }));
+ };
var div = document.createElement("DIV");
- materialize(spanCode, div);
+ Blaze.render(spanFunc).attach(div);
var check = function (expected) {
- test.equal(toHTML(spanCode), expected);
+ test.equal(Blaze.toHTML(spanFunc()), expected);
test.equal(canonicalizeHtml(div.innerHTML), expected);
};
check('');
@@ -545,19 +571,16 @@ Tinytest.add("ui - render - reactive attributes 2", function (test) {
R2.set([[]]);
Deps.flush();
// We combine `['foo']` with what evaluates to `[[[]]]`, which is nully.
- test.equal(spanCode.evaluateAttributes().blah, ["foo"]);
check('');
R2.set([['']]);
Deps.flush();
// We combine `['foo']` with what evaluates to `[[['']]]`, which is non-nully.
- test.equal(spanCode.evaluateAttributes().blah, [[['']]]);
check('');
R2.set(null);
Deps.flush();
// We combine `['foo']` with `[null]`, which is nully.
- test.equal(spanCode.evaluateAttributes().blah, ['foo']);
check('');
R1.set([[], []]);
@@ -611,101 +634,10 @@ Tinytest.add("ui - render - SVG", function (test) {
test.equal(circle.parentNode.namespaceURI, "http://www.w3.org/2000/svg");
});
-Tinytest.add("ui - UI.render", function (test) {
- var div = document.createElement("DIV");
- document.body.appendChild(div);
+Tinytest.add("ui - attributes", function (test) {
+ var SPAN = HTML.SPAN;
+ var amp = HTML.CharRef({html: '&', str: '&'});
- var R = ReactiveVar('aaa');
- var tmpl = UI.Component.extend({
- render: function () {
- var self = this;
- return SPAN(function () {
- return (self.get('greeting') || 'Hello') + ' ' + R.get();
- });
- }
- });
-
- UI.insert(UI.render(tmpl), div);
- UI.insert(UI.renderWithData(tmpl, {greeting: 'Bye'}), div);
- test.equal(canonicalizeHtml(div.innerHTML),
- "Hello aaaBye aaa");
- R.set('bbb');
- Deps.flush();
- test.equal(canonicalizeHtml(div.innerHTML),
- "Hello bbbBye bbb");
-
- document.body.removeChild(div);
-});
-
-Tinytest.add("ui - UI.insert fails on jQuery objects", function (test) {
- var tmpl = UI.Component.extend({
- render: function () {
- return SPAN();
- }
- });
- test.throws(function () {
- UI.insert(UI.render(tmpl), $('body'));
- }, /'parentElement' must be a DOM node/);
- test.throws(function () {
- UI.insert(UI.render(tmpl), document.body, $('body'));
- }, /'nextNode' must be a DOM node/);
-});
-
-Tinytest.add("ui - UI.getDataContext", function (test) {
- var div = document.createElement("DIV");
-
- var tmpl = UI.Component.extend({
- render: function () {
- return SPAN();
- }
- });
-
- UI.insert(UI.renderWithData(tmpl, {foo: "bar"}), div);
- var span = $(div).children('SPAN')[0];
- test.isTrue(span);
- test.equal(UI.getElementData(span), {foo: "bar"});
-});
-
-Tinytest.add("ui - UI.render _nestInCurrentComputation flag", function (test) {
- _.each([true, false], function (nest) {
-
- var firstComputation;
- var rv1 = new ReactiveVar;
- var rv2 = new ReactiveVar;
-
- // Render a component in an autorun. Save the current computation
- // from the first time we run the render function. Invalidate the
- // autorun, and check whether that stops the computation from the
- // first time the component rendered.
-
- var tmpl = UI.Component.extend({
- render: function () {
- return function () {
- if (! firstComputation) {
- firstComputation = Deps.currentComputation;
- }
- return rv1.get();
- };
- }
- });
-
- Deps.autorun(function () {
- rv2.get(); // register a dependency
- UI.render(tmpl, undefined, {
- _nestInCurrentComputation: nest
- });
- });
-
- rv2.set("foo");
- Deps.flush();
-
- // If we nested inside the current computation, then we expect the
- // computation from within the render function to have been stopped
- // when the outer computation was invalidated.
- if (nest) {
- test.equal(firstComputation.stopped, true);
- } else {
- test.equal(firstComputation.stopped, false);
- }
- });
+ test.equal(HTML.toHTML(SPAN({title: ['M', amp, 'Ms']}, 'M', amp, 'M candies')),
+ 'M&M candies');
});
diff --git a/packages/ui/ui.js b/packages/ui/ui.js
new file mode 100644
index 0000000000..e129a29445
--- /dev/null
+++ b/packages/ui/ui.js
@@ -0,0 +1,38 @@
+UI = {};
+
+UI._globalHelpers = {};
+
+UI.registerHelper = function (name, func) {
+ UI._globalHelpers[name] = func;
+};
+
+// Utility to HTML-escape a string.
+UI._escape = (function() {
+ var escape_map = {
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'",
+ "`": "`", /* IE allows backtick-delimited attributes?? */
+ "&": "&"
+ };
+ var escape_one = function(c) {
+ return escape_map[c];
+ };
+
+ return function (x) {
+ return x.replace(/[&<>"'`]/g, escape_one);
+ };
+})();
+
+var jsUrlsAllowed = false;
+UI._allowJavascriptUrls = function () {
+ jsUrlsAllowed = true;
+};
+UI._javascriptUrlsAllowed = function () {
+ return jsUrlsAllowed;
+};
+
+UI._parentData = Blaze._parentData;
+
+UI.getElementData = Blaze.getElementData;
diff --git a/packages/underscore-tests/each_test.js b/packages/underscore-tests/each_test.js
index 50b071ee8d..3b367b5816 100644
--- a/packages/underscore-tests/each_test.js
+++ b/packages/underscore-tests/each_test.js
@@ -1,4 +1,4 @@
-Tinytest.add("underscore - each", function (test) {
+Tinytest.add("underscore-tests - each", function (test) {
// arrays
_.each([42], function (val, index) {
test.equal(index, 0);
diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js
index 93a29cd7e5..a1f2353648 100644
--- a/packages/webapp/webapp_server.js
+++ b/packages/webapp/webapp_server.js
@@ -224,7 +224,7 @@ WebApp._timeoutAdjustmentRequestCallback = function (req, res) {
};
// Will be updated by main before we listen.
-var boilerplateTemplate = null;
+var boilerplateFunc = null;
var boilerplateBaseData = null;
var memoizedBoilerplate = {};
@@ -248,12 +248,9 @@ var getBoilerplate = function (request) {
htmlAttributes: htmlAttributes,
inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed()
}, boilerplateBaseData);
- var boilerplateInstance = boilerplateTemplate.extend({
- data: boilerplateData
- });
- var boilerplateHtmlJs = boilerplateInstance.render();
+
memoizedBoilerplate[boilerplateKey] = "\n" +
- HTML.toHTML(boilerplateHtmlJs, boilerplateInstance);
+ Blaze.toHTML(Blaze.With(boilerplateData, boilerplateFunc));
}
return memoizedBoilerplate[boilerplateKey];
};
@@ -489,8 +486,8 @@ var runWebAppServer = function () {
if (! appUrl(req.url))
return next();
- if (!boilerplateTemplate)
- throw new Error("boilerplateTemplate should be set before listening!");
+ if (!boilerplateFunc)
+ throw new Error("boilerplateFunc should be set before listening!");
if (!boilerplateBaseData)
throw new Error("boilerplateBaseData should be set before listening!");
@@ -663,17 +660,12 @@ var runWebAppServer = function () {
});
var boilerplateTemplateSource = Assets.getText("boilerplate.html");
- var boilerplateRenderCode = Spacebars.compile(
+ var boilerplateRenderCode = SpacebarsCompiler.compile(
boilerplateTemplateSource, { isBody: true });
// Note that we are actually depending on eval's local environment capture
// so that UI and HTML are visible to the eval'd code.
- var boilerplateRender = eval(boilerplateRenderCode);
-
- boilerplateTemplate = UI.Component.extend({
- kind: "MainPage",
- render: boilerplateRender
- });
+ boilerplateFunc = eval(boilerplateRenderCode);
// only start listening after all the startup code has run.
var localPort = parseInt(process.env.PORT) || 0;
diff --git a/scripts/doctool.js b/scripts/doctool.js
new file mode 100755
index 0000000000..1a3622c542
--- /dev/null
+++ b/scripts/doctool.js
@@ -0,0 +1,154 @@
+#!/usr/bin/env node
+
+/// # doctool.js
+///
+/// Usage: `doctool.js ...jsfiles...`
+///
+/// Reads each `.js` file and writes a `.md` file in the same directory.
+/// The output file consists of the concatenation of the "doc comments"
+/// in the input file, which are assumed to contain Markdown content,
+/// including any section headings necessary to organize the file.
+///
+/// A "doc comment" must begin at the start of a line or after
+/// whitespace. There are two kinds of doc comments: `/** ... */`
+/// (block) comments and `/// ...` (triple-slash) comments.
+///
+/// If a file begins with the magic string "///!README", the output
+/// filename is changed to `README.md`.
+///
+/// Examples:
+///
+/// ```
+/// /**
+/// * This is a block comment. The parser strips the sequence,
+/// * [optional whitespace, `*`, optional single space] from
+/// * every line that has it.
+/// *
+/// For lines that don't, no big deal.
+///
+/// Leading whitspace will be preserved here.
+///
+/// * We can create a bullet list in here:
+/// *
+/// * * This is a bullet
+/// */
+/// ```
+///
+/// ```
+/// /** Single-line block comments are also ok. */
+/// ```
+///
+/// ```
+/// /**
+/// A block comment whose first line doesn't have a `*` receives
+/// no stripping of `*` characters on any line.
+///
+/// * This is a bullet
+///
+/// */
+/// ```
+///
+/// ```
+/// /// A triple-slash comment starts with `///` followed by an
+/// /// optional space (i.e. one space is removed if present).
+/// /// Multiple consecutive lines that start with `///` are
+/// /// treated together as a single doc comment.
+/// /** Separate doc comments get separate paragraphs. */
+/// ```
+
+var fs = require('fs');
+var path = require('path');
+
+process.argv.slice(2).forEach(function (fileName) {
+ var text = fs.readFileSync(fileName, "utf8");
+
+ var outFileName = fileName.replace(/\.js$/, '') + '.md';
+ if (text.slice(0, 10) === '///!README') {
+ outFileName = path.join(path.dirname(fileName), 'README.md');
+ text = text.slice(10);
+ }
+
+ var docComments = [];
+ for (;;) {
+ // This regex breaks down as follows:
+ //
+ // 1. Start of line
+ // 2. Optional whitespace (not newline!)
+ // 3. `///` (capturing group 1) or `/**` (group 2)
+ // 4. Looking ahead, NOT `/` or `*`
+ var nextOpener = /^[ \t]*(?:(\/\/\/)|(\/\*\*))(?![\/\*])/m.exec(text);
+ if (! nextOpener)
+ break;
+ text = text.slice(nextOpener.index + nextOpener[0].length);
+ if (nextOpener[1]) {
+ // triple-slash
+ text = text.replace(/^[ \t]/, ''); // optional space
+ var comment = text.match(/^[^\n]*/)[0];
+ text = text.slice(comment.length);
+ var match;
+ while ((match = /^\n[ \t]*\/\/\/[ \t]?/.exec(text))) {
+ // multiple lines in a row become one comment
+ text = text.slice(match[0].length);
+ var restOfLine = text.match(/^[^\n]*/)[0];
+ text = text.slice(restOfLine.length);
+ comment += '\n' + restOfLine;
+ }
+ if (comment.trim())
+ docComments.push(['///', comment]);
+ } else if (nextOpener[2]) {
+ // block comment
+ var rawComment = text.match(/^[\s\S]*?\*\//);
+ if ((! rawComment) || (! rawComment[0]))
+ continue;
+ rawComment = rawComment[0];
+ text = text.slice(rawComment.length);
+ rawComment = rawComment.slice(0, -2); // remove final `*/`
+ if (rawComment.slice(-1) === ' ')
+ // make that ' */' for the benefit of single-line blocks
+ rawComment = rawComment.slice(0, -1);
+
+ var lines = rawComment.split('\n');
+
+ var stripStars = false;
+ if (lines[0].trim().length === 0) {
+ // The comment has a newline after the `/**` (with possible whitespace
+ // between). This is like most comments, though occasionally people
+ // may write `/** foo */` on one line. Skip the blank line.
+ lines.splice(0, 1);
+ if (! lines.length)
+ continue;
+ // Now we determine whether this is block comment with a column of
+ // asterisks running down the left side, so we can strip them.
+ stripStars = /^[ \t]*\*/.test(lines[1]);
+ } else {
+ // Trim beginning of line after `/**`
+ lines[0] = lines[0].replace(/^\s*/, '');
+ }
+
+ lines = lines.map(function (s) {
+ // Strip either up to an asterisk and then an optional space,
+ // or just an optional space, depending on `stripStars`.
+ if (stripStars)
+ return s.replace(/^[ \t]*\* ?/, '');
+ else
+ return s;
+ });
+
+ var result = lines.join('\n');
+
+ if (result.trim())
+ docComments.push(['/**', result]);
+ }
+ }
+
+ if (docComments.length) {
+ var output = docComments.map(function (x) { return x[1]; }).join('\n\n');
+ var fileShortName = path.basename(fileName);
+ output = '*This file is automatically generated from [`' +
+ fileShortName + '`](' + fileShortName + ').*\n\n' + output;
+ fs.writeFileSync(outFileName, output, 'utf8');
+ console.log("Wrote " + docComments.length + " comments to " + outFileName);
+ }
+
+
+});
diff --git a/scripts/doctool.md b/scripts/doctool.md
new file mode 100644
index 0000000000..38f8ae5a6a
--- /dev/null
+++ b/scripts/doctool.md
@@ -0,0 +1,57 @@
+*This file is automatically generated from [`doctool.js`](doctool.js).*
+
+# doctool.js
+
+Usage: `doctool.js ...jsfiles...`
+
+Reads each `.js` file and writes a `.md` file in the same directory.
+The output file consists of the concatenation of the "doc comments"
+in the input file, which are assumed to contain Markdown content,
+including any section headings necessary to organize the file.
+
+A "doc comment" must begin at the start of a line or after
+whitespace. There are two kinds of doc comments: `/** ... */`
+(block) comments and `/// ...` (triple-slash) comments.
+
+If a file begins with the magic string "///!README", the output
+filename is changed to `README.md`.
+
+Examples:
+
+```
+/**
+ * This is a block comment. The parser strips the sequence,
+ * [optional whitespace, `*`, optional single space] from
+ * every line that has it.
+ *
+For lines that don't, no big deal.
+
+ Leading whitspace will be preserved here.
+
+ * We can create a bullet list in here:
+ *
+ * * This is a bullet
+ */
+```
+
+```
+/** Single-line block comments are also ok. */
+```
+
+```
+/**
+A block comment whose first line doesn't have a `*` receives
+no stripping of `*` characters on any line.
+
+* This is a bullet
+
+*/
+```
+
+```
+/// A triple-slash comment starts with `///` followed by an
+/// optional space (i.e. one space is removed if present).
+/// Multiple consecutive lines that start with `///` are
+/// treated together as a single doc comment.
+/** Separate doc comments get separate paragraphs. */
+```
\ No newline at end of file
diff --git a/scripts/doctool.md.md b/scripts/doctool.md.md
new file mode 100644
index 0000000000..3988cc27ef
--- /dev/null
+++ b/scripts/doctool.md.md
@@ -0,0 +1,30 @@
+*This file is automatically generated from [`doctool.md`](doctool.md).*
+
+This is a block comment. The parser strips the sequence,
+[optional whitespace, `*`, optional single space] from
+every line that has it.
+
+For lines that don't, no big deal.
+
+ Leading whitspace will be preserved here.
+
+We can create a bullet list in here:
+
+* This is a bullet
+
+
+Single-line block comments are also ok.
+
+A block comment whose first line doesn't have a `*` receives
+no stripping of `*` characters on any line.
+
+* This is a bullet
+
+
+
+A triple-slash comment starts with `///` followed by an
+optional space (i.e. one space is removed if present).
+Multiple consecutive lines that start with `///` are
+treated together as a single doc comment.
+
+Separate doc comments get separate paragraphs.
\ No newline at end of file