mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Literally just deleted packages
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
# blaze-html-templates
|
||||
|
||||
A meta-package that includes everything you need to compile and run Meteor templates with Spacebars and Blaze.
|
||||
|
||||
For more details, see the documentation of the component packages:
|
||||
|
||||
- [templating](https://atmospherejs.com/meteor/templating): compiles `.html` files
|
||||
- [blaze](https://atmospherejs.com/meteor/blaze): the runtime library
|
||||
- [spacebars](https://atmospherejs.com/meteor/spacebars): the templating language
|
||||
@@ -1,25 +0,0 @@
|
||||
Package.describe({
|
||||
name: 'blaze-html-templates',
|
||||
version: '1.0.4',
|
||||
// Brief, one-line summary of the package.
|
||||
summary: 'Compile HTML templates into reactive UI with Meteor Blaze',
|
||||
// By default, Meteor will default to using README.md for documentation.
|
||||
// To avoid submitting documentation, set this field to null.
|
||||
documentation: 'README.md'
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.imply([
|
||||
// A library for reactive user interfaces
|
||||
'blaze',
|
||||
|
||||
// The following packages are basically empty shells that just exist to
|
||||
// satisfy code checking for the existence of a package. Rest assured that
|
||||
// they are not adding any bloat to your bundle.
|
||||
'ui', // XXX COMPAT WITH PACKAGES BUILT FOR 0.9.0.
|
||||
'spacebars', // XXX COMPAT WITH PACKAGES BUILT FOR 0.9.0
|
||||
|
||||
// Compile .html files into Blaze reactive views
|
||||
'templating'
|
||||
]);
|
||||
});
|
||||
1
packages/blaze-tools/.gitignore
vendored
1
packages/blaze-tools/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.build*
|
||||
@@ -1,4 +0,0 @@
|
||||
# blaze-tools
|
||||
|
||||
Compile-time utilities that are likely to be useful to any package
|
||||
that compiles templates for Blaze.
|
||||
@@ -1,24 +0,0 @@
|
||||
Package.describe({
|
||||
summary: "Compile-time tools for Blaze",
|
||||
version: '1.0.9'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.export('BlazeTools');
|
||||
|
||||
api.use('htmljs');
|
||||
api.use('underscore');
|
||||
|
||||
api.addFiles(['preamble.js',
|
||||
'tokens.js',
|
||||
'tojs.js']);
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use('blaze-tools');
|
||||
api.use('tinytest');
|
||||
api.use('underscore');
|
||||
api.use('html-tools');
|
||||
|
||||
api.addFiles(['token_tests.js']);
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
BlazeTools = {};
|
||||
@@ -1,156 +0,0 @@
|
||||
|
||||
BlazeTools.EmitCode = function (value) {
|
||||
if (! (this instanceof BlazeTools.EmitCode))
|
||||
// called without `new`
|
||||
return new BlazeTools.EmitCode(value);
|
||||
|
||||
if (typeof value !== 'string')
|
||||
throw new Error('BlazeTools.EmitCode must be constructed with a string');
|
||||
|
||||
this.value = value;
|
||||
};
|
||||
BlazeTools.EmitCode.prototype.toJS = function (visitor) {
|
||||
return this.value;
|
||||
};
|
||||
|
||||
// Turns any JSONable value into a JavaScript literal.
|
||||
toJSLiteral = function (obj) {
|
||||
// See <http://timelessrepo.com/json-isnt-a-javascript-subset> for `\u2028\u2029`.
|
||||
// Also escape Unicode surrogates.
|
||||
return (JSON.stringify(obj)
|
||||
.replace(/[\u2028\u2029\ud800-\udfff]/g, function (c) {
|
||||
return '\\u' + ('000' + c.charCodeAt(0).toString(16)).slice(-4);
|
||||
}));
|
||||
};
|
||||
BlazeTools.toJSLiteral = toJSLiteral;
|
||||
|
||||
|
||||
|
||||
var jsReservedWordSet = (function (set) {
|
||||
_.each("abstract else instanceof super boolean enum int switch break export interface synchronized byte extends let this case false long throw catch final native throws char finally new transient class float null true const for package try continue function private typeof debugger goto protected var default if public void delete implements return volatile do import short while double in static with".split(' '), function (w) {
|
||||
set[w] = 1;
|
||||
});
|
||||
return set;
|
||||
})({});
|
||||
|
||||
toObjectLiteralKey = function (k) {
|
||||
if (/^[a-zA-Z$_][a-zA-Z$0-9_]*$/.test(k) && jsReservedWordSet[k] !== 1)
|
||||
return k;
|
||||
return toJSLiteral(k);
|
||||
};
|
||||
BlazeTools.toObjectLiteralKey = toObjectLiteralKey;
|
||||
|
||||
var hasToJS = function (x) {
|
||||
return x.toJS && (typeof (x.toJS) === 'function');
|
||||
};
|
||||
|
||||
ToJSVisitor = HTML.Visitor.extend();
|
||||
ToJSVisitor.def({
|
||||
visitNull: function (nullOrUndefined) {
|
||||
return 'null';
|
||||
},
|
||||
visitPrimitive: function (stringBooleanOrNumber) {
|
||||
return toJSLiteral(stringBooleanOrNumber);
|
||||
},
|
||||
visitArray: function (array) {
|
||||
var parts = [];
|
||||
for (var i = 0; i < array.length; i++)
|
||||
parts.push(this.visit(array[i]));
|
||||
return '[' + parts.join(', ') + ']';
|
||||
},
|
||||
visitTag: function (tag) {
|
||||
return this.generateCall(tag.tagName, tag.attrs, tag.children);
|
||||
},
|
||||
visitComment: function (comment) {
|
||||
return this.generateCall('HTML.Comment', null, [comment.value]);
|
||||
},
|
||||
visitCharRef: function (charRef) {
|
||||
return this.generateCall('HTML.CharRef',
|
||||
{html: charRef.html, str: charRef.str});
|
||||
},
|
||||
visitRaw: function (raw) {
|
||||
return this.generateCall('HTML.Raw', null, [raw.value]);
|
||||
},
|
||||
visitObject: function (x) {
|
||||
if (hasToJS(x)) {
|
||||
return x.toJS(this);
|
||||
}
|
||||
|
||||
throw new Error("Unexpected object in HTMLjs in toJS: " + x);
|
||||
},
|
||||
generateCall: function (name, attrs, children) {
|
||||
var tagSymbol;
|
||||
if (name.indexOf('.') >= 0) {
|
||||
tagSymbol = name;
|
||||
} else if (HTML.isTagEnsured(name)) {
|
||||
tagSymbol = 'HTML.' + HTML.getSymbolName(name);
|
||||
} else {
|
||||
tagSymbol = 'HTML.getTag(' + toJSLiteral(name) + ')';
|
||||
}
|
||||
|
||||
var attrsArray = null;
|
||||
if (attrs) {
|
||||
attrsArray = [];
|
||||
var needsHTMLAttrs = false;
|
||||
if (HTML.isArray(attrs)) {
|
||||
var attrsArray = [];
|
||||
for (var i = 0; i < attrs.length; i++) {
|
||||
var a = attrs[i];
|
||||
if (hasToJS(a)) {
|
||||
attrsArray.push(a.toJS(this));
|
||||
needsHTMLAttrs = true;
|
||||
} else {
|
||||
var attrsObjStr = this.generateAttrsDictionary(attrs[i]);
|
||||
if (attrsObjStr !== null)
|
||||
attrsArray.push(attrsObjStr);
|
||||
}
|
||||
}
|
||||
} else if (hasToJS(attrs)) {
|
||||
attrsArray.push(attrs.toJS(this));
|
||||
needsHTMLAttrs = true;
|
||||
} else {
|
||||
attrsArray.push(this.generateAttrsDictionary(attrs));
|
||||
}
|
||||
}
|
||||
var attrsStr = null;
|
||||
if (attrsArray && attrsArray.length) {
|
||||
if (attrsArray.length === 1 && ! needsHTMLAttrs) {
|
||||
attrsStr = attrsArray[0];
|
||||
} else {
|
||||
attrsStr = 'HTML.Attrs(' + attrsArray.join(', ') + ')';
|
||||
}
|
||||
}
|
||||
|
||||
var argStrs = [];
|
||||
if (attrsStr !== null)
|
||||
argStrs.push(attrsStr);
|
||||
|
||||
if (children) {
|
||||
for (var i = 0; i < children.length; i++)
|
||||
argStrs.push(this.visit(children[i]));
|
||||
}
|
||||
|
||||
return tagSymbol + '(' + argStrs.join(', ') + ')';
|
||||
},
|
||||
generateAttrsDictionary: function (attrsDict) {
|
||||
if (attrsDict.toJS && (typeof (attrsDict.toJS) === 'function')) {
|
||||
// not an attrs dictionary, but something else! Like a template tag.
|
||||
return attrsDict.toJS(this);
|
||||
}
|
||||
|
||||
var kvStrs = [];
|
||||
for (var k in attrsDict) {
|
||||
if (! HTML.isNully(attrsDict[k]))
|
||||
kvStrs.push(toObjectLiteralKey(k) + ': ' +
|
||||
this.visit(attrsDict[k]));
|
||||
}
|
||||
if (kvStrs.length)
|
||||
return '{' + kvStrs.join(', ') + '}';
|
||||
return null;
|
||||
}
|
||||
});
|
||||
BlazeTools.ToJSVisitor = ToJSVisitor;
|
||||
|
||||
BlazeTools.toJS = function (content) {
|
||||
return (new ToJSVisitor).visit(content);
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
Tinytest.add("blaze-tools - token parsers", function (test) {
|
||||
|
||||
var run = function (func, input, expected) {
|
||||
var scanner = new HTMLTools.Scanner('z' + input);
|
||||
// make sure the parse function respects `scanner.pos`
|
||||
scanner.pos = 1;
|
||||
var result = func(scanner);
|
||||
if (expected === null) {
|
||||
test.equal(scanner.pos, 1);
|
||||
test.equal(result, null);
|
||||
} else {
|
||||
test.isTrue(scanner.isEOF());
|
||||
test.equal(result, expected);
|
||||
}
|
||||
};
|
||||
|
||||
var runValue = function (func, input, expectedValue) {
|
||||
var expected;
|
||||
if (expectedValue === null)
|
||||
expected = null;
|
||||
else
|
||||
expected = { text: input, value: expectedValue };
|
||||
run(func, input, expected);
|
||||
};
|
||||
|
||||
var parseNumber = BlazeTools.parseNumber;
|
||||
var parseIdentifierName = BlazeTools.parseIdentifierName;
|
||||
var parseExtendedIdentifierName = BlazeTools.parseExtendedIdentifierName;
|
||||
var parseStringLiteral = BlazeTools.parseStringLiteral;
|
||||
|
||||
runValue(parseNumber, "0", 0);
|
||||
runValue(parseNumber, "-0", 0);
|
||||
runValue(parseNumber, "-", null);
|
||||
runValue(parseNumber, ".a", null);
|
||||
runValue(parseNumber, ".1", 0.1);
|
||||
runValue(parseNumber, "1.", 1);
|
||||
runValue(parseNumber, "1.1", 1.1);
|
||||
runValue(parseNumber, "0x", null);
|
||||
runValue(parseNumber, "0xa", 10);
|
||||
runValue(parseNumber, "-0xa", -10);
|
||||
runValue(parseNumber, "1e+1", 10);
|
||||
|
||||
_.each([parseIdentifierName, parseExtendedIdentifierName], function (f) {
|
||||
run(f, "a", "a");
|
||||
run(f, "true", "true");
|
||||
run(f, "null", "null");
|
||||
run(f, "if", "if");
|
||||
run(f, "1", null);
|
||||
run(f, "1a", null);
|
||||
run(f, "+a", null);
|
||||
run(f, "a1", "a1");
|
||||
run(f, "a1a", "a1a");
|
||||
run(f, "_a8f_f8d88_", "_a8f_f8d88_");
|
||||
});
|
||||
run(parseIdentifierName, "@index", null);
|
||||
run(parseExtendedIdentifierName, "@index", "@index");
|
||||
run(parseExtendedIdentifierName, "@something", "@something");
|
||||
run(parseExtendedIdentifierName, "@", null);
|
||||
|
||||
runValue(parseStringLiteral, '"a"', 'a');
|
||||
runValue(parseStringLiteral, '"\'"', "'");
|
||||
runValue(parseStringLiteral, '\'"\'', '"');
|
||||
runValue(parseStringLiteral, '"a\\\nb"', 'ab'); // line continuation
|
||||
runValue(parseStringLiteral, '"a\u0062c"', 'abc');
|
||||
// Note: IE 8 doesn't correctly parse '\v' in JavaScript.
|
||||
runValue(parseStringLiteral, '"\\0\\b\\f\\n\\r\\t\\v"', '\0\b\f\n\r\t\u000b');
|
||||
runValue(parseStringLiteral, '"\\x41"', 'A');
|
||||
runValue(parseStringLiteral, '"\\\\"', '\\');
|
||||
runValue(parseStringLiteral, '"\\\""', '\"');
|
||||
runValue(parseStringLiteral, '"\\\'"', '\'');
|
||||
runValue(parseStringLiteral, "'\\\\'", '\\');
|
||||
runValue(parseStringLiteral, "'\\\"'", '\"');
|
||||
runValue(parseStringLiteral, "'\\\''", '\'');
|
||||
|
||||
test.throws(function () {
|
||||
run(parseStringLiteral, "'this is my string");
|
||||
}, /Unterminated string literal/);
|
||||
});
|
||||
@@ -1,193 +0,0 @@
|
||||
|
||||
// Adapted from source code of http://xregexp.com/plugins/#unicode
|
||||
var unicodeCategories = {
|
||||
Ll: "0061-007A00B500DF-00F600F8-00FF01010103010501070109010B010D010F01110113011501170119011B011D011F01210123012501270129012B012D012F01310133013501370138013A013C013E014001420144014601480149014B014D014F01510153015501570159015B015D015F01610163016501670169016B016D016F0171017301750177017A017C017E-0180018301850188018C018D019201950199-019B019E01A101A301A501A801AA01AB01AD01B001B401B601B901BA01BD-01BF01C601C901CC01CE01D001D201D401D601D801DA01DC01DD01DF01E101E301E501E701E901EB01ED01EF01F001F301F501F901FB01FD01FF02010203020502070209020B020D020F02110213021502170219021B021D021F02210223022502270229022B022D022F02310233-0239023C023F0240024202470249024B024D024F-02930295-02AF037103730377037B-037D039003AC-03CE03D003D103D5-03D703D903DB03DD03DF03E103E303E503E703E903EB03ED03EF-03F303F503F803FB03FC0430-045F04610463046504670469046B046D046F04710473047504770479047B047D047F0481048B048D048F04910493049504970499049B049D049F04A104A304A504A704A904AB04AD04AF04B104B304B504B704B904BB04BD04BF04C204C404C604C804CA04CC04CE04CF04D104D304D504D704D904DB04DD04DF04E104E304E504E704E904EB04ED04EF04F104F304F504F704F904FB04FD04FF05010503050505070509050B050D050F05110513051505170519051B051D051F05210523052505270561-05871D00-1D2B1D6B-1D771D79-1D9A1E011E031E051E071E091E0B1E0D1E0F1E111E131E151E171E191E1B1E1D1E1F1E211E231E251E271E291E2B1E2D1E2F1E311E331E351E371E391E3B1E3D1E3F1E411E431E451E471E491E4B1E4D1E4F1E511E531E551E571E591E5B1E5D1E5F1E611E631E651E671E691E6B1E6D1E6F1E711E731E751E771E791E7B1E7D1E7F1E811E831E851E871E891E8B1E8D1E8F1E911E931E95-1E9D1E9F1EA11EA31EA51EA71EA91EAB1EAD1EAF1EB11EB31EB51EB71EB91EBB1EBD1EBF1EC11EC31EC51EC71EC91ECB1ECD1ECF1ED11ED31ED51ED71ED91EDB1EDD1EDF1EE11EE31EE51EE71EE91EEB1EED1EEF1EF11EF31EF51EF71EF91EFB1EFD1EFF-1F071F10-1F151F20-1F271F30-1F371F40-1F451F50-1F571F60-1F671F70-1F7D1F80-1F871F90-1F971FA0-1FA71FB0-1FB41FB61FB71FBE1FC2-1FC41FC61FC71FD0-1FD31FD61FD71FE0-1FE71FF2-1FF41FF61FF7210A210E210F2113212F21342139213C213D2146-2149214E21842C30-2C5E2C612C652C662C682C6A2C6C2C712C732C742C76-2C7B2C812C832C852C872C892C8B2C8D2C8F2C912C932C952C972C992C9B2C9D2C9F2CA12CA32CA52CA72CA92CAB2CAD2CAF2CB12CB32CB52CB72CB92CBB2CBD2CBF2CC12CC32CC52CC72CC92CCB2CCD2CCF2CD12CD32CD52CD72CD92CDB2CDD2CDF2CE12CE32CE42CEC2CEE2CF32D00-2D252D272D2DA641A643A645A647A649A64BA64DA64FA651A653A655A657A659A65BA65DA65FA661A663A665A667A669A66BA66DA681A683A685A687A689A68BA68DA68FA691A693A695A697A723A725A727A729A72BA72DA72F-A731A733A735A737A739A73BA73DA73FA741A743A745A747A749A74BA74DA74FA751A753A755A757A759A75BA75DA75FA761A763A765A767A769A76BA76DA76FA771-A778A77AA77CA77FA781A783A785A787A78CA78EA791A793A7A1A7A3A7A5A7A7A7A9A7FAFB00-FB06FB13-FB17FF41-FF5A",
|
||||
Lm: "02B0-02C102C6-02D102E0-02E402EC02EE0374037A0559064006E506E607F407F507FA081A0824082809710E460EC610FC17D718431AA71C78-1C7D1D2C-1D6A1D781D9B-1DBF2071207F2090-209C2C7C2C7D2D6F2E2F30053031-3035303B309D309E30FC-30FEA015A4F8-A4FDA60CA67FA717-A71FA770A788A7F8A7F9A9CFAA70AADDAAF3AAF4FF70FF9EFF9F",
|
||||
Lo: "00AA00BA01BB01C0-01C3029405D0-05EA05F0-05F20620-063F0641-064A066E066F0671-06D306D506EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA0800-08150840-085808A008A2-08AC0904-0939093D09500958-09610972-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E450E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10D0-10FA10FD-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317DC1820-18421844-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541B05-1B331B45-1B4B1B83-1BA01BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C771CE9-1CEC1CEE-1CF11CF51CF62135-21382D30-2D672D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE3006303C3041-3096309F30A1-30FA30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCCA000-A014A016-A48CA4D0-A4F7A500-A60BA610-A61FA62AA62BA66EA6A0-A6E5A7FB-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2AA00-AA28AA40-AA42AA44-AA4BAA60-AA6FAA71-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADBAADCAAE0-AAEAAAF2AB01-AB06AB09-AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF66-FF6FFF71-FF9DFFA0-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC",
|
||||
Lt: "01C501C801CB01F21F88-1F8F1F98-1F9F1FA8-1FAF1FBC1FCC1FFC",
|
||||
Lu: "0041-005A00C0-00D600D8-00DE01000102010401060108010A010C010E01100112011401160118011A011C011E01200122012401260128012A012C012E01300132013401360139013B013D013F0141014301450147014A014C014E01500152015401560158015A015C015E01600162016401660168016A016C016E017001720174017601780179017B017D018101820184018601870189-018B018E-0191019301940196-0198019C019D019F01A001A201A401A601A701A901AC01AE01AF01B1-01B301B501B701B801BC01C401C701CA01CD01CF01D101D301D501D701D901DB01DE01E001E201E401E601E801EA01EC01EE01F101F401F6-01F801FA01FC01FE02000202020402060208020A020C020E02100212021402160218021A021C021E02200222022402260228022A022C022E02300232023A023B023D023E02410243-02460248024A024C024E03700372037603860388-038A038C038E038F0391-03A103A3-03AB03CF03D2-03D403D803DA03DC03DE03E003E203E403E603E803EA03EC03EE03F403F703F903FA03FD-042F04600462046404660468046A046C046E04700472047404760478047A047C047E0480048A048C048E04900492049404960498049A049C049E04A004A204A404A604A804AA04AC04AE04B004B204B404B604B804BA04BC04BE04C004C104C304C504C704C904CB04CD04D004D204D404D604D804DA04DC04DE04E004E204E404E604E804EA04EC04EE04F004F204F404F604F804FA04FC04FE05000502050405060508050A050C050E05100512051405160518051A051C051E05200522052405260531-055610A0-10C510C710CD1E001E021E041E061E081E0A1E0C1E0E1E101E121E141E161E181E1A1E1C1E1E1E201E221E241E261E281E2A1E2C1E2E1E301E321E341E361E381E3A1E3C1E3E1E401E421E441E461E481E4A1E4C1E4E1E501E521E541E561E581E5A1E5C1E5E1E601E621E641E661E681E6A1E6C1E6E1E701E721E741E761E781E7A1E7C1E7E1E801E821E841E861E881E8A1E8C1E8E1E901E921E941E9E1EA01EA21EA41EA61EA81EAA1EAC1EAE1EB01EB21EB41EB61EB81EBA1EBC1EBE1EC01EC21EC41EC61EC81ECA1ECC1ECE1ED01ED21ED41ED61ED81EDA1EDC1EDE1EE01EE21EE41EE61EE81EEA1EEC1EEE1EF01EF21EF41EF61EF81EFA1EFC1EFE1F08-1F0F1F18-1F1D1F28-1F2F1F38-1F3F1F48-1F4D1F591F5B1F5D1F5F1F68-1F6F1FB8-1FBB1FC8-1FCB1FD8-1FDB1FE8-1FEC1FF8-1FFB21022107210B-210D2110-211221152119-211D212421262128212A-212D2130-2133213E213F214521832C00-2C2E2C602C62-2C642C672C692C6B2C6D-2C702C722C752C7E-2C802C822C842C862C882C8A2C8C2C8E2C902C922C942C962C982C9A2C9C2C9E2CA02CA22CA42CA62CA82CAA2CAC2CAE2CB02CB22CB42CB62CB82CBA2CBC2CBE2CC02CC22CC42CC62CC82CCA2CCC2CCE2CD02CD22CD42CD62CD82CDA2CDC2CDE2CE02CE22CEB2CED2CF2A640A642A644A646A648A64AA64CA64EA650A652A654A656A658A65AA65CA65EA660A662A664A666A668A66AA66CA680A682A684A686A688A68AA68CA68EA690A692A694A696A722A724A726A728A72AA72CA72EA732A734A736A738A73AA73CA73EA740A742A744A746A748A74AA74CA74EA750A752A754A756A758A75AA75CA75EA760A762A764A766A768A76AA76CA76EA779A77BA77DA77EA780A782A784A786A78BA78DA790A792A7A0A7A2A7A4A7A6A7A8A7AAFF21-FF3A",
|
||||
Mc: "0903093B093E-09400949-094C094E094F0982098309BE-09C009C709C809CB09CC09D70A030A3E-0A400A830ABE-0AC00AC90ACB0ACC0B020B030B3E0B400B470B480B4B0B4C0B570BBE0BBF0BC10BC20BC6-0BC80BCA-0BCC0BD70C01-0C030C41-0C440C820C830CBE0CC0-0CC40CC70CC80CCA0CCB0CD50CD60D020D030D3E-0D400D46-0D480D4A-0D4C0D570D820D830DCF-0DD10DD8-0DDF0DF20DF30F3E0F3F0F7F102B102C10311038103B103C105610571062-10641067-106D108310841087-108C108F109A-109C17B617BE-17C517C717C81923-19261929-192B193019311933-193819B0-19C019C819C91A19-1A1B1A551A571A611A631A641A6D-1A721B041B351B3B1B3D-1B411B431B441B821BA11BA61BA71BAA1BAC1BAD1BE71BEA-1BEC1BEE1BF21BF31C24-1C2B1C341C351CE11CF21CF3302E302FA823A824A827A880A881A8B4-A8C3A952A953A983A9B4A9B5A9BAA9BBA9BD-A9C0AA2FAA30AA33AA34AA4DAA7BAAEBAAEEAAEFAAF5ABE3ABE4ABE6ABE7ABE9ABEAABEC",
|
||||
Mn: "0300-036F0483-04870591-05BD05BF05C105C205C405C505C70610-061A064B-065F067006D6-06DC06DF-06E406E706E806EA-06ED07110730-074A07A6-07B007EB-07F30816-0819081B-08230825-08270829-082D0859-085B08E4-08FE0900-0902093A093C0941-0948094D0951-095709620963098109BC09C1-09C409CD09E209E30A010A020A3C0A410A420A470A480A4B-0A4D0A510A700A710A750A810A820ABC0AC1-0AC50AC70AC80ACD0AE20AE30B010B3C0B3F0B41-0B440B4D0B560B620B630B820BC00BCD0C3E-0C400C46-0C480C4A-0C4D0C550C560C620C630CBC0CBF0CC60CCC0CCD0CE20CE30D41-0D440D4D0D620D630DCA0DD2-0DD40DD60E310E34-0E3A0E47-0E4E0EB10EB4-0EB90EBB0EBC0EC8-0ECD0F180F190F350F370F390F71-0F7E0F80-0F840F860F870F8D-0F970F99-0FBC0FC6102D-10301032-10371039103A103D103E10581059105E-10601071-1074108210851086108D109D135D-135F1712-17141732-1734175217531772177317B417B517B7-17BD17C617C9-17D317DD180B-180D18A91920-19221927192819321939-193B1A171A181A561A58-1A5E1A601A621A65-1A6C1A73-1A7C1A7F1B00-1B031B341B36-1B3A1B3C1B421B6B-1B731B801B811BA2-1BA51BA81BA91BAB1BE61BE81BE91BED1BEF-1BF11C2C-1C331C361C371CD0-1CD21CD4-1CE01CE2-1CE81CED1CF41DC0-1DE61DFC-1DFF20D0-20DC20E120E5-20F02CEF-2CF12D7F2DE0-2DFF302A-302D3099309AA66FA674-A67DA69FA6F0A6F1A802A806A80BA825A826A8C4A8E0-A8F1A926-A92DA947-A951A980-A982A9B3A9B6-A9B9A9BCAA29-AA2EAA31AA32AA35AA36AA43AA4CAAB0AAB2-AAB4AAB7AAB8AABEAABFAAC1AAECAAEDAAF6ABE5ABE8ABEDFB1EFE00-FE0FFE20-FE26",
|
||||
Nd: "0030-00390660-066906F0-06F907C0-07C90966-096F09E6-09EF0A66-0A6F0AE6-0AEF0B66-0B6F0BE6-0BEF0C66-0C6F0CE6-0CEF0D66-0D6F0E50-0E590ED0-0ED90F20-0F291040-10491090-109917E0-17E91810-18191946-194F19D0-19D91A80-1A891A90-1A991B50-1B591BB0-1BB91C40-1C491C50-1C59A620-A629A8D0-A8D9A900-A909A9D0-A9D9AA50-AA59ABF0-ABF9FF10-FF19",
|
||||
Nl: "16EE-16F02160-21822185-218830073021-30293038-303AA6E6-A6EF",
|
||||
Pc: "005F203F20402054FE33FE34FE4D-FE4FFF3F"
|
||||
};
|
||||
|
||||
var unicodeClass = function (abbrev) {
|
||||
return '[' +
|
||||
unicodeCategories[abbrev].replace(/[0-9A-F]{4}/ig, "\\u$&") + ']';
|
||||
};
|
||||
|
||||
// See ECMA-262 spec, 3rd edition, Section 7.6
|
||||
// Match one or more characters that can start an identifier.
|
||||
// This is IdentifierStart+.
|
||||
var rIdentifierPrefix = new RegExp(
|
||||
"^([a-zA-Z$_]+|\\\\u[0-9a-fA-F]{4}|" +
|
||||
[unicodeClass('Lu'), unicodeClass('Ll'), unicodeClass('Lt'),
|
||||
unicodeClass('Lm'), unicodeClass('Lo'), unicodeClass('Nl')].join('|') +
|
||||
")+");
|
||||
// Match one or more characters that can continue an identifier.
|
||||
// This is (IdentifierPart and not IdentifierStart)+.
|
||||
// To match a full identifier, match rIdentifierPrefix, then
|
||||
// match rIdentifierMiddle followed by rIdentifierPrefix until they both fail.
|
||||
var rIdentifierMiddle = new RegExp(
|
||||
"^([0-9]|" + [unicodeClass('Mn'), unicodeClass('Mc'), unicodeClass('Nd'),
|
||||
unicodeClass('Pc')].join('|') + ")+");
|
||||
|
||||
|
||||
// See ECMA-262 spec, 3rd edition, Section 7.8.3
|
||||
var rHexLiteral = /^0[xX][0-9a-fA-F]+(?!\w)/;
|
||||
var rDecLiteral =
|
||||
/^(((0|[1-9][0-9]*)(\.[0-9]*)?)|\.[0-9]+)([Ee][+-]?[0-9]+)?(?!\w)/;
|
||||
|
||||
// Section 7.8.4
|
||||
var rStringQuote = /^["']/;
|
||||
// Match one or more characters besides quotes, backslashes, or line ends
|
||||
var rStringMiddle = /^(?=.)[^"'\\]+?((?!.)|(?=["'\\]))/;
|
||||
// Match one escape sequence, including the backslash.
|
||||
var rEscapeSequence =
|
||||
/^\\(['"\\bfnrtv]|0(?![0-9])|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|(?=.)[^ux0-9])/;
|
||||
// Match one ES5 line continuation
|
||||
var rLineContinuation =
|
||||
/^\\(\r\n|[\u000A\u000D\u2028\u2029])/;
|
||||
|
||||
|
||||
BlazeTools.parseNumber = function (scanner) {
|
||||
var startPos = scanner.pos;
|
||||
|
||||
var isNegative = false;
|
||||
if (scanner.peek() === '-') {
|
||||
scanner.pos++;
|
||||
isNegative = true;
|
||||
}
|
||||
// Note that we allow `"-0xa"`, unlike `Number(...)`.
|
||||
|
||||
var rest = scanner.rest();
|
||||
var match = rDecLiteral.exec(rest) || rHexLiteral.exec(rest);
|
||||
if (! match) {
|
||||
scanner.pos = startPos;
|
||||
return null;
|
||||
}
|
||||
var matchText = match[0];
|
||||
scanner.pos += matchText.length;
|
||||
|
||||
var text = (isNegative ? '-' : '') + matchText;
|
||||
var value = Number(matchText);
|
||||
value = (isNegative ? -value : value);
|
||||
return { text: text, value: value };
|
||||
};
|
||||
|
||||
BlazeTools.parseIdentifierName = function (scanner) {
|
||||
var startPos = scanner.pos;
|
||||
var rest = scanner.rest();
|
||||
var match = rIdentifierPrefix.exec(rest);
|
||||
if (! match)
|
||||
return null;
|
||||
scanner.pos += match[0].length;
|
||||
rest = scanner.rest();
|
||||
var foundMore = true;
|
||||
|
||||
while (foundMore) {
|
||||
foundMore = false;
|
||||
|
||||
match = rIdentifierMiddle.exec(rest);
|
||||
if (match) {
|
||||
foundMore = true;
|
||||
scanner.pos += match[0].length;
|
||||
rest = scanner.rest();
|
||||
}
|
||||
|
||||
match = rIdentifierPrefix.exec(rest);
|
||||
if (match) {
|
||||
foundMore = true;
|
||||
scanner.pos += match[0].length;
|
||||
rest = scanner.rest();
|
||||
}
|
||||
}
|
||||
|
||||
return scanner.input.substring(startPos, scanner.pos);
|
||||
};
|
||||
|
||||
BlazeTools.parseExtendedIdentifierName = function (scanner) {
|
||||
// parse an identifier name optionally preceded by '@'
|
||||
if (scanner.peek() === '@') {
|
||||
scanner.pos++;
|
||||
var afterAt = BlazeTools.parseIdentifierName(scanner);
|
||||
if (afterAt) {
|
||||
return '@' + afterAt;
|
||||
} else {
|
||||
scanner.pos--;
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return BlazeTools.parseIdentifierName(scanner);
|
||||
}
|
||||
};
|
||||
|
||||
BlazeTools.parseStringLiteral = function (scanner) {
|
||||
var startPos = scanner.pos;
|
||||
var rest = scanner.rest();
|
||||
var match = rStringQuote.exec(rest);
|
||||
if (! match)
|
||||
return null;
|
||||
|
||||
var quote = match[0];
|
||||
scanner.pos++;
|
||||
rest = scanner.rest();
|
||||
|
||||
var jsonLiteral = '"';
|
||||
|
||||
while (match) {
|
||||
match = rStringMiddle.exec(rest);
|
||||
if (match) {
|
||||
jsonLiteral += match[0];
|
||||
} else {
|
||||
match = rEscapeSequence.exec(rest);
|
||||
if (match) {
|
||||
var esc = match[0];
|
||||
// Convert all string escapes to JSON-compatible string escapes, so we
|
||||
// can use JSON.parse for some of the work. JSON strings are not the
|
||||
// same as JS strings. They don't support `\0`, `\v`, `\'`, or hex
|
||||
// escapes.
|
||||
if (esc === '\\0')
|
||||
jsonLiteral += '\\u0000';
|
||||
else if (esc === '\\v')
|
||||
// Note: IE 8 doesn't correctly parse '\v' in JavaScript.
|
||||
jsonLiteral += '\\u000b';
|
||||
else if (esc.charAt(1) === 'x')
|
||||
jsonLiteral += '\\u00' + esc.slice(2);
|
||||
else if (esc === '\\\'')
|
||||
jsonLiteral += "'";
|
||||
else
|
||||
jsonLiteral += esc;
|
||||
} else {
|
||||
match = rLineContinuation.exec(rest);
|
||||
if (! match) {
|
||||
match = rStringQuote.exec(rest);
|
||||
if (match) {
|
||||
var c = match[0];
|
||||
if (c !== quote) {
|
||||
if (c === '"')
|
||||
jsonLiteral += '\\';
|
||||
jsonLiteral += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
scanner.pos += match[0].length;
|
||||
rest = scanner.rest();
|
||||
if (match[0] === quote)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! match || match[0] !== quote)
|
||||
scanner.fatal("Unterminated string literal");
|
||||
|
||||
jsonLiteral += '"';
|
||||
var text = scanner.input.substring(startPos, scanner.pos);
|
||||
var value = JSON.parse(jsonLiteral);
|
||||
return { text: text, value: value };
|
||||
};
|
||||
1
packages/blaze/.gitignore
vendored
1
packages/blaze/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.build*
|
||||
@@ -1,318 +0,0 @@
|
||||
# Blaze
|
||||
|
||||
Blaze is a powerful library for creating user interfaces by writing
|
||||
reactive HTML templates. Compared to using a combination of
|
||||
traditional templates and jQuery, Blaze eliminates the need for all
|
||||
the "update logic" in your app that listens for data changes and
|
||||
manipulates the DOM. Instead, familiar template directives like
|
||||
|
||||
`{{#if}}` and `{{#each}}` integrate with
|
||||
[Tracker's](https://meteor.com/tracker) "transparent reactivity" and
|
||||
[Minimongo's](https://meteor.com/mini-databases) database cursors so
|
||||
that the DOM updates automatically.
|
||||
|
||||
Read more on the Blaze [project page](http://www.meteor.com/blaze).
|
||||
|
||||
## Details
|
||||
|
||||
Blaze has two major parts:
|
||||
|
||||
* A template compiler that compiles template files into JavaScript
|
||||
code that runs against the Blaze runtime library. Moreover, Blaze
|
||||
provides a compiler toolchain (think LLVM) that can be used to
|
||||
support arbitrary template syntaxes. The flagship template syntax
|
||||
is Spacebars, a variant of Handlebars, but a community alternative
|
||||
based on Jade is already in use by many apps.
|
||||
|
||||
* A reactive DOM engine that builds and manages the DOM at runtime,
|
||||
invoked via templates or directly from the app, which features
|
||||
reactively updating regions, lists, and attributes; event
|
||||
delegation; and many callbacks and hooks to aid the app developer.
|
||||
|
||||
Blaze is sometimes compared to frameworks like React, Angular, Ember,
|
||||
Polymer, Knockout, and others by virtue of its advanced templating
|
||||
system. What sets Blaze apart is a relentless focus on the developer
|
||||
experience, using templating, transparent reactivity, and
|
||||
interoperability with existing libraries to create a gentle learning
|
||||
curve while enabling you to build world-class apps.
|
||||
|
||||
## Examples
|
||||
|
||||
Here are two Spacebars templates from an example app called
|
||||
"Leaderboard" which displays a sorted list of top players and their
|
||||
scores:
|
||||
|
||||
```html
|
||||
<template name="leaderboard">
|
||||
<ol class="leaderboard">
|
||||
{{#each players}}
|
||||
{{> player}}
|
||||
{{/each}}
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
<template name="player">
|
||||
<li class="player {{selected}}">
|
||||
<span class="name">{{name}}</span>
|
||||
<span class="score">{{score}}</span>
|
||||
</li>
|
||||
</template>
|
||||
```
|
||||
|
||||
The template tags `{{name}}` and `{{score}}` refer to properties of
|
||||
the data context (the current player), while `players` and `selected`
|
||||
refer to helper functions. Helper functions and event handlers are defined
|
||||
in JavaScript:
|
||||
|
||||
```javascript
|
||||
Template.leaderboard.helpers({
|
||||
players: function () {
|
||||
// Perform a reactive database query against minimongo
|
||||
return Players.find({}, { sort: { score: -1, name: 1 } });
|
||||
}
|
||||
});
|
||||
|
||||
Template.player.events({
|
||||
'click': function () {
|
||||
// click on a player to select it
|
||||
Session.set("selectedPlayer", this._id);
|
||||
}
|
||||
});
|
||||
|
||||
Template.player.helpers({
|
||||
selected: function () {
|
||||
return Session.equals("selectedPlayer", this._id) ? "selected" : '';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
No additional UI code is necessary to ensure that the list of players
|
||||
stays up-to-date, or that the "selected" class is added and removed
|
||||
from the LI elements as appropriate when the user clicks on a player.
|
||||
|
||||
Thanks to a powerful template language, it doesn't take much ceremony
|
||||
to write a loop, include another template, or bind an attribute (or
|
||||
part of an attribute). And thanks to Tracker's transparent
|
||||
reactivity, there's no ceremony around depending on reactive data
|
||||
sources like the database or Session; it just happens when you read
|
||||
the value, and when the value changes, the DOM will be updated in a
|
||||
fine-grained way.
|
||||
|
||||
# Principles
|
||||
|
||||
## Gentle Learning Curve
|
||||
|
||||
To get started with Blaze, you don't have to learn a lot of concepts
|
||||
or terminology. As web developers, we are already students of HTML,
|
||||
CSS, and JavaScript, which are complex technologies described in thick
|
||||
books. Blaze lets you apply your existing knowledge in exciting new
|
||||
ways without having to read another book first.
|
||||
|
||||
Many factors go into making Blaze easy to pick up and use, including
|
||||
the other principles below. In general, we prefer APIs that lead to
|
||||
simple and obvious-looking application code, and we recognize that
|
||||
developers have limited time and energy to learn new and unfamiliar
|
||||
terms and syntaxes.
|
||||
|
||||
It may sound obvious to "keep it simple" and prioritize the developer
|
||||
experience when creating a system for reactive HTML, but it's also
|
||||
challenging, and we think it's not done often enough! We use feedback
|
||||
from the Meteor community to ensure that Blaze's features stay simple,
|
||||
understandable, and useful.
|
||||
|
||||
## Transparent Reactivity
|
||||
|
||||
Under the hood, Blaze uses the [Tracker](https://meteor.com/tracker)
|
||||
library to automatically keep track of when to recalculate each
|
||||
template helper. If a helper reads a value from the client-side
|
||||
database, for example, the helper will automatically be recalculated
|
||||
when the value changes.
|
||||
|
||||
What this means for the developer is simple. You don't have to
|
||||
explicitly declare when to update the DOM, or even perform any
|
||||
explicit "data-binding." You don't have to know how Tracker works, or
|
||||
even exactly what "reactivity" means, to benefit. The result is less
|
||||
thinking and less typing than other approaches.
|
||||
|
||||
## Clean Templates
|
||||
|
||||
Blaze embraces popular template syntaxes such as Handlebars and Jade
|
||||
which are clean, readable, and familiar to developers coming from
|
||||
other frameworks.
|
||||
|
||||
A good template language should clearly distinguish the special
|
||||
"template directives" (often enclosed in curly braces) from the HTML,
|
||||
and it should not obscure the structure of the resulting HTML. These
|
||||
properties make templating an easy concept to learn after static HTML
|
||||
(or alongside it), and make templates easy to read, easy to style with
|
||||
CSS, and easy to relate to the DOM.
|
||||
|
||||
In contrast, some newer frameworks try to remake templates as just
|
||||
HTML (Angular, Polymer) or replace them with just JavaScript (React).
|
||||
These approaches tend to obscure either the structure of the template,
|
||||
or what is a real DOM element and what is not, or both. In addition,
|
||||
since templates are generally precompiled anyway as a best practice,
|
||||
it's really not important that raw template source code be
|
||||
browser-parsable. Meanwhile, the developer experience of reading,
|
||||
writing, and maintaining templates is hugely important.
|
||||
|
||||
## Plugin Interoperability
|
||||
|
||||
Web developers often share snippets of HTML, JavaScript, and CSS, or
|
||||
publish them as libraries, widgets, or jQuery plugins. They want to
|
||||
embed videos, maps, and other third-party content.
|
||||
|
||||
Blaze doesn't assume it owns the whole DOM, and it tries to make as
|
||||
few assumptions as possible about the DOM outside of its updates.
|
||||
It hooks into jQuery's clean-up routines to prevent memory leaks,
|
||||
and it preserves classes, attributes, and styles added to elements
|
||||
by jQuery or any third-party library.
|
||||
|
||||
While it's certainly possible for Blaze and jQuery to step on each
|
||||
other's toes if you aren't careful, there are established patterns for
|
||||
keeping the peace, and Meteor users rightfully expect to be able to
|
||||
use the various widgets and enhancements cooked up by the broader web
|
||||
community in their apps.
|
||||
|
||||
# Comparisons to other libraries
|
||||
|
||||
Compared to Backbone and other libraries that simply re-render
|
||||
templates, Blaze does much less re-rendering and doesn't suffer from
|
||||
the dreaded "nested view" problem, which is when two templates can't
|
||||
be updated independently of each other because one is nested inside
|
||||
the other. In addition, Blaze automatically determines when
|
||||
re-rendering must occur, using Tracker.
|
||||
|
||||
Compared to Ember, Blaze offers finer-grained, automatic DOM updates.
|
||||
Because Blaze uses Tracker's transparent reactivity, you don't have to
|
||||
perform explicit "data-binding" to get data into your template, or
|
||||
declare the data dependencies of each template helper.
|
||||
|
||||
Compared to Angular and Polymer, Blaze has a gentler learning curve,
|
||||
simpler concepts, and nicer template syntax that cleanly separates
|
||||
template directives and HTML. Also, Blaze is targeted at today's
|
||||
browsers and not designed around a hypothetical "browser of the
|
||||
future."
|
||||
|
||||
Compared to React, Blaze emphasizes HTML templates rather than
|
||||
JavaScript component classes. Templates are more approachable than
|
||||
JavaScript code and easier to read, write, and style with CSS.
|
||||
Instead of using Tracker, React relies on a combination of explicit
|
||||
"setState" calls and data-model diffing in order to achieve efficient
|
||||
rendering.
|
||||
|
||||
# Future Work
|
||||
|
||||
### Components
|
||||
|
||||
Blaze will get better patterns for creating reusable UI components.
|
||||
Templates already serve as reusable components, to a point.
|
||||
Improvements will focus on:
|
||||
|
||||
* Argument-handling
|
||||
* Local reactive state
|
||||
* "Methods" that are callable from other components and have side
|
||||
effects, versus the current "helpers" which are called from the
|
||||
template language and are "pure"
|
||||
* Scoping and the lookup chain
|
||||
* Inheritance and configuration
|
||||
|
||||
### Forms
|
||||
|
||||
Most applications have a lot of forms, where input fields and other
|
||||
widgets are used to enter data, which must then be validated and
|
||||
turned into database changes. Server-side frameworks like Rails and
|
||||
Django have well-honed patterns for this, but client-side frameworks
|
||||
are typically more lacking, perhaps because they are more estranged
|
||||
from the database.
|
||||
|
||||
Meteor developers have already found ways and built packages to deal
|
||||
with forms and validation, but we think there's a great opportunity to
|
||||
make this part of the core, out-of-the-box Meteor experience.
|
||||
|
||||
### Mobile and Animation
|
||||
|
||||
Blaze will cater to the needs of the mobile web, including enhanced
|
||||
performance and patterns for touch and other mobile interaction.
|
||||
|
||||
We'll also improve the ease with which developers can integrate
|
||||
animated transitions into their apps.
|
||||
|
||||
### JavaScript Expressions in Templates
|
||||
|
||||
We plan to support JavaScript expressions in templates. This will
|
||||
make templates more expressive, and it will further shorten
|
||||
application code by eliminating the need for a certain class of
|
||||
one-line helpers.
|
||||
|
||||
The usual argument against allowing JavaScript expressions in a
|
||||
template language is one of "separation of concerns" -- separating
|
||||
business logic from presentation, so that the business logic may be
|
||||
better organized, maintained, and tested independently. Meanwhile,
|
||||
even "logicless" template languages often include some concessions in
|
||||
the form of microsyntax for filtering, querying, and transforming data
|
||||
before using it. This special syntax (and its extension mechanisms)
|
||||
must then be learned.
|
||||
|
||||
While keeping business logic out of templates is indeed good policy,
|
||||
there is a large class of "presentation logic" that is not really
|
||||
separable from the concerns of templates and HTML, such as the code to
|
||||
calculate styles and classes to apply to HTML elements or to massage
|
||||
data records into a better form for templating purposes. In many
|
||||
cases where this code is short, it may be more convenient or more
|
||||
readable to embed the code in the template, and it's certainly better
|
||||
than evolving the template syntax in a direction that diverges from
|
||||
JavaScript.
|
||||
|
||||
Because templates are already precompiled to JavaScript code, there is
|
||||
nothing fundamentally difficult or inelegant about allowing a large
|
||||
subset of JavaScript to be used within templates (see e.g. the project
|
||||
Ractive.js).
|
||||
|
||||
### Other Template Enhancements
|
||||
|
||||
Source maps for debugging templates. Imagine seeing your template
|
||||
code in the browser's debugger! Pretty slick.
|
||||
|
||||
True lexical scoping.
|
||||
|
||||
Better support for pluggable template syntax (e.g. Jade-like
|
||||
templates). There is already a Jade package in use, but we should
|
||||
learn from it and clarify the abstraction boundary that authors of
|
||||
template syntaxes are programming against.
|
||||
|
||||
### Pluggable Backends (don't require jQuery)
|
||||
|
||||
While Blaze currently requires jQuery, it is architected to run
|
||||
against other "DOM backends" using a common adaptor interface. You
|
||||
should be able to use Zepto, or some very small shim if browser
|
||||
compatibility is not a big deal for your application for some reason.
|
||||
At the moment, no such adaptors besides the jQuery one have been
|
||||
written.
|
||||
|
||||
The Blaze team experimented with dropping jQuery and talking directly
|
||||
to "modern browsers," but it turns out there is about 5-10K of code at
|
||||
the heart of jQuery that you can't throw out even if you don't care
|
||||
about old browsers or supporting jQuery's app-facing API, which is
|
||||
required just to bring browsers up to the modest expectations of web
|
||||
developers.
|
||||
|
||||
### Better Stand-alone Support
|
||||
|
||||
Blaze will get better support for using it outside of Meteor, such as
|
||||
regular stand-alone builds.
|
||||
|
||||
# Resources
|
||||
|
||||
* [Templates API](http://docs.meteor.com/#templates_api)
|
||||
* [Blaze API](http://docs.meteor.com/#blaze)
|
||||
* [Spacebars syntax](https://github.com/meteor/meteor/blob/devel/packages/spacebars/README.md)
|
||||
|
||||
# Packages
|
||||
|
||||
* blaze
|
||||
* blaze-tools
|
||||
* html-tools
|
||||
* htmljs
|
||||
* spacebars
|
||||
* spacebars-compiler
|
||||
@@ -1,365 +0,0 @@
|
||||
var jsUrlsAllowed = false;
|
||||
Blaze._allowJavascriptUrls = function () {
|
||||
jsUrlsAllowed = true;
|
||||
};
|
||||
Blaze._javascriptUrlsAllowed = function () {
|
||||
return jsUrlsAllowed;
|
||||
};
|
||||
|
||||
// An AttributeHandler object is responsible for updating a particular attribute
|
||||
// of a particular element. AttributeHandler subclasses implement
|
||||
// browser-specific logic for dealing with particular attributes across
|
||||
// different browsers.
|
||||
//
|
||||
// To define a new type of AttributeHandler, use
|
||||
// `var FooHandler = AttributeHandler.extend({ update: function ... })`
|
||||
// where the `update` function takes arguments `(element, oldValue, value)`.
|
||||
// The `element` argument is always the same between calls to `update` on
|
||||
// the same instance. `oldValue` and `value` are each either `null` or
|
||||
// a Unicode string of the type that might be passed to the value argument
|
||||
// of `setAttribute` (i.e. not an HTML string with character references).
|
||||
// When an AttributeHandler is installed, an initial call to `update` is
|
||||
// always made with `oldValue = null`. The `update` method can access
|
||||
// `this.name` if the AttributeHandler class is a generic one that applies
|
||||
// to multiple attribute names.
|
||||
//
|
||||
// AttributeHandlers can store custom properties on `this`, as long as they
|
||||
// don't use the names `element`, `name`, `value`, and `oldValue`.
|
||||
//
|
||||
// AttributeHandlers can't influence how attributes appear in rendered HTML,
|
||||
// only how they are updated after materialization as DOM.
|
||||
|
||||
AttributeHandler = function (name, value) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
};
|
||||
Blaze._AttributeHandler = AttributeHandler;
|
||||
|
||||
AttributeHandler.prototype.update = function (element, oldValue, value) {
|
||||
if (value === null) {
|
||||
if (oldValue !== null)
|
||||
element.removeAttribute(this.name);
|
||||
} else {
|
||||
element.setAttribute(this.name, value);
|
||||
}
|
||||
};
|
||||
|
||||
AttributeHandler.extend = function (options) {
|
||||
var curType = this;
|
||||
var subType = function AttributeHandlerSubtype(/*arguments*/) {
|
||||
AttributeHandler.apply(this, arguments);
|
||||
};
|
||||
subType.prototype = new curType;
|
||||
subType.extend = curType.extend;
|
||||
if (options)
|
||||
_.extend(subType.prototype, options);
|
||||
return subType;
|
||||
};
|
||||
|
||||
/// Apply the diff between the attributes of "oldValue" and "value" to "element."
|
||||
//
|
||||
// Each subclass must implement a parseValue method which takes a string
|
||||
// as an input and returns a dict of attributes. The keys of the dict
|
||||
// are unique identifiers (ie. css properties in the case of styles), and the
|
||||
// values are the entire attribute which will be injected into the element.
|
||||
//
|
||||
// Extended below to support classes, SVG elements and styles.
|
||||
|
||||
var DiffingAttributeHandler = AttributeHandler.extend({
|
||||
update: function (element, oldValue, value) {
|
||||
if (!this.getCurrentValue || !this.setValue || !this.parseValue)
|
||||
throw new Error("Missing methods in subclass of 'DiffingAttributeHandler'");
|
||||
|
||||
var oldAttrsMap = oldValue ? this.parseValue(oldValue) : {};
|
||||
var newAttrsMap = value ? this.parseValue(value) : {};
|
||||
|
||||
// the current attributes on the element, which we will mutate.
|
||||
|
||||
var attrString = this.getCurrentValue(element);
|
||||
var attrsMap = attrString ? this.parseValue(attrString) : {};
|
||||
|
||||
_.each(_.keys(oldAttrsMap), function (t) {
|
||||
if (! (t in newAttrsMap))
|
||||
delete attrsMap[t];
|
||||
});
|
||||
|
||||
_.each(_.keys(newAttrsMap), function (t) {
|
||||
attrsMap[t] = newAttrsMap[t];
|
||||
});
|
||||
|
||||
this.setValue(element, _.values(attrsMap).join(' '));
|
||||
}
|
||||
});
|
||||
|
||||
var ClassHandler = DiffingAttributeHandler.extend({
|
||||
// @param rawValue {String}
|
||||
getCurrentValue: function (element) {
|
||||
return element.className;
|
||||
},
|
||||
setValue: function (element, className) {
|
||||
element.className = className;
|
||||
},
|
||||
parseValue: function (attrString) {
|
||||
var tokens = {};
|
||||
|
||||
_.each(attrString.split(' '), function(token) {
|
||||
if (token)
|
||||
tokens[token] = token;
|
||||
});
|
||||
return tokens;
|
||||
}
|
||||
});
|
||||
|
||||
var SVGClassHandler = ClassHandler.extend({
|
||||
getCurrentValue: function (element) {
|
||||
return element.className.baseVal;
|
||||
},
|
||||
setValue: function (element, className) {
|
||||
element.setAttribute('class', className);
|
||||
}
|
||||
});
|
||||
|
||||
var StyleHandler = DiffingAttributeHandler.extend({
|
||||
getCurrentValue: function (element) {
|
||||
return element.getAttribute('style');
|
||||
},
|
||||
setValue: function (element, style) {
|
||||
if (style === '') {
|
||||
element.removeAttribute('style');
|
||||
} else {
|
||||
element.setAttribute('style', style);
|
||||
}
|
||||
},
|
||||
|
||||
// Parse a string to produce a map from property to attribute string.
|
||||
//
|
||||
// Example:
|
||||
// "color:red; foo:12px" produces a token {color: "color:red", foo:"foo:12px"}
|
||||
parseValue: function (attrString) {
|
||||
var tokens = {};
|
||||
|
||||
// Regex for parsing a css attribute declaration, taken from css-parse:
|
||||
// https://github.com/reworkcss/css-parse/blob/7cef3658d0bba872cde05a85339034b187cb3397/index.js#L219
|
||||
var regex = /(\*?[-#\/\*\\\w]+(?:\[[0-9a-z_-]+\])?)\s*:\s*(?:\'(?:\\\'|.)*?\'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+[;\s]*/g;
|
||||
var match = regex.exec(attrString);
|
||||
while (match) {
|
||||
// match[0] = entire matching string
|
||||
// match[1] = css property
|
||||
// Prefix the token to prevent conflicts with existing properties.
|
||||
|
||||
// XXX No `String.trim` on Safari 4. Swap out $.trim if we want to
|
||||
// remove strong dep on jquery.
|
||||
tokens[' ' + match[1]] = match[0].trim ?
|
||||
match[0].trim() : $.trim(match[0]);
|
||||
|
||||
match = regex.exec(attrString);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
});
|
||||
|
||||
var BooleanHandler = AttributeHandler.extend({
|
||||
update: function (element, oldValue, value) {
|
||||
var name = this.name;
|
||||
if (value == null) {
|
||||
if (oldValue != null)
|
||||
element[name] = false;
|
||||
} else {
|
||||
element[name] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var DOMPropertyHandler = AttributeHandler.extend({
|
||||
update: function (element, oldValue, value) {
|
||||
var name = this.name;
|
||||
if (value !== element[name])
|
||||
element[name] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// attributes of the type 'xlink:something' should be set using
|
||||
// the correct namespace in order to work
|
||||
var XlinkHandler = AttributeHandler.extend({
|
||||
update: function(element, oldValue, value) {
|
||||
var NS = 'http://www.w3.org/1999/xlink';
|
||||
if (value === null) {
|
||||
if (oldValue !== null)
|
||||
element.removeAttributeNS(NS, this.name);
|
||||
} else {
|
||||
element.setAttributeNS(NS, this.name, this.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// cross-browser version of `instanceof SVGElement`
|
||||
var isSVGElement = function (elem) {
|
||||
return 'ownerSVGElement' in elem;
|
||||
};
|
||||
|
||||
var isUrlAttribute = function (tagName, attrName) {
|
||||
// Compiled from http://www.w3.org/TR/REC-html40/index/attributes.html
|
||||
// and
|
||||
// http://www.w3.org/html/wg/drafts/html/master/index.html#attributes-1
|
||||
var urlAttrs = {
|
||||
FORM: ['action'],
|
||||
BODY: ['background'],
|
||||
BLOCKQUOTE: ['cite'],
|
||||
Q: ['cite'],
|
||||
DEL: ['cite'],
|
||||
INS: ['cite'],
|
||||
OBJECT: ['classid', 'codebase', 'data', 'usemap'],
|
||||
APPLET: ['codebase'],
|
||||
A: ['href'],
|
||||
AREA: ['href'],
|
||||
LINK: ['href'],
|
||||
BASE: ['href'],
|
||||
IMG: ['longdesc', 'src', 'usemap'],
|
||||
FRAME: ['longdesc', 'src'],
|
||||
IFRAME: ['longdesc', 'src'],
|
||||
HEAD: ['profile'],
|
||||
SCRIPT: ['src'],
|
||||
INPUT: ['src', 'usemap', 'formaction'],
|
||||
BUTTON: ['formaction'],
|
||||
BASE: ['href'],
|
||||
MENUITEM: ['icon'],
|
||||
HTML: ['manifest'],
|
||||
VIDEO: ['poster']
|
||||
};
|
||||
|
||||
if (attrName === 'itemid') {
|
||||
return true;
|
||||
}
|
||||
|
||||
var urlAttrNames = urlAttrs[tagName] || [];
|
||||
return _.contains(urlAttrNames, attrName);
|
||||
};
|
||||
|
||||
// To get the protocol for a URL, we let the browser normalize it for
|
||||
// us, by setting it as the href for an anchor tag and then reading out
|
||||
// the 'protocol' property.
|
||||
if (Meteor.isClient) {
|
||||
var anchorForNormalization = document.createElement('A');
|
||||
}
|
||||
|
||||
var getUrlProtocol = function (url) {
|
||||
if (Meteor.isClient) {
|
||||
anchorForNormalization.href = url;
|
||||
return (anchorForNormalization.protocol || "").toLowerCase();
|
||||
} else {
|
||||
throw new Error('getUrlProtocol not implemented on the server');
|
||||
}
|
||||
};
|
||||
|
||||
// UrlHandler is an attribute handler for all HTML attributes that take
|
||||
// URL values. It disallows javascript: URLs, unless
|
||||
// Blaze._allowJavascriptUrls() has been called. To detect javascript:
|
||||
// urls, we set the attribute on a dummy anchor element and then read
|
||||
// out the 'protocol' property of the attribute.
|
||||
var origUpdate = AttributeHandler.prototype.update;
|
||||
var UrlHandler = AttributeHandler.extend({
|
||||
update: function (element, oldValue, value) {
|
||||
var self = this;
|
||||
var args = arguments;
|
||||
|
||||
if (Blaze._javascriptUrlsAllowed()) {
|
||||
origUpdate.apply(self, args);
|
||||
} else {
|
||||
var isJavascriptProtocol = (getUrlProtocol(value) === "javascript:");
|
||||
var isVBScriptProtocol = (getUrlProtocol(value) === "vbscript:");
|
||||
if (isJavascriptProtocol || isVBScriptProtocol) {
|
||||
Blaze._warn("URLs that use the 'javascript:' or 'vbscript:' protocol are not " +
|
||||
"allowed in URL attribute values. " +
|
||||
"Call Blaze._allowJavascriptUrls() " +
|
||||
"to enable them.");
|
||||
origUpdate.apply(self, [element, oldValue, null]);
|
||||
} else {
|
||||
origUpdate.apply(self, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// XXX make it possible for users to register attribute handlers!
|
||||
makeAttributeHandler = function (elem, name, value) {
|
||||
// generally, use setAttribute but certain attributes need to be set
|
||||
// by directly setting a JavaScript property on the DOM element.
|
||||
if (name === 'class') {
|
||||
if (isSVGElement(elem)) {
|
||||
return new SVGClassHandler(name, value);
|
||||
} else {
|
||||
return new ClassHandler(name, value);
|
||||
}
|
||||
} else if (name === 'style') {
|
||||
return new StyleHandler(name, value);
|
||||
} else if ((elem.tagName === 'OPTION' && name === 'selected') ||
|
||||
(elem.tagName === 'INPUT' && name === 'checked')) {
|
||||
return new BooleanHandler(name, value);
|
||||
} else if ((elem.tagName === 'TEXTAREA' || elem.tagName === 'INPUT')
|
||||
&& name === 'value') {
|
||||
// internally, TEXTAREAs tracks their value in the 'value'
|
||||
// attribute just like INPUTs.
|
||||
return new DOMPropertyHandler(name, value);
|
||||
} else if (name.substring(0,6) === 'xlink:') {
|
||||
return new XlinkHandler(name.substring(6), value);
|
||||
} else if (isUrlAttribute(elem.tagName, name)) {
|
||||
return new UrlHandler(name, value);
|
||||
} else {
|
||||
return new AttributeHandler(name, value);
|
||||
}
|
||||
|
||||
// XXX will need one for 'style' on IE, though modern browsers
|
||||
// seem to handle setAttribute ok.
|
||||
};
|
||||
|
||||
|
||||
ElementAttributesUpdater = function (elem) {
|
||||
this.elem = elem;
|
||||
this.handlers = {};
|
||||
};
|
||||
|
||||
// Update attributes on `elem` to the dictionary `attrs`, whose
|
||||
// values are strings.
|
||||
ElementAttributesUpdater.prototype.update = function(newAttrs) {
|
||||
var elem = this.elem;
|
||||
var handlers = this.handlers;
|
||||
|
||||
for (var k in handlers) {
|
||||
if (! _.has(newAttrs, k)) {
|
||||
// remove attributes (and handlers) for attribute names
|
||||
// that don't exist as keys of `newAttrs` and so won't
|
||||
// be visited when traversing it. (Attributes that
|
||||
// exist in the `newAttrs` object but are `null`
|
||||
// are handled later.)
|
||||
var handler = handlers[k];
|
||||
var oldValue = handler.value;
|
||||
handler.value = null;
|
||||
handler.update(elem, oldValue, null);
|
||||
delete handlers[k];
|
||||
}
|
||||
}
|
||||
|
||||
for (var k in newAttrs) {
|
||||
var handler = null;
|
||||
var oldValue;
|
||||
var value = newAttrs[k];
|
||||
if (! _.has(handlers, k)) {
|
||||
if (value !== null) {
|
||||
// make new handler
|
||||
handler = makeAttributeHandler(elem, k, value);
|
||||
handlers[k] = handler;
|
||||
oldValue = null;
|
||||
}
|
||||
} else {
|
||||
handler = handlers[k];
|
||||
oldValue = handler.value;
|
||||
}
|
||||
if (oldValue !== value) {
|
||||
handler.value = value;
|
||||
handler.update(elem, oldValue, value);
|
||||
if (value === null)
|
||||
delete handlers[k];
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
UI = Blaze;
|
||||
|
||||
Blaze.ReactiveVar = ReactiveVar;
|
||||
UI._templateInstance = Blaze.Template.instance;
|
||||
|
||||
Handlebars = {};
|
||||
Handlebars.registerHelper = Blaze.registerHelper;
|
||||
|
||||
Handlebars._escape = Blaze._escape;
|
||||
|
||||
// Return these from {{...}} helpers to achieve the same as returning
|
||||
// strings from {{{...}}} helpers
|
||||
Handlebars.SafeString = function(string) {
|
||||
this.string = string;
|
||||
};
|
||||
Handlebars.SafeString.prototype.toString = function() {
|
||||
return this.string.toString();
|
||||
};
|
||||
@@ -1,354 +0,0 @@
|
||||
Blaze._calculateCondition = function (cond) {
|
||||
if (cond instanceof Array && cond.length === 0)
|
||||
cond = false;
|
||||
return !! cond;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Constructs a View that renders content with a data context.
|
||||
* @locus Client
|
||||
* @param {Object|Function} data An object to use as the data context, or a function returning such an object. If a function is provided, it will be reactively re-run.
|
||||
* @param {Function} contentFunc A Function that returns [*renderable content*](#renderable_content).
|
||||
*/
|
||||
Blaze.With = function (data, contentFunc) {
|
||||
var view = Blaze.View('with', contentFunc);
|
||||
|
||||
view.dataVar = new ReactiveVar;
|
||||
|
||||
view.onViewCreated(function () {
|
||||
if (typeof data === 'function') {
|
||||
// `data` is a reactive function
|
||||
view.autorun(function () {
|
||||
view.dataVar.set(data());
|
||||
}, view.parentView, 'setData');
|
||||
} else {
|
||||
view.dataVar.set(data);
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches bindings to the instantiated view.
|
||||
* @param {Object} bindings A dictionary of bindings, each binding name
|
||||
* corresponds to a value or a function that will be reactively re-run.
|
||||
* @param {View} view The target.
|
||||
*/
|
||||
Blaze._attachBindingsToView = function (bindings, view) {
|
||||
view.onViewCreated(function () {
|
||||
_.each(bindings, function (binding, name) {
|
||||
view._scopeBindings[name] = new ReactiveVar;
|
||||
if (typeof binding === 'function') {
|
||||
view.autorun(function () {
|
||||
view._scopeBindings[name].set(binding());
|
||||
}, view.parentView);
|
||||
} else {
|
||||
view._scopeBindings[name].set(binding);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Constructs a View setting the local lexical scope in the block.
|
||||
* @param {Function} bindings Dictionary mapping names of bindings to
|
||||
* values or computations to reactively re-run.
|
||||
* @param {Function} contentFunc A Function that returns [*renderable content*](#renderable_content).
|
||||
*/
|
||||
Blaze.Let = function (bindings, contentFunc) {
|
||||
var view = Blaze.View('let', contentFunc);
|
||||
Blaze._attachBindingsToView(bindings, view);
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Constructs a View that renders content conditionally.
|
||||
* @locus Client
|
||||
* @param {Function} conditionFunc A function to reactively re-run. Whether the result is truthy or falsy determines whether `contentFunc` or `elseFunc` is shown. An empty array is considered falsy.
|
||||
* @param {Function} contentFunc A Function that returns [*renderable content*](#renderable_content).
|
||||
* @param {Function} [elseFunc] Optional. A Function that returns [*renderable content*](#renderable_content). If no `elseFunc` is supplied, no content is shown in the "else" case.
|
||||
*/
|
||||
Blaze.If = function (conditionFunc, contentFunc, elseFunc, _not) {
|
||||
var conditionVar = new ReactiveVar;
|
||||
|
||||
var view = Blaze.View(_not ? 'unless' : 'if', function () {
|
||||
return conditionVar.get() ? contentFunc() :
|
||||
(elseFunc ? elseFunc() : null);
|
||||
});
|
||||
view.__conditionVar = conditionVar;
|
||||
view.onViewCreated(function () {
|
||||
this.autorun(function () {
|
||||
var cond = Blaze._calculateCondition(conditionFunc());
|
||||
conditionVar.set(_not ? (! cond) : cond);
|
||||
}, this.parentView, 'condition');
|
||||
});
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary An inverted [`Blaze.If`](#blaze_if).
|
||||
* @locus Client
|
||||
* @param {Function} conditionFunc A function to reactively re-run. If the result is falsy, `contentFunc` is shown, otherwise `elseFunc` is shown. An empty array is considered falsy.
|
||||
* @param {Function} contentFunc A Function that returns [*renderable content*](#renderable_content).
|
||||
* @param {Function} [elseFunc] Optional. A Function that returns [*renderable content*](#renderable_content). If no `elseFunc` is supplied, no content is shown in the "else" case.
|
||||
*/
|
||||
Blaze.Unless = function (conditionFunc, contentFunc, elseFunc) {
|
||||
return Blaze.If(conditionFunc, contentFunc, elseFunc, true /*_not*/);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Constructs a View that renders `contentFunc` for each item in a sequence.
|
||||
* @locus Client
|
||||
* @param {Function} argFunc A function to reactively re-run. The function can
|
||||
* return one of two options:
|
||||
*
|
||||
* 1. An object with two fields: '_variable' and '_sequence'. Each iterates over
|
||||
* '_sequence', it may be a Cursor, an array, null, or undefined. Inside the
|
||||
* Each body you will be able to get the current item from the sequence using
|
||||
* the name specified in the '_variable' field.
|
||||
*
|
||||
* 2. Just a sequence (Cursor, array, null, or undefined) not wrapped into an
|
||||
* object. Inside the Each body, the current item will be set as the data
|
||||
* context.
|
||||
* @param {Function} contentFunc A Function that returns [*renderable
|
||||
* content*](#renderable_content).
|
||||
* @param {Function} [elseFunc] A Function that returns [*renderable
|
||||
* content*](#renderable_content) to display in the case when there are no items
|
||||
* in the sequence.
|
||||
*/
|
||||
Blaze.Each = function (argFunc, contentFunc, elseFunc) {
|
||||
var eachView = Blaze.View('each', function () {
|
||||
var subviews = this.initialSubviews;
|
||||
this.initialSubviews = null;
|
||||
if (this._isCreatedForExpansion) {
|
||||
this.expandedValueDep = new Tracker.Dependency;
|
||||
this.expandedValueDep.depend();
|
||||
}
|
||||
return subviews;
|
||||
});
|
||||
eachView.initialSubviews = [];
|
||||
eachView.numItems = 0;
|
||||
eachView.inElseMode = false;
|
||||
eachView.stopHandle = null;
|
||||
eachView.contentFunc = contentFunc;
|
||||
eachView.elseFunc = elseFunc;
|
||||
eachView.argVar = new ReactiveVar;
|
||||
eachView.variableName = null;
|
||||
|
||||
// update the @index value in the scope of all subviews in the range
|
||||
var updateIndices = function (from, to) {
|
||||
if (to === undefined) {
|
||||
to = eachView.numItems - 1;
|
||||
}
|
||||
|
||||
for (var i = from; i <= to; i++) {
|
||||
var view = eachView._domrange.members[i].view;
|
||||
view._scopeBindings['@index'].set(i);
|
||||
}
|
||||
};
|
||||
|
||||
eachView.onViewCreated(function () {
|
||||
// We evaluate argFunc in an autorun to make sure
|
||||
// Blaze.currentView is always set when it runs (rather than
|
||||
// passing argFunc straight to ObserveSequence).
|
||||
eachView.autorun(function () {
|
||||
// argFunc can return either a sequence as is or a wrapper object with a
|
||||
// _sequence and _variable fields set.
|
||||
var arg = argFunc();
|
||||
if (_.isObject(arg) && _.has(arg, '_sequence')) {
|
||||
eachView.variableName = arg._variable || null;
|
||||
arg = arg._sequence;
|
||||
}
|
||||
|
||||
eachView.argVar.set(arg);
|
||||
}, eachView.parentView, 'collection');
|
||||
|
||||
eachView.stopHandle = ObserveSequence.observe(function () {
|
||||
return eachView.argVar.get();
|
||||
}, {
|
||||
addedAt: function (id, item, index) {
|
||||
Tracker.nonreactive(function () {
|
||||
var newItemView;
|
||||
if (eachView.variableName) {
|
||||
// new-style #each (as in {{#each item in items}})
|
||||
// doesn't create a new data context
|
||||
newItemView = Blaze.View('item', eachView.contentFunc);
|
||||
} else {
|
||||
newItemView = Blaze.With(item, eachView.contentFunc);
|
||||
}
|
||||
|
||||
eachView.numItems++;
|
||||
|
||||
var bindings = {};
|
||||
bindings['@index'] = index;
|
||||
if (eachView.variableName) {
|
||||
bindings[eachView.variableName] = item;
|
||||
}
|
||||
Blaze._attachBindingsToView(bindings, newItemView);
|
||||
|
||||
if (eachView.expandedValueDep) {
|
||||
eachView.expandedValueDep.changed();
|
||||
} else if (eachView._domrange) {
|
||||
if (eachView.inElseMode) {
|
||||
eachView._domrange.removeMember(0);
|
||||
eachView.inElseMode = false;
|
||||
}
|
||||
|
||||
var range = Blaze._materializeView(newItemView, eachView);
|
||||
eachView._domrange.addMember(range, index);
|
||||
updateIndices(index);
|
||||
} else {
|
||||
eachView.initialSubviews.splice(index, 0, newItemView);
|
||||
}
|
||||
});
|
||||
},
|
||||
removedAt: function (id, item, index) {
|
||||
Tracker.nonreactive(function () {
|
||||
eachView.numItems--;
|
||||
if (eachView.expandedValueDep) {
|
||||
eachView.expandedValueDep.changed();
|
||||
} else if (eachView._domrange) {
|
||||
eachView._domrange.removeMember(index);
|
||||
updateIndices(index);
|
||||
if (eachView.elseFunc && eachView.numItems === 0) {
|
||||
eachView.inElseMode = true;
|
||||
eachView._domrange.addMember(
|
||||
Blaze._materializeView(
|
||||
Blaze.View('each_else',eachView.elseFunc),
|
||||
eachView), 0);
|
||||
}
|
||||
} else {
|
||||
eachView.initialSubviews.splice(index, 1);
|
||||
}
|
||||
});
|
||||
},
|
||||
changedAt: function (id, newItem, oldItem, index) {
|
||||
Tracker.nonreactive(function () {
|
||||
if (eachView.expandedValueDep) {
|
||||
eachView.expandedValueDep.changed();
|
||||
} else {
|
||||
var itemView;
|
||||
if (eachView._domrange) {
|
||||
itemView = eachView._domrange.getMember(index).view;
|
||||
} else {
|
||||
itemView = eachView.initialSubviews[index];
|
||||
}
|
||||
if (eachView.variableName) {
|
||||
itemView._scopeBindings[eachView.variableName].set(newItem);
|
||||
} else {
|
||||
itemView.dataVar.set(newItem);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
movedTo: function (id, item, fromIndex, toIndex) {
|
||||
Tracker.nonreactive(function () {
|
||||
if (eachView.expandedValueDep) {
|
||||
eachView.expandedValueDep.changed();
|
||||
} else if (eachView._domrange) {
|
||||
eachView._domrange.moveMember(fromIndex, toIndex);
|
||||
updateIndices(
|
||||
Math.min(fromIndex, toIndex), Math.max(fromIndex, toIndex));
|
||||
} else {
|
||||
var subviews = eachView.initialSubviews;
|
||||
var itemView = subviews[fromIndex];
|
||||
subviews.splice(fromIndex, 1);
|
||||
subviews.splice(toIndex, 0, itemView);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (eachView.elseFunc && eachView.numItems === 0) {
|
||||
eachView.inElseMode = true;
|
||||
eachView.initialSubviews[0] =
|
||||
Blaze.View('each_else', eachView.elseFunc);
|
||||
}
|
||||
});
|
||||
|
||||
eachView.onViewDestroyed(function () {
|
||||
if (eachView.stopHandle)
|
||||
eachView.stopHandle.stop();
|
||||
});
|
||||
|
||||
return eachView;
|
||||
};
|
||||
|
||||
Blaze._TemplateWith = function (arg, contentFunc) {
|
||||
var w;
|
||||
|
||||
var argFunc = arg;
|
||||
if (typeof arg !== 'function') {
|
||||
argFunc = function () {
|
||||
return arg;
|
||||
};
|
||||
}
|
||||
|
||||
// This is a little messy. When we compile `{{> Template.contentBlock}}`, we
|
||||
// wrap it in Blaze._InOuterTemplateScope in order to skip the intermediate
|
||||
// parent Views in the current template. However, when there's an argument
|
||||
// (`{{> Template.contentBlock arg}}`), the argument needs to be evaluated
|
||||
// in the original scope. There's no good order to nest
|
||||
// Blaze._InOuterTemplateScope and Spacebars.TemplateWith to achieve this,
|
||||
// so we wrap argFunc to run it in the "original parentView" of the
|
||||
// Blaze._InOuterTemplateScope.
|
||||
//
|
||||
// To make this better, reconsider _InOuterTemplateScope as a primitive.
|
||||
// Longer term, evaluate expressions in the proper lexical scope.
|
||||
var wrappedArgFunc = function () {
|
||||
var viewToEvaluateArg = null;
|
||||
if (w.parentView && w.parentView.name === 'InOuterTemplateScope') {
|
||||
viewToEvaluateArg = w.parentView.originalParentView;
|
||||
}
|
||||
if (viewToEvaluateArg) {
|
||||
return Blaze._withCurrentView(viewToEvaluateArg, argFunc);
|
||||
} else {
|
||||
return argFunc();
|
||||
}
|
||||
};
|
||||
|
||||
var wrappedContentFunc = function () {
|
||||
var content = contentFunc.call(this);
|
||||
|
||||
// Since we are generating the Blaze._TemplateWith view for the
|
||||
// user, set the flag on the child view. If `content` is a template,
|
||||
// construct the View so that we can set the flag.
|
||||
if (content instanceof Blaze.Template) {
|
||||
content = content.constructView();
|
||||
}
|
||||
if (content instanceof Blaze.View) {
|
||||
content._hasGeneratedParent = true;
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
w = Blaze.With(wrappedArgFunc, wrappedContentFunc);
|
||||
w.__isTemplateWith = true;
|
||||
return w;
|
||||
};
|
||||
|
||||
Blaze._InOuterTemplateScope = function (templateView, contentFunc) {
|
||||
var view = Blaze.View('InOuterTemplateScope', contentFunc);
|
||||
var parentView = templateView.parentView;
|
||||
|
||||
// Hack so that if you call `{{> foo bar}}` and it expands into
|
||||
// `{{#with bar}}{{> foo}}{{/with}}`, and then `foo` is a template
|
||||
// that inserts `{{> Template.contentBlock}}`, the data context for
|
||||
// `Template.contentBlock` is not `bar` but the one enclosing that.
|
||||
if (parentView.__isTemplateWith)
|
||||
parentView = parentView.parentView;
|
||||
|
||||
view.onViewCreated(function () {
|
||||
this.originalParentView = this.parentView;
|
||||
this.parentView = parentView;
|
||||
this.__childDoesntStartNewLexicalScope = true;
|
||||
});
|
||||
return view;
|
||||
};
|
||||
|
||||
// XXX COMPAT WITH 0.9.0
|
||||
Blaze.InOuterTemplateScope = Blaze._InOuterTemplateScope;
|
||||
@@ -1,179 +0,0 @@
|
||||
var DOMBackend = {};
|
||||
Blaze._DOMBackend = DOMBackend;
|
||||
|
||||
var $jq = (typeof jQuery !== 'undefined' ? jQuery :
|
||||
(typeof Package !== 'undefined' ?
|
||||
Package.jquery && Package.jquery.jQuery : null));
|
||||
if (! $jq)
|
||||
throw new Error("jQuery not found");
|
||||
|
||||
DOMBackend._$jq = $jq;
|
||||
|
||||
DOMBackend.parseHTML = function (html) {
|
||||
// Return an array of nodes.
|
||||
//
|
||||
// jQuery does fancy stuff like creating an appropriate
|
||||
// container element and setting innerHTML on it, as well
|
||||
// as working around various IE quirks.
|
||||
return $jq.parseHTML(html) || [];
|
||||
};
|
||||
|
||||
DOMBackend.Events = {
|
||||
// `selector` is non-null. `type` is one type (but
|
||||
// may be in backend-specific form, e.g. have namespaces).
|
||||
// Order fired must be order bound.
|
||||
delegateEvents: function (elem, type, selector, handler) {
|
||||
$jq(elem).on(type, selector, handler);
|
||||
},
|
||||
|
||||
undelegateEvents: function (elem, type, handler) {
|
||||
$jq(elem).off(type, '**', handler);
|
||||
},
|
||||
|
||||
bindEventCapturer: function (elem, type, selector, handler) {
|
||||
var $elem = $jq(elem);
|
||||
|
||||
var wrapper = function (event) {
|
||||
event = $jq.event.fix(event);
|
||||
event.currentTarget = event.target;
|
||||
|
||||
// Note: It might improve jQuery interop if we called into jQuery
|
||||
// here somehow. Since we don't use jQuery to dispatch the event,
|
||||
// we don't fire any of jQuery's event hooks or anything. However,
|
||||
// since jQuery can't bind capturing handlers, it's not clear
|
||||
// where we would hook in. Internal jQuery functions like `dispatch`
|
||||
// are too high-level.
|
||||
var $target = $jq(event.currentTarget);
|
||||
if ($target.is($elem.find(selector)))
|
||||
handler.call(elem, event);
|
||||
};
|
||||
|
||||
handler._meteorui_wrapper = wrapper;
|
||||
|
||||
type = DOMBackend.Events.parseEventType(type);
|
||||
// add *capturing* event listener
|
||||
elem.addEventListener(type, wrapper, true);
|
||||
},
|
||||
|
||||
unbindEventCapturer: function (elem, type, handler) {
|
||||
type = DOMBackend.Events.parseEventType(type);
|
||||
elem.removeEventListener(type, handler._meteorui_wrapper, true);
|
||||
},
|
||||
|
||||
parseEventType: function (type) {
|
||||
// strip off namespaces
|
||||
var dotLoc = type.indexOf('.');
|
||||
if (dotLoc >= 0)
|
||||
return type.slice(0, dotLoc);
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
///// Removal detection and interoperability.
|
||||
|
||||
// For an explanation of this technique, see:
|
||||
// http://bugs.jquery.com/ticket/12213#comment:23 .
|
||||
//
|
||||
// In short, an element is considered "removed" when jQuery
|
||||
// cleans up its *private* userdata on the element,
|
||||
// which we can detect using a custom event with a teardown
|
||||
// hook.
|
||||
|
||||
var NOOP = function () {};
|
||||
|
||||
// Circular doubly-linked list
|
||||
var TeardownCallback = function (func) {
|
||||
this.next = this;
|
||||
this.prev = this;
|
||||
this.func = func;
|
||||
};
|
||||
|
||||
// Insert newElt before oldElt in the circular list
|
||||
TeardownCallback.prototype.linkBefore = function(oldElt) {
|
||||
this.prev = oldElt.prev;
|
||||
this.next = oldElt;
|
||||
oldElt.prev.next = this;
|
||||
oldElt.prev = this;
|
||||
};
|
||||
|
||||
TeardownCallback.prototype.unlink = function () {
|
||||
this.prev.next = this.next;
|
||||
this.next.prev = this.prev;
|
||||
};
|
||||
|
||||
TeardownCallback.prototype.go = function () {
|
||||
var func = this.func;
|
||||
func && func();
|
||||
};
|
||||
|
||||
TeardownCallback.prototype.stop = TeardownCallback.prototype.unlink;
|
||||
|
||||
DOMBackend.Teardown = {
|
||||
_JQUERY_EVENT_NAME: 'blaze_teardown_watcher',
|
||||
_CB_PROP: '$blaze_teardown_callbacks',
|
||||
// Registers a callback function to be called when the given element or
|
||||
// one of its ancestors is removed from the DOM via the backend library.
|
||||
// The callback function is called at most once, and it receives the element
|
||||
// in question as an argument.
|
||||
onElementTeardown: function (elem, func) {
|
||||
var elt = new TeardownCallback(func);
|
||||
|
||||
var propName = DOMBackend.Teardown._CB_PROP;
|
||||
if (! elem[propName]) {
|
||||
// create an empty node that is never unlinked
|
||||
elem[propName] = new TeardownCallback;
|
||||
|
||||
// Set up the event, only the first time.
|
||||
$jq(elem).on(DOMBackend.Teardown._JQUERY_EVENT_NAME, NOOP);
|
||||
}
|
||||
|
||||
elt.linkBefore(elem[propName]);
|
||||
|
||||
return elt; // so caller can call stop()
|
||||
},
|
||||
// Recursively call all teardown hooks, in the backend and registered
|
||||
// through DOMBackend.onElementTeardown.
|
||||
tearDownElement: function (elem) {
|
||||
var elems = [];
|
||||
// Array.prototype.slice.call doesn't work when given a NodeList in
|
||||
// IE8 ("JScript object expected").
|
||||
var nodeList = elem.getElementsByTagName('*');
|
||||
for (var i = 0; i < nodeList.length; i++) {
|
||||
elems.push(nodeList[i]);
|
||||
}
|
||||
elems.push(elem);
|
||||
$jq.cleanData(elems);
|
||||
}
|
||||
};
|
||||
|
||||
$jq.event.special[DOMBackend.Teardown._JQUERY_EVENT_NAME] = {
|
||||
setup: function () {
|
||||
// This "setup" callback is important even though it is empty!
|
||||
// Without it, jQuery will call addEventListener, which is a
|
||||
// performance hit, especially with Chrome's async stack trace
|
||||
// feature enabled.
|
||||
},
|
||||
teardown: function() {
|
||||
var elem = this;
|
||||
var callbacks = elem[DOMBackend.Teardown._CB_PROP];
|
||||
if (callbacks) {
|
||||
var elt = callbacks.next;
|
||||
while (elt !== callbacks) {
|
||||
elt.go();
|
||||
elt = elt.next;
|
||||
}
|
||||
callbacks.go();
|
||||
|
||||
elem[DOMBackend.Teardown._CB_PROP] = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Must use jQuery semantics for `context`, not
|
||||
// querySelectorAll's. In other words, all the parts
|
||||
// of `selector` must be found under `context`.
|
||||
DOMBackend.findBySelector = function (selector, context) {
|
||||
return $jq(selector, context);
|
||||
};
|
||||
@@ -1,485 +0,0 @@
|
||||
|
||||
// A constant empty array (frozen if the JS engine supports it).
|
||||
var _emptyArray = Object.freeze ? Object.freeze([]) : [];
|
||||
|
||||
// `[new] Blaze._DOMRange([nodeAndRangeArray])`
|
||||
//
|
||||
// A DOMRange consists of an array of consecutive nodes and DOMRanges,
|
||||
// which may be replaced at any time with a new array. If the DOMRange
|
||||
// has been attached to the DOM at some location, then updating
|
||||
// the array will cause the DOM to be updated at that location.
|
||||
Blaze._DOMRange = function (nodeAndRangeArray) {
|
||||
if (! (this instanceof DOMRange))
|
||||
// called without `new`
|
||||
return new DOMRange(nodeAndRangeArray);
|
||||
|
||||
var members = (nodeAndRangeArray || _emptyArray);
|
||||
if (! (members && (typeof members.length) === 'number'))
|
||||
throw new Error("Expected array");
|
||||
|
||||
for (var i = 0; i < members.length; i++)
|
||||
this._memberIn(members[i]);
|
||||
|
||||
this.members = members;
|
||||
this.emptyRangePlaceholder = null;
|
||||
this.attached = false;
|
||||
this.parentElement = null;
|
||||
this.parentRange = null;
|
||||
this.attachedCallbacks = _emptyArray;
|
||||
};
|
||||
var DOMRange = Blaze._DOMRange;
|
||||
|
||||
// In IE 8, don't use empty text nodes as placeholders
|
||||
// in empty DOMRanges, use comment nodes instead. Using
|
||||
// empty text nodes in modern browsers is great because
|
||||
// it doesn't clutter the web inspector. In IE 8, however,
|
||||
// it seems to lead in some roundabout way to the OAuth
|
||||
// pop-up crashing the browser completely. In the past,
|
||||
// we didn't use empty text nodes on IE 8 because they
|
||||
// don't accept JS properties, so just use the same logic
|
||||
// even though we don't need to set properties on the
|
||||
// placeholder anymore.
|
||||
DOMRange._USE_COMMENT_PLACEHOLDERS = (function () {
|
||||
var result = false;
|
||||
var textNode = document.createTextNode("");
|
||||
try {
|
||||
textNode.someProp = true;
|
||||
} catch (e) {
|
||||
// IE 8
|
||||
result = true;
|
||||
}
|
||||
return result;
|
||||
})();
|
||||
|
||||
// static methods
|
||||
DOMRange._insert = function (rangeOrNode, parentElement, nextNode, _isMove) {
|
||||
var m = rangeOrNode;
|
||||
if (m instanceof DOMRange) {
|
||||
m.attach(parentElement, nextNode, _isMove);
|
||||
} else {
|
||||
if (_isMove)
|
||||
DOMRange._moveNodeWithHooks(m, parentElement, nextNode);
|
||||
else
|
||||
DOMRange._insertNodeWithHooks(m, parentElement, nextNode);
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange._remove = function (rangeOrNode) {
|
||||
var m = rangeOrNode;
|
||||
if (m instanceof DOMRange) {
|
||||
m.detach();
|
||||
} else {
|
||||
DOMRange._removeNodeWithHooks(m);
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange._removeNodeWithHooks = function (n) {
|
||||
if (! n.parentNode)
|
||||
return;
|
||||
if (n.nodeType === 1 &&
|
||||
n.parentNode._uihooks && n.parentNode._uihooks.removeElement) {
|
||||
n.parentNode._uihooks.removeElement(n);
|
||||
} else {
|
||||
n.parentNode.removeChild(n);
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange._insertNodeWithHooks = function (n, parent, next) {
|
||||
// `|| null` because IE throws an error if 'next' is undefined
|
||||
next = next || null;
|
||||
if (n.nodeType === 1 &&
|
||||
parent._uihooks && parent._uihooks.insertElement) {
|
||||
parent._uihooks.insertElement(n, next);
|
||||
} else {
|
||||
parent.insertBefore(n, next);
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange._moveNodeWithHooks = function (n, parent, next) {
|
||||
if (n.parentNode !== parent)
|
||||
return;
|
||||
// `|| null` because IE throws an error if 'next' is undefined
|
||||
next = next || null;
|
||||
if (n.nodeType === 1 &&
|
||||
parent._uihooks && parent._uihooks.moveElement) {
|
||||
parent._uihooks.moveElement(n, next);
|
||||
} else {
|
||||
parent.insertBefore(n, next);
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange.forElement = function (elem) {
|
||||
if (elem.nodeType !== 1)
|
||||
throw new Error("Expected element, found: " + elem);
|
||||
var range = null;
|
||||
while (elem && ! range) {
|
||||
range = (elem.$blaze_range || null);
|
||||
if (! range)
|
||||
elem = elem.parentNode;
|
||||
}
|
||||
return range;
|
||||
};
|
||||
|
||||
DOMRange.prototype.attach = function (parentElement, nextNode, _isMove, _isReplace) {
|
||||
// This method is called to insert the DOMRange into the DOM for
|
||||
// the first time, but it's also used internally when
|
||||
// updating the DOM.
|
||||
//
|
||||
// If _isMove is true, move this attached range to a different
|
||||
// location under the same parentElement.
|
||||
if (_isMove || _isReplace) {
|
||||
if (! (this.parentElement === parentElement &&
|
||||
this.attached))
|
||||
throw new Error("Can only move or replace an attached DOMRange, and only under the same parent element");
|
||||
}
|
||||
|
||||
var members = this.members;
|
||||
if (members.length) {
|
||||
this.emptyRangePlaceholder = null;
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
DOMRange._insert(members[i], parentElement, nextNode, _isMove);
|
||||
}
|
||||
} else {
|
||||
var placeholder = (
|
||||
DOMRange._USE_COMMENT_PLACEHOLDERS ?
|
||||
document.createComment("") :
|
||||
document.createTextNode(""));
|
||||
this.emptyRangePlaceholder = placeholder;
|
||||
parentElement.insertBefore(placeholder, nextNode || null);
|
||||
}
|
||||
this.attached = true;
|
||||
this.parentElement = parentElement;
|
||||
|
||||
if (! (_isMove || _isReplace)) {
|
||||
for(var i = 0; i < this.attachedCallbacks.length; i++) {
|
||||
var obj = this.attachedCallbacks[i];
|
||||
obj.attached && obj.attached(this, parentElement);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange.prototype.setMembers = function (newNodeAndRangeArray) {
|
||||
var newMembers = newNodeAndRangeArray;
|
||||
if (! (newMembers && (typeof newMembers.length) === 'number'))
|
||||
throw new Error("Expected array");
|
||||
|
||||
var oldMembers = this.members;
|
||||
|
||||
for (var i = 0; i < oldMembers.length; i++)
|
||||
this._memberOut(oldMembers[i]);
|
||||
for (var i = 0; i < newMembers.length; i++)
|
||||
this._memberIn(newMembers[i]);
|
||||
|
||||
if (! this.attached) {
|
||||
this.members = newMembers;
|
||||
} else {
|
||||
// don't do anything if we're going from empty to empty
|
||||
if (newMembers.length || oldMembers.length) {
|
||||
// detach the old members and insert the new members
|
||||
var nextNode = this.lastNode().nextSibling;
|
||||
var parentElement = this.parentElement;
|
||||
// Use detach/attach, but don't fire attached/detached hooks
|
||||
this.detach(true /*_isReplace*/);
|
||||
this.members = newMembers;
|
||||
this.attach(parentElement, nextNode, false, true /*_isReplace*/);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange.prototype.firstNode = function () {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
if (! this.members.length)
|
||||
return this.emptyRangePlaceholder;
|
||||
|
||||
var m = this.members[0];
|
||||
return (m instanceof DOMRange) ? m.firstNode() : m;
|
||||
};
|
||||
|
||||
DOMRange.prototype.lastNode = function () {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
if (! this.members.length)
|
||||
return this.emptyRangePlaceholder;
|
||||
|
||||
var m = this.members[this.members.length - 1];
|
||||
return (m instanceof DOMRange) ? m.lastNode() : m;
|
||||
};
|
||||
|
||||
DOMRange.prototype.detach = function (_isReplace) {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
var oldParentElement = this.parentElement;
|
||||
var members = this.members;
|
||||
if (members.length) {
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
DOMRange._remove(members[i]);
|
||||
}
|
||||
} else {
|
||||
var placeholder = this.emptyRangePlaceholder;
|
||||
this.parentElement.removeChild(placeholder);
|
||||
this.emptyRangePlaceholder = null;
|
||||
}
|
||||
|
||||
if (! _isReplace) {
|
||||
this.attached = false;
|
||||
this.parentElement = null;
|
||||
|
||||
for(var i = 0; i < this.attachedCallbacks.length; i++) {
|
||||
var obj = this.attachedCallbacks[i];
|
||||
obj.detached && obj.detached(this, oldParentElement);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange.prototype.addMember = function (newMember, atIndex, _isMove) {
|
||||
var members = this.members;
|
||||
if (! (atIndex >= 0 && atIndex <= members.length))
|
||||
throw new Error("Bad index in range.addMember: " + atIndex);
|
||||
|
||||
if (! _isMove)
|
||||
this._memberIn(newMember);
|
||||
|
||||
if (! this.attached) {
|
||||
// currently detached; just updated members
|
||||
members.splice(atIndex, 0, newMember);
|
||||
} else if (members.length === 0) {
|
||||
// empty; use the empty-to-nonempty handling of setMembers
|
||||
this.setMembers([newMember]);
|
||||
} else {
|
||||
var nextNode;
|
||||
if (atIndex === members.length) {
|
||||
// insert at end
|
||||
nextNode = this.lastNode().nextSibling;
|
||||
} else {
|
||||
var m = members[atIndex];
|
||||
nextNode = (m instanceof DOMRange) ? m.firstNode() : m;
|
||||
}
|
||||
members.splice(atIndex, 0, newMember);
|
||||
DOMRange._insert(newMember, this.parentElement, nextNode, _isMove);
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange.prototype.removeMember = function (atIndex, _isMove) {
|
||||
var members = this.members;
|
||||
if (! (atIndex >= 0 && atIndex < members.length))
|
||||
throw new Error("Bad index in range.removeMember: " + atIndex);
|
||||
|
||||
if (_isMove) {
|
||||
members.splice(atIndex, 1);
|
||||
} else {
|
||||
var oldMember = members[atIndex];
|
||||
this._memberOut(oldMember);
|
||||
|
||||
if (members.length === 1) {
|
||||
// becoming empty; use the logic in setMembers
|
||||
this.setMembers(_emptyArray);
|
||||
} else {
|
||||
members.splice(atIndex, 1);
|
||||
if (this.attached)
|
||||
DOMRange._remove(oldMember);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange.prototype.moveMember = function (oldIndex, newIndex) {
|
||||
var member = this.members[oldIndex];
|
||||
this.removeMember(oldIndex, true /*_isMove*/);
|
||||
this.addMember(member, newIndex, true /*_isMove*/);
|
||||
};
|
||||
|
||||
DOMRange.prototype.getMember = function (atIndex) {
|
||||
var members = this.members;
|
||||
if (! (atIndex >= 0 && atIndex < members.length))
|
||||
throw new Error("Bad index in range.getMember: " + atIndex);
|
||||
return this.members[atIndex];
|
||||
};
|
||||
|
||||
DOMRange.prototype._memberIn = function (m) {
|
||||
if (m instanceof DOMRange)
|
||||
m.parentRange = this;
|
||||
else if (m.nodeType === 1) // DOM Element
|
||||
m.$blaze_range = this;
|
||||
};
|
||||
|
||||
DOMRange._destroy = function (m, _skipNodes) {
|
||||
if (m instanceof DOMRange) {
|
||||
if (m.view)
|
||||
Blaze._destroyView(m.view, _skipNodes);
|
||||
} else if ((! _skipNodes) && m.nodeType === 1) {
|
||||
// DOM Element
|
||||
if (m.$blaze_range) {
|
||||
Blaze._destroyNode(m);
|
||||
m.$blaze_range = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange.prototype._memberOut = DOMRange._destroy;
|
||||
|
||||
// Tear down, but don't remove, the members. Used when chunks
|
||||
// of DOM are being torn down or replaced.
|
||||
DOMRange.prototype.destroyMembers = function (_skipNodes) {
|
||||
var members = this.members;
|
||||
for (var i = 0; i < members.length; i++)
|
||||
this._memberOut(members[i], _skipNodes);
|
||||
};
|
||||
|
||||
DOMRange.prototype.destroy = function (_skipNodes) {
|
||||
DOMRange._destroy(this, _skipNodes);
|
||||
};
|
||||
|
||||
DOMRange.prototype.containsElement = function (elem) {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
// An element is contained in this DOMRange if it's possible to
|
||||
// reach it by walking parent pointers, first through the DOM and
|
||||
// then parentRange pointers. In other words, the element or some
|
||||
// ancestor of it is at our level of the DOM (a child of our
|
||||
// parentElement), and this element is one of our members or
|
||||
// is a member of a descendant Range.
|
||||
|
||||
// First check that elem is a descendant of this.parentElement,
|
||||
// according to the DOM.
|
||||
if (! Blaze._elementContains(this.parentElement, elem))
|
||||
return false;
|
||||
|
||||
// If elem is not an immediate child of this.parentElement,
|
||||
// walk up to its ancestor that is.
|
||||
while (elem.parentNode !== this.parentElement)
|
||||
elem = elem.parentNode;
|
||||
|
||||
var range = elem.$blaze_range;
|
||||
while (range && range !== this)
|
||||
range = range.parentRange;
|
||||
|
||||
return range === this;
|
||||
};
|
||||
|
||||
DOMRange.prototype.containsRange = function (range) {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
if (! range.attached)
|
||||
return false;
|
||||
|
||||
// A DOMRange is contained in this DOMRange if it's possible
|
||||
// to reach this range by following parent pointers. If the
|
||||
// DOMRange has the same parentElement, then it should be
|
||||
// a member, or a member of a member etc. Otherwise, we must
|
||||
// contain its parentElement.
|
||||
|
||||
if (range.parentElement !== this.parentElement)
|
||||
return this.containsElement(range.parentElement);
|
||||
|
||||
if (range === this)
|
||||
return false; // don't contain self
|
||||
|
||||
while (range && range !== this)
|
||||
range = range.parentRange;
|
||||
|
||||
return range === this;
|
||||
};
|
||||
|
||||
DOMRange.prototype.onAttached = function (attached) {
|
||||
this.onAttachedDetached({ attached: attached });
|
||||
};
|
||||
|
||||
// callbacks are `attached(range, element)` and
|
||||
// `detached(range, element)`, and they may
|
||||
// access the `callbacks` object in `this`.
|
||||
// The arguments to `detached` are the same
|
||||
// range and element that were passed to `attached`.
|
||||
DOMRange.prototype.onAttachedDetached = function (callbacks) {
|
||||
if (this.attachedCallbacks === _emptyArray)
|
||||
this.attachedCallbacks = [];
|
||||
this.attachedCallbacks.push(callbacks);
|
||||
};
|
||||
|
||||
DOMRange.prototype.$ = function (selector) {
|
||||
var self = this;
|
||||
|
||||
var parentNode = this.parentElement;
|
||||
if (! parentNode)
|
||||
throw new Error("Can't select in removed DomRange");
|
||||
|
||||
// Strategy: Find all selector matches under parentNode,
|
||||
// then filter out the ones that aren't in this DomRange
|
||||
// using `DOMRange#containsElement`. This is
|
||||
// asymptotically slow in the presence of O(N) sibling
|
||||
// content that is under parentNode but not in our range,
|
||||
// so if performance is an issue, the selector should be
|
||||
// run on a child element.
|
||||
|
||||
// Since jQuery can't run selectors on a DocumentFragment,
|
||||
// we don't expect findBySelector to work.
|
||||
if (parentNode.nodeType === 11 /* DocumentFragment */)
|
||||
throw new Error("Can't use $ on an offscreen range");
|
||||
|
||||
var results = Blaze._DOMBackend.findBySelector(selector, parentNode);
|
||||
|
||||
// We don't assume `results` has jQuery API; a plain array
|
||||
// should do just as well. However, if we do have a jQuery
|
||||
// array, we want to end up with one also, so we use
|
||||
// `.filter`.
|
||||
|
||||
// Function that selects only elements that are actually
|
||||
// in this DomRange, rather than simply descending from
|
||||
// `parentNode`.
|
||||
var filterFunc = function (elem) {
|
||||
// handle jQuery's arguments to filter, where the node
|
||||
// is in `this` and the index is the first argument.
|
||||
if (typeof elem === 'number')
|
||||
elem = this;
|
||||
|
||||
return self.containsElement(elem);
|
||||
};
|
||||
|
||||
if (! results.filter) {
|
||||
// not a jQuery array, and not a browser with
|
||||
// Array.prototype.filter (e.g. IE <9)
|
||||
var newResults = [];
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var x = results[i];
|
||||
if (filterFunc(x))
|
||||
newResults.push(x);
|
||||
}
|
||||
results = newResults;
|
||||
} else {
|
||||
// `results.filter` is either jQuery's or ECMAScript's `filter`
|
||||
results = results.filter(filterFunc);
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
// Returns true if element a contains node b and is not node b.
|
||||
//
|
||||
// The restriction that `a` be an element (not a document fragment,
|
||||
// say) is based on what's easy to implement cross-browser.
|
||||
Blaze._elementContains = function (a, b) {
|
||||
if (a.nodeType !== 1) // ELEMENT
|
||||
return false;
|
||||
if (a === b)
|
||||
return false;
|
||||
|
||||
if (a.compareDocumentPosition) {
|
||||
return a.compareDocumentPosition(b) & 0x10;
|
||||
} else {
|
||||
// Should be only old IE and maybe other old browsers here.
|
||||
// Modern Safari has both functions but seems to get contains() wrong.
|
||||
// IE can't handle b being a text node. We work around this
|
||||
// by doing a direct parent test now.
|
||||
b = b.parentNode;
|
||||
if (! (b && b.nodeType === 1)) // ELEMENT
|
||||
return false;
|
||||
if (a === b)
|
||||
return true;
|
||||
|
||||
return a.contains(b);
|
||||
}
|
||||
};
|
||||
@@ -1,204 +0,0 @@
|
||||
var EventSupport = Blaze._EventSupport = {};
|
||||
|
||||
var DOMBackend = Blaze._DOMBackend;
|
||||
|
||||
// List of events to always delegate, never capture.
|
||||
// Since jQuery fakes bubbling for certain events in
|
||||
// certain browsers (like `submit`), we don't want to
|
||||
// get in its way.
|
||||
//
|
||||
// We could list all known bubbling
|
||||
// events here to avoid creating speculative capturers
|
||||
// for them, but it would only be an optimization.
|
||||
var eventsToDelegate = EventSupport.eventsToDelegate = {
|
||||
blur: 1, change: 1, click: 1, focus: 1, focusin: 1,
|
||||
focusout: 1, reset: 1, submit: 1
|
||||
};
|
||||
|
||||
var EVENT_MODE = EventSupport.EVENT_MODE = {
|
||||
TBD: 0,
|
||||
BUBBLING: 1,
|
||||
CAPTURING: 2
|
||||
};
|
||||
|
||||
var NEXT_HANDLERREC_ID = 1;
|
||||
|
||||
var HandlerRec = function (elem, type, selector, handler, recipient) {
|
||||
this.elem = elem;
|
||||
this.type = type;
|
||||
this.selector = selector;
|
||||
this.handler = handler;
|
||||
this.recipient = recipient;
|
||||
this.id = (NEXT_HANDLERREC_ID++);
|
||||
|
||||
this.mode = EVENT_MODE.TBD;
|
||||
|
||||
// It's important that delegatedHandler be a different
|
||||
// instance for each handlerRecord, because its identity
|
||||
// is used to remove it.
|
||||
//
|
||||
// It's also important that the closure have access to
|
||||
// `this` when it is not called with it set.
|
||||
this.delegatedHandler = (function (h) {
|
||||
return function (evt) {
|
||||
if ((! h.selector) && evt.currentTarget !== evt.target)
|
||||
// no selector means only fire on target
|
||||
return;
|
||||
return h.handler.apply(h.recipient, arguments);
|
||||
};
|
||||
})(this);
|
||||
|
||||
// WHY CAPTURE AND DELEGATE: jQuery can't delegate
|
||||
// non-bubbling events, because
|
||||
// event capture doesn't work in IE 8. However, there
|
||||
// are all sorts of new-fangled non-bubbling events
|
||||
// like "play" and "touchenter". We delegate these
|
||||
// events using capture in all browsers except IE 8.
|
||||
// IE 8 doesn't support these events anyway.
|
||||
|
||||
var tryCapturing = elem.addEventListener &&
|
||||
(! _.has(eventsToDelegate,
|
||||
DOMBackend.Events.parseEventType(type)));
|
||||
|
||||
if (tryCapturing) {
|
||||
this.capturingHandler = (function (h) {
|
||||
return function (evt) {
|
||||
if (h.mode === EVENT_MODE.TBD) {
|
||||
// must be first time we're called.
|
||||
if (evt.bubbles) {
|
||||
// this type of event bubbles, so don't
|
||||
// get called again.
|
||||
h.mode = EVENT_MODE.BUBBLING;
|
||||
DOMBackend.Events.unbindEventCapturer(
|
||||
h.elem, h.type, h.capturingHandler);
|
||||
return;
|
||||
} else {
|
||||
// this type of event doesn't bubble,
|
||||
// so unbind the delegation, preventing
|
||||
// it from ever firing.
|
||||
h.mode = EVENT_MODE.CAPTURING;
|
||||
DOMBackend.Events.undelegateEvents(
|
||||
h.elem, h.type, h.delegatedHandler);
|
||||
}
|
||||
}
|
||||
|
||||
h.delegatedHandler(evt);
|
||||
};
|
||||
})(this);
|
||||
|
||||
} else {
|
||||
this.mode = EVENT_MODE.BUBBLING;
|
||||
}
|
||||
};
|
||||
EventSupport.HandlerRec = HandlerRec;
|
||||
|
||||
HandlerRec.prototype.bind = function () {
|
||||
// `this.mode` may be EVENT_MODE_TBD, in which case we bind both. in
|
||||
// this case, 'capturingHandler' is in charge of detecting the
|
||||
// correct mode and turning off one or the other handlers.
|
||||
if (this.mode !== EVENT_MODE.BUBBLING) {
|
||||
DOMBackend.Events.bindEventCapturer(
|
||||
this.elem, this.type, this.selector || '*',
|
||||
this.capturingHandler);
|
||||
}
|
||||
|
||||
if (this.mode !== EVENT_MODE.CAPTURING)
|
||||
DOMBackend.Events.delegateEvents(
|
||||
this.elem, this.type,
|
||||
this.selector || '*', this.delegatedHandler);
|
||||
};
|
||||
|
||||
HandlerRec.prototype.unbind = function () {
|
||||
if (this.mode !== EVENT_MODE.BUBBLING)
|
||||
DOMBackend.Events.unbindEventCapturer(this.elem, this.type,
|
||||
this.capturingHandler);
|
||||
|
||||
if (this.mode !== EVENT_MODE.CAPTURING)
|
||||
DOMBackend.Events.undelegateEvents(this.elem, this.type,
|
||||
this.delegatedHandler);
|
||||
};
|
||||
|
||||
EventSupport.listen = function (element, events, selector, handler, recipient, getParentRecipient) {
|
||||
|
||||
// Prevent this method from being JITed by Safari. Due to a
|
||||
// presumed JIT bug in Safari -- observed in Version 7.0.6
|
||||
// (9537.78.2) -- this method may crash the Safari render process if
|
||||
// it is JITed.
|
||||
// Repro: https://github.com/dgreensp/public/tree/master/safari-crash
|
||||
try { element = element; } finally {}
|
||||
|
||||
var eventTypes = [];
|
||||
events.replace(/[^ /]+/g, function (e) {
|
||||
eventTypes.push(e);
|
||||
});
|
||||
|
||||
var newHandlerRecs = [];
|
||||
for (var i = 0, N = eventTypes.length; i < N; i++) {
|
||||
var type = eventTypes[i];
|
||||
|
||||
var eventDict = element.$blaze_events;
|
||||
if (! eventDict)
|
||||
eventDict = (element.$blaze_events = {});
|
||||
|
||||
var info = eventDict[type];
|
||||
if (! info) {
|
||||
info = eventDict[type] = {};
|
||||
info.handlers = [];
|
||||
}
|
||||
var handlerList = info.handlers;
|
||||
var handlerRec = new HandlerRec(
|
||||
element, type, selector, handler, recipient);
|
||||
newHandlerRecs.push(handlerRec);
|
||||
handlerRec.bind();
|
||||
handlerList.push(handlerRec);
|
||||
// Move handlers of enclosing ranges to end, by unbinding and rebinding
|
||||
// them. In jQuery (or other DOMBackend) this causes them to fire
|
||||
// later when the backend dispatches event handlers.
|
||||
if (getParentRecipient) {
|
||||
for (var r = getParentRecipient(recipient); r;
|
||||
r = getParentRecipient(r)) {
|
||||
// r is an enclosing range (recipient)
|
||||
for (var j = 0, Nj = handlerList.length;
|
||||
j < Nj; j++) {
|
||||
var h = handlerList[j];
|
||||
if (h.recipient === r) {
|
||||
h.unbind();
|
||||
h.bind();
|
||||
handlerList.splice(j, 1); // remove handlerList[j]
|
||||
handlerList.push(h);
|
||||
j--; // account for removed handler
|
||||
Nj--; // don't visit appended handlers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// closes over just `element` and `newHandlerRecs`
|
||||
stop: function () {
|
||||
var eventDict = element.$blaze_events;
|
||||
if (! eventDict)
|
||||
return;
|
||||
// newHandlerRecs has only one item unless you specify multiple
|
||||
// event types. If this code is slow, it's because we have to
|
||||
// iterate over handlerList here. Clearing a whole handlerList
|
||||
// via stop() methods is O(N^2) in the number of handlers on
|
||||
// an element.
|
||||
for (var i = 0; i < newHandlerRecs.length; i++) {
|
||||
var handlerToRemove = newHandlerRecs[i];
|
||||
var info = eventDict[handlerToRemove.type];
|
||||
if (! info)
|
||||
continue;
|
||||
var handlerList = info.handlers;
|
||||
for (var j = handlerList.length - 1; j >= 0; j--) {
|
||||
if (handlerList[j] === handlerToRemove) {
|
||||
handlerToRemove.unbind();
|
||||
handlerList.splice(j, 1); // remove handlerList[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
newHandlerRecs.length = 0;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
var debugFunc;
|
||||
|
||||
// We call into user code in many places, and it's nice to catch exceptions
|
||||
// propagated from user code immediately so that the whole system doesn't just
|
||||
// break. Catching exceptions is easy; reporting them is hard. This helper
|
||||
// reports exceptions.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ```
|
||||
// try {
|
||||
// // ... someStuff ...
|
||||
// } catch (e) {
|
||||
// reportUIException(e);
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// An optional second argument overrides the default message.
|
||||
|
||||
// Set this to `true` to cause `reportException` to throw
|
||||
// the next exception rather than reporting it. This is
|
||||
// useful in unit tests that test error messages.
|
||||
Blaze._throwNextException = false;
|
||||
|
||||
Blaze._reportException = function (e, msg) {
|
||||
if (Blaze._throwNextException) {
|
||||
Blaze._throwNextException = false;
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (! debugFunc)
|
||||
// adapted from Tracker
|
||||
debugFunc = function () {
|
||||
return (typeof Meteor !== "undefined" ? Meteor._debug :
|
||||
((typeof console !== "undefined") && console.log ? console.log :
|
||||
function () {}));
|
||||
};
|
||||
|
||||
// In Chrome, `e.stack` is a multiline string that starts with the message
|
||||
// and contains a stack trace. Furthermore, `console.log` makes it clickable.
|
||||
// `console.log` supplies the space between the two arguments.
|
||||
debugFunc()(msg || 'Exception caught in template:', e.stack || e.message || e);
|
||||
};
|
||||
|
||||
Blaze._wrapCatchingExceptions = function (f, where) {
|
||||
if (typeof f !== 'function')
|
||||
return f;
|
||||
|
||||
return function () {
|
||||
try {
|
||||
return f.apply(this, arguments);
|
||||
} catch (e) {
|
||||
Blaze._reportException(e, 'Exception in ' + where + ':');
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,238 +0,0 @@
|
||||
Blaze._globalHelpers = {};
|
||||
|
||||
// Documented as Template.registerHelper.
|
||||
// This definition also provides back-compat for `UI.registerHelper`.
|
||||
Blaze.registerHelper = function (name, func) {
|
||||
Blaze._globalHelpers[name] = func;
|
||||
};
|
||||
|
||||
// Also documented as Template.deregisterHelper
|
||||
Blaze.deregisterHelper = function(name) {
|
||||
delete Blaze._globalHelpers[name];
|
||||
}
|
||||
|
||||
var bindIfIsFunction = function (x, target) {
|
||||
if (typeof x !== 'function')
|
||||
return x;
|
||||
return _.bind(x, target);
|
||||
};
|
||||
|
||||
// If `x` is a function, binds the value of `this` for that function
|
||||
// to the current data context.
|
||||
var bindDataContext = function (x) {
|
||||
if (typeof x === 'function') {
|
||||
return function () {
|
||||
var data = Blaze.getData();
|
||||
if (data == null)
|
||||
data = {};
|
||||
return x.apply(data, arguments);
|
||||
};
|
||||
}
|
||||
return x;
|
||||
};
|
||||
|
||||
Blaze._OLDSTYLE_HELPER = {};
|
||||
|
||||
Blaze._getTemplateHelper = function (template, name, tmplInstanceFunc) {
|
||||
// XXX COMPAT WITH 0.9.3
|
||||
var isKnownOldStyleHelper = false;
|
||||
|
||||
if (template.__helpers.has(name)) {
|
||||
var helper = template.__helpers.get(name);
|
||||
if (helper === Blaze._OLDSTYLE_HELPER) {
|
||||
isKnownOldStyleHelper = true;
|
||||
} else if (helper != null) {
|
||||
return wrapHelper(bindDataContext(helper), tmplInstanceFunc);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// old-style helper
|
||||
if (name in template) {
|
||||
// Only warn once per helper
|
||||
if (! isKnownOldStyleHelper) {
|
||||
template.__helpers.set(name, Blaze._OLDSTYLE_HELPER);
|
||||
if (! template._NOWARN_OLDSTYLE_HELPERS) {
|
||||
Blaze._warn('Assigning helper with `' + template.viewName + '.' +
|
||||
name + ' = ...` is deprecated. Use `' + template.viewName +
|
||||
'.helpers(...)` instead.');
|
||||
}
|
||||
}
|
||||
if (template[name] != null) {
|
||||
return wrapHelper(bindDataContext(template[name]), tmplInstanceFunc);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
var wrapHelper = function (f, templateFunc) {
|
||||
if (typeof f !== "function") {
|
||||
return f;
|
||||
}
|
||||
|
||||
return function () {
|
||||
var self = this;
|
||||
var args = arguments;
|
||||
|
||||
return Blaze.Template._withTemplateInstanceFunc(templateFunc, function () {
|
||||
return Blaze._wrapCatchingExceptions(f, 'template helper').apply(self, args);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
Blaze._lexicalBindingLookup = function (view, name) {
|
||||
var currentView = view;
|
||||
var blockHelpersStack = [];
|
||||
|
||||
// walk up the views stopping at a Spacebars.include or Template view that
|
||||
// doesn't have an InOuterTemplateScope view as a parent
|
||||
do {
|
||||
// skip block helpers views
|
||||
// if we found the binding on the scope, return it
|
||||
if (_.has(currentView._scopeBindings, name)) {
|
||||
var bindingReactiveVar = currentView._scopeBindings[name];
|
||||
return function () {
|
||||
return bindingReactiveVar.get();
|
||||
};
|
||||
}
|
||||
} while (! (currentView.__startsNewLexicalScope &&
|
||||
! (currentView.parentView &&
|
||||
currentView.parentView.__childDoesntStartNewLexicalScope))
|
||||
&& (currentView = currentView.parentView));
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// templateInstance argument is provided to be available for possible
|
||||
// alternative implementations of this function by 3rd party packages.
|
||||
Blaze._getTemplate = function (name, templateInstance) {
|
||||
if ((name in Blaze.Template) && (Blaze.Template[name] instanceof Blaze.Template)) {
|
||||
return Blaze.Template[name];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Blaze._getGlobalHelper = function (name, templateInstance) {
|
||||
if (Blaze._globalHelpers[name] != null) {
|
||||
return wrapHelper(bindDataContext(Blaze._globalHelpers[name]), templateInstance);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Looks up a name, like "foo" or "..", as a helper of the
|
||||
// current template; the name of a template; a global helper;
|
||||
// or a property of the data context. Called on the View of
|
||||
// a template (i.e. a View with a `.template` property,
|
||||
// where the helpers are). Used for the first name in a
|
||||
// "path" in a template tag, like "foo" in `{{foo.bar}}` or
|
||||
// ".." in `{{frobulate ../blah}}`.
|
||||
//
|
||||
// Returns a function, a non-function value, or null. If
|
||||
// a function is found, it is bound appropriately.
|
||||
//
|
||||
// NOTE: This function must not establish any reactive
|
||||
// dependencies itself. If there is any reactivity in the
|
||||
// value, lookup should return a function.
|
||||
Blaze.View.prototype.lookup = function (name, _options) {
|
||||
var template = this.template;
|
||||
var lookupTemplate = _options && _options.template;
|
||||
var helper;
|
||||
var binding;
|
||||
var boundTmplInstance;
|
||||
var foundTemplate;
|
||||
|
||||
if (this.templateInstance) {
|
||||
boundTmplInstance = _.bind(this.templateInstance, this);
|
||||
}
|
||||
|
||||
// 0. looking up the parent data context with the special "../" syntax
|
||||
if (/^\./.test(name)) {
|
||||
// starts with a dot. must be a series of dots which maps to an
|
||||
// ancestor of the appropriate height.
|
||||
if (!/^(\.)+$/.test(name))
|
||||
throw new Error("id starting with dot must be a series of dots");
|
||||
|
||||
return Blaze._parentData(name.length - 1, true /*_functionWrapped*/);
|
||||
|
||||
}
|
||||
|
||||
// 1. look up a helper on the current template
|
||||
if (template && ((helper = Blaze._getTemplateHelper(template, name, boundTmplInstance)) != null)) {
|
||||
return helper;
|
||||
}
|
||||
|
||||
// 2. look up a binding by traversing the lexical view hierarchy inside the
|
||||
// current template
|
||||
if (template && (binding = Blaze._lexicalBindingLookup(Blaze.currentView, name)) != null) {
|
||||
return binding;
|
||||
}
|
||||
|
||||
// 3. look up a template by name
|
||||
if (lookupTemplate && ((foundTemplate = Blaze._getTemplate(name, boundTmplInstance)) != null)) {
|
||||
return foundTemplate;
|
||||
}
|
||||
|
||||
// 4. look up a global helper
|
||||
if ((helper = Blaze._getGlobalHelper(name, boundTmplInstance)) != null) {
|
||||
return helper;
|
||||
}
|
||||
|
||||
// 5. look up in a data context
|
||||
return function () {
|
||||
var isCalledAsFunction = (arguments.length > 0);
|
||||
var data = Blaze.getData();
|
||||
var x = data && data[name];
|
||||
if (! x) {
|
||||
if (lookupTemplate) {
|
||||
throw new Error("No such template: " + name);
|
||||
} else if (isCalledAsFunction) {
|
||||
throw new Error("No such function: " + name);
|
||||
} else if (name.charAt(0) === '@' && ((x === null) ||
|
||||
(x === undefined))) {
|
||||
// Throw an error if the user tries to use a `@directive`
|
||||
// that doesn't exist. We don't implement all directives
|
||||
// from Handlebars, so there's a potential for confusion
|
||||
// if we fail silently. On the other hand, we want to
|
||||
// throw late in case some app or package wants to provide
|
||||
// a missing directive.
|
||||
throw new Error("Unsupported directive: " + name);
|
||||
}
|
||||
}
|
||||
if (! data) {
|
||||
return null;
|
||||
}
|
||||
if (typeof x !== 'function') {
|
||||
if (isCalledAsFunction) {
|
||||
throw new Error("Can't call non-function: " + x);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
return x.apply(data, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
// Implement Spacebars' {{../..}}.
|
||||
// @param height {Number} The number of '..'s
|
||||
Blaze._parentData = function (height, _functionWrapped) {
|
||||
// If height is null or undefined, we default to 1, the first parent.
|
||||
if (height == null) {
|
||||
height = 1;
|
||||
}
|
||||
var theWith = Blaze.getView('with');
|
||||
for (var i = 0; (i < height) && theWith; i++) {
|
||||
theWith = Blaze.getView(theWith, 'with');
|
||||
}
|
||||
|
||||
if (! theWith)
|
||||
return null;
|
||||
if (_functionWrapped)
|
||||
return function () { return theWith.dataVar.get(); };
|
||||
return theWith.dataVar.get();
|
||||
};
|
||||
|
||||
|
||||
Blaze.View.prototype.lookupTemplate = function (name) {
|
||||
return this.lookup(name, {template:true});
|
||||
};
|
||||
@@ -1,191 +0,0 @@
|
||||
// Turns HTMLjs into DOM nodes and DOMRanges.
|
||||
//
|
||||
// - `htmljs`: the value to materialize, which may be any of the htmljs
|
||||
// types (Tag, CharRef, Comment, Raw, array, string, boolean, number,
|
||||
// null, or undefined) or a View or Template (which will be used to
|
||||
// construct a View).
|
||||
// - `intoArray`: the array of DOM nodes and DOMRanges to push the output
|
||||
// into (required)
|
||||
// - `parentView`: the View we are materializing content for (optional)
|
||||
// - `_existingWorkStack`: optional argument, only used for recursive
|
||||
// calls when there is some other _materializeDOM on the call stack.
|
||||
// If _materializeDOM called your function and passed in a workStack,
|
||||
// pass it back when you call _materializeDOM (such as from a workStack
|
||||
// task).
|
||||
//
|
||||
// Returns `intoArray`, which is especially useful if you pass in `[]`.
|
||||
Blaze._materializeDOM = function (htmljs, intoArray, parentView,
|
||||
_existingWorkStack) {
|
||||
// In order to use fewer stack frames, materializeDOMInner can push
|
||||
// tasks onto `workStack`, and they will be popped off
|
||||
// and run, last first, after materializeDOMInner returns. The
|
||||
// reason we use a stack instead of a queue is so that we recurse
|
||||
// depth-first, doing newer tasks first.
|
||||
var workStack = (_existingWorkStack || []);
|
||||
materializeDOMInner(htmljs, intoArray, parentView, workStack);
|
||||
|
||||
if (! _existingWorkStack) {
|
||||
// We created the work stack, so we are responsible for finishing
|
||||
// the work. Call each "task" function, starting with the top
|
||||
// of the stack.
|
||||
while (workStack.length) {
|
||||
// Note that running task() may push new items onto workStack.
|
||||
var task = workStack.pop();
|
||||
task();
|
||||
}
|
||||
}
|
||||
|
||||
return intoArray;
|
||||
};
|
||||
|
||||
var materializeDOMInner = function (htmljs, intoArray, parentView, workStack) {
|
||||
if (htmljs == null) {
|
||||
// null or undefined
|
||||
return;
|
||||
}
|
||||
|
||||
switch (typeof htmljs) {
|
||||
case 'string': case 'boolean': case 'number':
|
||||
intoArray.push(document.createTextNode(String(htmljs)));
|
||||
return;
|
||||
case 'object':
|
||||
if (htmljs.htmljsType) {
|
||||
switch (htmljs.htmljsType) {
|
||||
case HTML.Tag.htmljsType:
|
||||
intoArray.push(materializeTag(htmljs, parentView, workStack));
|
||||
return;
|
||||
case HTML.CharRef.htmljsType:
|
||||
intoArray.push(document.createTextNode(htmljs.str));
|
||||
return;
|
||||
case HTML.Comment.htmljsType:
|
||||
intoArray.push(document.createComment(htmljs.sanitizedValue));
|
||||
return;
|
||||
case HTML.Raw.htmljsType:
|
||||
// Get an array of DOM nodes by using the browser's HTML parser
|
||||
// (like innerHTML).
|
||||
var nodes = Blaze._DOMBackend.parseHTML(htmljs.value);
|
||||
for (var i = 0; i < nodes.length; i++)
|
||||
intoArray.push(nodes[i]);
|
||||
return;
|
||||
}
|
||||
} else if (HTML.isArray(htmljs)) {
|
||||
for (var i = htmljs.length-1; i >= 0; i--) {
|
||||
workStack.push(_.bind(Blaze._materializeDOM, null,
|
||||
htmljs[i], intoArray, parentView, workStack));
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
if (htmljs instanceof Blaze.Template) {
|
||||
htmljs = htmljs.constructView();
|
||||
// fall through to Blaze.View case below
|
||||
}
|
||||
if (htmljs instanceof Blaze.View) {
|
||||
Blaze._materializeView(htmljs, parentView, workStack, intoArray);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Unexpected object in htmljs: " + htmljs);
|
||||
};
|
||||
|
||||
var materializeTag = function (tag, parentView, workStack) {
|
||||
var tagName = tag.tagName;
|
||||
var elem;
|
||||
if ((HTML.isKnownSVGElement(tagName) || isSVGAnchor(tag))
|
||||
&& document.createElementNS) {
|
||||
// inline SVG
|
||||
elem = document.createElementNS('http://www.w3.org/2000/svg', tagName);
|
||||
} else {
|
||||
// normal elements
|
||||
elem = document.createElement(tagName);
|
||||
}
|
||||
|
||||
var rawAttrs = tag.attrs;
|
||||
var children = tag.children;
|
||||
if (tagName === 'textarea' && tag.children.length &&
|
||||
! (rawAttrs && ('value' in rawAttrs))) {
|
||||
// Provide very limited support for TEXTAREA tags with children
|
||||
// rather than a "value" attribute.
|
||||
// Reactivity in the form of Views nested in the tag's children
|
||||
// won't work. Compilers should compile textarea contents into
|
||||
// the "value" attribute of the tag, wrapped in a function if there
|
||||
// is reactivity.
|
||||
if (typeof rawAttrs === 'function' ||
|
||||
HTML.isArray(rawAttrs)) {
|
||||
throw new Error("Can't have reactive children of TEXTAREA node; " +
|
||||
"use the 'value' attribute instead.");
|
||||
}
|
||||
rawAttrs = _.extend({}, rawAttrs || null);
|
||||
rawAttrs.value = Blaze._expand(children, parentView);
|
||||
children = [];
|
||||
}
|
||||
|
||||
if (rawAttrs) {
|
||||
var attrUpdater = new ElementAttributesUpdater(elem);
|
||||
var updateAttributes = function () {
|
||||
var expandedAttrs = Blaze._expandAttributes(rawAttrs, parentView);
|
||||
var flattenedAttrs = HTML.flattenAttributes(expandedAttrs);
|
||||
var stringAttrs = {};
|
||||
for (var attrName in flattenedAttrs) {
|
||||
stringAttrs[attrName] = Blaze._toText(flattenedAttrs[attrName],
|
||||
parentView,
|
||||
HTML.TEXTMODE.STRING);
|
||||
}
|
||||
attrUpdater.update(stringAttrs);
|
||||
};
|
||||
var updaterComputation;
|
||||
if (parentView) {
|
||||
updaterComputation =
|
||||
parentView.autorun(updateAttributes, undefined, 'updater');
|
||||
} else {
|
||||
updaterComputation = Tracker.nonreactive(function () {
|
||||
return Tracker.autorun(function () {
|
||||
Tracker._withCurrentView(parentView, updateAttributes);
|
||||
});
|
||||
});
|
||||
}
|
||||
Blaze._DOMBackend.Teardown.onElementTeardown(elem, function attrTeardown() {
|
||||
updaterComputation.stop();
|
||||
});
|
||||
}
|
||||
|
||||
if (children.length) {
|
||||
var childNodesAndRanges = [];
|
||||
// push this function first so that it's done last
|
||||
workStack.push(function () {
|
||||
for (var i = 0; i < childNodesAndRanges.length; i++) {
|
||||
var x = childNodesAndRanges[i];
|
||||
if (x instanceof Blaze._DOMRange)
|
||||
x.attach(elem);
|
||||
else
|
||||
elem.appendChild(x);
|
||||
}
|
||||
});
|
||||
// now push the task that calculates childNodesAndRanges
|
||||
workStack.push(_.bind(Blaze._materializeDOM, null,
|
||||
children, childNodesAndRanges, parentView,
|
||||
workStack));
|
||||
}
|
||||
|
||||
return elem;
|
||||
};
|
||||
|
||||
|
||||
var isSVGAnchor = function (node) {
|
||||
// We generally aren't able to detect SVG <a> elements because
|
||||
// if "A" were in our list of known svg element names, then all
|
||||
// <a> nodes would be created using
|
||||
// `document.createElementNS`. But in the special case of <a
|
||||
// xlink:href="...">, we can at least detect that attribute and
|
||||
// create an SVG <a> 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);
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
// Microscore is a partial polyfill for Underscore. It implements
|
||||
// a subset of Underscore functions, and for some functions it
|
||||
// implements a subset of the full functionality.
|
||||
//
|
||||
// Code written against Microscore should just work with Underscore.
|
||||
// The reverse is not true, because Microscore doesn't support
|
||||
// all features of every function. A list of known differences
|
||||
// between Underscore and Microscore is given with each function.
|
||||
//
|
||||
// This file should be curated to keep it small, so that it doesn't
|
||||
// grow into Underscore.
|
||||
//
|
||||
// In the future, we'll figure out something better, like package
|
||||
// slices and dead code elimination.
|
||||
|
||||
if (typeof _ !== 'undefined')
|
||||
throw new Error("If you have Underscore, don't use Microscore");
|
||||
|
||||
_ = {};
|
||||
|
||||
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
var objectToString = Object.prototype.toString;
|
||||
|
||||
// Doesn't support more than two arguments (more than one "source"
|
||||
// object).
|
||||
_.extend = function (tgt, src) {
|
||||
for (var k in src) {
|
||||
if (hasOwnProperty.call(src, k))
|
||||
tgt[k] = src[k];
|
||||
}
|
||||
return tgt;
|
||||
};
|
||||
|
||||
_.has = function (obj, key) {
|
||||
return hasOwnProperty.call(obj, key);
|
||||
};
|
||||
|
||||
// Returns a copy of `array` with falsy elements removed.
|
||||
_.compact = function (array) {
|
||||
var result = [];
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
var item = array[i];
|
||||
if (item)
|
||||
result.push(item);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Returns whether `array` contains an element that is
|
||||
// `=== item`.
|
||||
_.contains = function (array, item) {
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
if (array[i] === item)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Returns `array` filtered to exclude elements that are
|
||||
// `=== item`. Similar to `_.without`.
|
||||
_.without = function (array, item) {
|
||||
var result = [];
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
var x = array[i];
|
||||
if (x !== item)
|
||||
result.push(x);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Assembles an array by calling `func(oldElement, index)`
|
||||
// on each element of `array`. Assumes argument is an array.
|
||||
_.map = function (array, func) {
|
||||
var result = new Array(array.length);
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
result[i] = func(array[i], i);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Given an array: Calls `func(element, index)` on each element of
|
||||
// `array`.
|
||||
//
|
||||
// Given an object: Calls `func(value, key)` on each key/value of
|
||||
// `obj`.
|
||||
//
|
||||
// Only REAL arrays are treated as arrays. No Arguments objects, jQuery
|
||||
// objects, etc. This may be relaxed to the standard Meteor approach
|
||||
// if it is too constraining.
|
||||
//
|
||||
// Doesn't accept `null` as first argument. Doesn't delegate to built-in
|
||||
// `forEach` (which is generally not faster anyway because it calls
|
||||
// across the C/JS boundary). Doesn't mess with JavaScript's built-in
|
||||
// behavior if keys are added or removed during iteration (i.e. may
|
||||
// or may not visit them).
|
||||
|
||||
_.each = function (arrayOrObject, func) {
|
||||
if (objectToString.call(arrayOrObject) === '[object Array]') {
|
||||
var array = arrayOrObject;
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
func(array[i], i);
|
||||
}
|
||||
} else {
|
||||
var obj = arrayOrObject;
|
||||
for (var key in obj) {
|
||||
if (_.has(obj, key))
|
||||
func(obj[key], key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_.bind = function (f, context) {
|
||||
return function () {
|
||||
return f.apply(target, context);
|
||||
};
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
Package.describe({
|
||||
summary: "Meteor Reactive Templating library",
|
||||
version: '2.1.8'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.export(['Blaze', 'UI', 'Handlebars']);
|
||||
api.use('jquery'); // should be a weak dep, by having multiple "DOM backends"
|
||||
api.use('tracker');
|
||||
api.use('check');
|
||||
api.use('underscore'); // only the subset in microscore.js
|
||||
api.use('htmljs');
|
||||
api.imply('htmljs');
|
||||
api.use('observe-sequence');
|
||||
api.use('reactive-var');
|
||||
|
||||
api.addFiles([
|
||||
'preamble.js'
|
||||
]);
|
||||
|
||||
// client-only files
|
||||
api.addFiles([
|
||||
'dombackend.js',
|
||||
'domrange.js',
|
||||
'events.js',
|
||||
'attrs.js',
|
||||
'materializer.js'
|
||||
], 'client');
|
||||
|
||||
// client and server
|
||||
api.addFiles([
|
||||
'exceptions.js',
|
||||
'view.js',
|
||||
'builtins.js',
|
||||
'lookup.js',
|
||||
'template.js',
|
||||
'backcompat.js'
|
||||
]);
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use('jquery'); // strong dependency, for testing jQuery backend
|
||||
api.use('blaze');
|
||||
api.use('test-helpers');
|
||||
api.use('underscore');
|
||||
api.use('blaze-tools'); // for BlazeTools.toJS
|
||||
api.use('html-tools');
|
||||
api.use('reactive-var');
|
||||
api.use('tracker');
|
||||
api.use('templating');
|
||||
|
||||
api.addFiles('view_tests.js');
|
||||
api.addFiles('render_tests.js', 'client');
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* @namespace Blaze
|
||||
* @summary The namespace for all Blaze-related methods and classes.
|
||||
*/
|
||||
Blaze = {};
|
||||
|
||||
// Utility to HTML-escape a string. Included for legacy reasons.
|
||||
Blaze._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);
|
||||
};
|
||||
})();
|
||||
|
||||
Blaze._warn = function (msg) {
|
||||
msg = 'Warning: ' + msg;
|
||||
|
||||
if ((typeof console !== 'undefined') && console.warn) {
|
||||
console.warn(msg);
|
||||
}
|
||||
};
|
||||
@@ -1,688 +0,0 @@
|
||||
var toCode = BlazeTools.toJS;
|
||||
|
||||
var P = HTML.P;
|
||||
var CharRef = HTML.CharRef;
|
||||
var DIV = HTML.DIV;
|
||||
var Comment = HTML.Comment;
|
||||
var BR = HTML.BR;
|
||||
var A = HTML.A;
|
||||
var UL = HTML.UL;
|
||||
var LI = HTML.LI;
|
||||
var SPAN = HTML.SPAN;
|
||||
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, parent);
|
||||
};
|
||||
|
||||
var toHTML = Blaze.toHTML;
|
||||
|
||||
Tinytest.add("blaze - render - basic", function (test) {
|
||||
var run = function (input, expectedInnerHTML, expectedHTML, expectedCode) {
|
||||
var div = document.createElement("DIV");
|
||||
materialize(input, div);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), expectedInnerHTML);
|
||||
test.equal(toHTML(input), expectedHTML);
|
||||
if (typeof expectedCode !== 'undefined')
|
||||
test.equal(toCode(input), expectedCode);
|
||||
};
|
||||
|
||||
run(P('Hello'),
|
||||
'<p>Hello</p>',
|
||||
'<p>Hello</p>',
|
||||
'HTML.P("Hello")');
|
||||
|
||||
run([], '', '', '[]');
|
||||
run([null, null], '', '', '[null, null]');
|
||||
|
||||
// Test crazy character references
|
||||
|
||||
// `𝕫` is "Mathematical double-struck small z" a.k.a. "open-face z"
|
||||
run(P(CharRef({html: '𝕫', str: '\ud835\udd6b'})),
|
||||
'<p>\ud835\udd6b</p>',
|
||||
'<p>𝕫</p>',
|
||||
'HTML.P(HTML.CharRef({html: "𝕫", str: "\\ud835\\udd6b"}))');
|
||||
|
||||
run(P({id: CharRef({html: '𝕫', str: '\ud835\udd6b'})}, 'Hello'),
|
||||
'<p id="\ud835\udd6b">Hello</p>',
|
||||
'<p id="𝕫">Hello</p>',
|
||||
'HTML.P({id: HTML.CharRef({html: "𝕫", str: "\\ud835\\udd6b"})}, "Hello")');
|
||||
|
||||
run(P({id: [CharRef({html: '𝕫', str: '\ud835\udd6b'}), '!']}, 'Hello'),
|
||||
'<p id="\ud835\udd6b!">Hello</p>',
|
||||
'<p id="𝕫!">Hello</p>',
|
||||
'HTML.P({id: [HTML.CharRef({html: "𝕫", str: "\\ud835\\udd6b"}), "!"]}, "Hello")');
|
||||
|
||||
// Test comments
|
||||
|
||||
run(DIV(Comment('Test')),
|
||||
'<div><!----></div>', // our innerHTML-canonicalization function kills comment contents
|
||||
'<div><!--Test--></div>',
|
||||
'HTML.DIV(HTML.Comment("Test"))');
|
||||
|
||||
// Test arrays
|
||||
|
||||
run([P('Hello'), P('World')],
|
||||
'<p>Hello</p><p>World</p>',
|
||||
'<p>Hello</p><p>World</p>',
|
||||
'[HTML.P("Hello"), HTML.P("World")]');
|
||||
|
||||
// Test slightly more complicated structure
|
||||
|
||||
run(DIV({'class': 'foo'}, UL(LI(P(A({href: '#one'}, 'One'))),
|
||||
LI(P('Two', BR(), 'Three')))),
|
||||
'<div class="foo"><ul><li><p><a href="#one">One</a></p></li><li><p>Two<br>Three</p></li></ul></div>',
|
||||
'<div class="foo"><ul><li><p><a href="#one">One</a></p></li><li><p>Two<br>Three</p></li></ul></div>',
|
||||
'HTML.DIV({"class": "foo"}, HTML.UL(HTML.LI(HTML.P(HTML.A({href: "#one"}, "One"))), HTML.LI(HTML.P("Two", HTML.BR(), "Three"))))');
|
||||
|
||||
|
||||
// Test nully attributes
|
||||
run(BR({x: null,
|
||||
y: [[], []],
|
||||
a: [['']]}),
|
||||
'<br a="">',
|
||||
'<br a="">',
|
||||
'HTML.BR({a: [[""]]})');
|
||||
|
||||
run(BR({
|
||||
x: function () { return Blaze.View(function () { return Blaze.View(function () { return []; }); }); },
|
||||
a: function () { return Blaze.View(function () { return Blaze.View(function () { return ''; }); }); }}),
|
||||
'<br a="">',
|
||||
'<br a="">');
|
||||
});
|
||||
|
||||
// test that we correctly update the 'value' property on input fields
|
||||
// rather than the 'value' attribute. the 'value' attribute only sets
|
||||
// the initial value.
|
||||
Tinytest.add("blaze - render - input - value", function (test) {
|
||||
var R = ReactiveVar("hello");
|
||||
var div = document.createElement("DIV");
|
||||
materialize(INPUT({value: function () { return R.get(); }}), div);
|
||||
var inputEl = div.querySelector('input');
|
||||
test.equal(inputEl.value, "hello");
|
||||
inputEl.value = "goodbye";
|
||||
R.set("hola");
|
||||
Tracker.flush();
|
||||
test.equal(inputEl.value, "hola");
|
||||
});
|
||||
|
||||
// test that we correctly update the 'checked' property rather than
|
||||
// the 'checked' attribute on input fields of type 'checkbox'. the
|
||||
// 'checked' attribute only sets the initial value.
|
||||
Tinytest.add("blaze - render - input - checked", function (test) {
|
||||
var R = ReactiveVar(null);
|
||||
var div = document.createElement("DIV");
|
||||
materialize(INPUT({type: "checkbox", checked: function () { return R.get(); }}), div);
|
||||
var inputEl = div.querySelector('input');
|
||||
test.equal(inputEl.checked, false);
|
||||
inputEl.checked = true;
|
||||
|
||||
R.set("checked");
|
||||
Tracker.flush();
|
||||
R.set(null);
|
||||
Tracker.flush();
|
||||
test.equal(inputEl.checked, false);
|
||||
});
|
||||
|
||||
Tinytest.add("blaze - render - textarea", function (test) {
|
||||
var run = function (optNode, text, html, code) {
|
||||
if (typeof optNode === 'string') {
|
||||
// called with args (text, html, code)
|
||||
code = html;
|
||||
html = text;
|
||||
text = optNode;
|
||||
optNode = null;
|
||||
}
|
||||
var div = document.createElement("DIV");
|
||||
var node = TEXTAREA({value: optNode || text});
|
||||
materialize(node, div);
|
||||
|
||||
var value = div.querySelector('textarea').value;
|
||||
value = value.replace(/\r\n/g, "\n"); // IE8 substitutes \n with \r\n
|
||||
test.equal(value, text);
|
||||
|
||||
test.equal(toHTML(node), html);
|
||||
if (typeof code === 'string')
|
||||
test.equal(toCode(node), code);
|
||||
};
|
||||
|
||||
run('Hello',
|
||||
'<textarea>Hello</textarea>',
|
||||
'HTML.TEXTAREA({value: "Hello"})');
|
||||
|
||||
run('\nHello',
|
||||
'<textarea>\n\nHello</textarea>',
|
||||
'HTML.TEXTAREA({value: "\\nHello"})');
|
||||
|
||||
run('</textarea>',
|
||||
'<textarea></textarea></textarea>',
|
||||
'HTML.TEXTAREA({value: "</textarea>"})');
|
||||
|
||||
run(CharRef({html: '&', str: '&'}),
|
||||
'&',
|
||||
'<textarea>&</textarea>',
|
||||
'HTML.TEXTAREA({value: HTML.CharRef({html: "&", str: "&"})})');
|
||||
|
||||
run(function () {
|
||||
return ['a', Blaze.View(function () { return 'b'; }), 'c'];
|
||||
},
|
||||
'abc',
|
||||
'<textarea>abc</textarea>');
|
||||
|
||||
// test that reactivity of textarea "value" attribute works...
|
||||
(function () {
|
||||
var R = ReactiveVar('one');
|
||||
var div = document.createElement("DIV");
|
||||
var node = TEXTAREA({value: function () {
|
||||
return Blaze.View(function () {
|
||||
return R.get();
|
||||
});
|
||||
}});
|
||||
materialize(node, div);
|
||||
var textarea = div.querySelector('textarea');
|
||||
test.equal(textarea.value, 'one');
|
||||
R.set('two');
|
||||
Tracker.flush();
|
||||
test.equal(textarea.value, 'two');
|
||||
})();
|
||||
|
||||
// ... while "content" reactivity simply doesn't update
|
||||
// (but doesn't throw either)
|
||||
(function () {
|
||||
var R = ReactiveVar('one');
|
||||
var div = document.createElement("DIV");
|
||||
var node = TEXTAREA([Blaze.View(function () {
|
||||
return R.get();
|
||||
})]);
|
||||
materialize(node, div);
|
||||
var textarea = div.querySelector('textarea');
|
||||
test.equal(textarea.value, 'one');
|
||||
R.set('two');
|
||||
Tracker.flush({_throwFirstError: true});
|
||||
test.equal(textarea.value, 'one');
|
||||
})();
|
||||
});
|
||||
|
||||
Tinytest.add("blaze - render - view isolation", function (test) {
|
||||
|
||||
// Reactively change a text node
|
||||
(function () {
|
||||
var R = ReactiveVar('Hello');
|
||||
var test1 = function () {
|
||||
return P(Blaze.View(function () { return R.get(); }));
|
||||
};
|
||||
|
||||
test.equal(toHTML(test1()), '<p>Hello</p>');
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
materialize(test1, div);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "<p>Hello</p>");
|
||||
|
||||
R.set('World');
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "<p>World</p>");
|
||||
})();
|
||||
|
||||
// Reactively change an array of text nodes
|
||||
(function () {
|
||||
var R = ReactiveVar(['Hello', ' World']);
|
||||
var test1 = function () {
|
||||
return P(Blaze.View(function () { return R.get(); }));
|
||||
};
|
||||
|
||||
test.equal(toHTML(test1()), '<p>Hello World</p>');
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
materialize(test1, div);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "<p>Hello World</p>");
|
||||
|
||||
R.set(['Goodbye', ' World']);
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "<p>Goodbye World</p>");
|
||||
})();
|
||||
|
||||
});
|
||||
|
||||
// IE strips malformed styles like "bar::d" from the `style`
|
||||
// attribute. We detect this to adjust expectations for the StyleHandler
|
||||
// test below.
|
||||
var malformedStylesAllowed = function () {
|
||||
var div = document.createElement("div");
|
||||
div.setAttribute("style", "bar::d;");
|
||||
return (div.getAttribute("style") === "bar::d;");
|
||||
};
|
||||
|
||||
Tinytest.add("blaze - render - view GC", function (test) {
|
||||
// test that removing parent element removes listeners and stops autoruns.
|
||||
(function () {
|
||||
var R = ReactiveVar('Hello');
|
||||
var test1 = P(Blaze.View(function () { return R.get(); }));
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
materialize(test1, div);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "<p>Hello</p>");
|
||||
|
||||
R.set('World');
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "<p>World</p>");
|
||||
|
||||
test.equal(R._numListeners(), 1);
|
||||
|
||||
$(div).remove();
|
||||
|
||||
test.equal(R._numListeners(), 0);
|
||||
|
||||
R.set('Steve');
|
||||
Tracker.flush();
|
||||
// should not have changed:
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "<p>World</p>");
|
||||
})();
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("blaze - render - reactive attributes", function (test) {
|
||||
(function () {
|
||||
var R = ReactiveVar({'class': ['david gre', CharRef({html: 'ë', str: '\u00eb'}), 'nspan'],
|
||||
id: 'foo'});
|
||||
|
||||
var spanFunc = function () {
|
||||
return SPAN(HTML.Attrs(
|
||||
function () { return R.get(); }));
|
||||
};
|
||||
|
||||
test.equal(Blaze.toHTML(spanFunc()),
|
||||
'<span class="david greënspan" id="foo"></span>');
|
||||
|
||||
test.equal(R._numListeners(), 0);
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
Blaze.render(spanFunc, div);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span class="david gre\u00ebnspan" id="foo"></span>');
|
||||
|
||||
test.equal(R._numListeners(), 1);
|
||||
|
||||
var span = div.firstChild;
|
||||
test.equal(span.nodeName, 'SPAN');
|
||||
span.className += ' blah'; // change the element's class outside of Blaze. this simulates what a jQuery could do
|
||||
|
||||
R.set({'class': 'david smith', id: 'bar'});
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span class="david blah smith" id="bar"></span>');
|
||||
test.equal(R._numListeners(), 1);
|
||||
|
||||
R.set({});
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span class="blah"></span>');
|
||||
test.equal(R._numListeners(), 1);
|
||||
|
||||
$(div).remove();
|
||||
|
||||
test.equal(R._numListeners(), 0);
|
||||
})();
|
||||
|
||||
// Test styles.
|
||||
(function () {
|
||||
// Test the case where there is a semicolon in the css attribute.
|
||||
var R = ReactiveVar({'style': 'foo: "a;aa"; bar: b;',
|
||||
id: 'foo'});
|
||||
|
||||
var spanFunc = function () {
|
||||
return SPAN(HTML.Attrs(function () { return R.get(); }));
|
||||
};
|
||||
|
||||
test.equal(Blaze.toHTML(spanFunc()), '<span style="foo: "a;aa"; bar: b;" id="foo"></span>');
|
||||
|
||||
test.equal(R._numListeners(), 0);
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
Blaze.render(spanFunc, div);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span id="foo" style="foo: "a;aa"; bar: b"></span>');
|
||||
|
||||
test.equal(R._numListeners(), 1);
|
||||
var span = div.firstChild;
|
||||
test.equal(span.nodeName, 'SPAN');
|
||||
|
||||
span.setAttribute('style', span.getAttribute('style') + '; jquery-style: hidden');
|
||||
|
||||
R.set({'style': 'foo: "a;zz;aa";', id: 'bar'});
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML, true), '<span id="bar" style="foo: "a;zz;aa"; jquery-style: hidden"></span>');
|
||||
test.equal(R._numListeners(), 1);
|
||||
|
||||
R.set({});
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span style="jquery-style: hidden"></span>');
|
||||
test.equal(R._numListeners(), 1);
|
||||
|
||||
$(div).remove();
|
||||
|
||||
test.equal(R._numListeners(), 0);
|
||||
})();
|
||||
|
||||
// Test that identical styles are successfully overwritten.
|
||||
(function () {
|
||||
|
||||
var R = ReactiveVar({'style': 'foo: a;'});
|
||||
|
||||
var spanFunc = function () {
|
||||
return SPAN(HTML.Attrs(function () { return R.get(); }));
|
||||
};
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
document.body.appendChild(div);
|
||||
Blaze.render(spanFunc, div);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: a"></span>');
|
||||
|
||||
var span = div.firstChild;
|
||||
test.equal(span.nodeName, 'SPAN');
|
||||
span.setAttribute("style", 'foo: b;');
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: b"></span>');
|
||||
|
||||
R.set({'style': 'foo: c;'});
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: c"></span>');
|
||||
|
||||
// test malformed styles - different expectations in IE (which
|
||||
// strips malformed styles) from other browsers
|
||||
R.set({'style': 'foo: a; bar::d;:e; baz: c;'});
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML),
|
||||
malformedStylesAllowed() ?
|
||||
'<span style="foo: a; bar::d; baz: c"></span>' :
|
||||
'<span style="foo: a; baz: c"></span>');
|
||||
|
||||
// Test strange styles
|
||||
R.set({'style': ' foo: c; constructor: a; __proto__: b;'});
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: c; constructor: a; __proto__: b"></span>');
|
||||
|
||||
R.set({});
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span></span>');
|
||||
|
||||
R.set({'style': 'foo: bar;'});
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: bar"></span>');
|
||||
})();
|
||||
|
||||
// Test `null`, `undefined`, and `[]` attributes
|
||||
(function () {
|
||||
var R = ReactiveVar({id: 'foo',
|
||||
aaa: null,
|
||||
bbb: undefined,
|
||||
ccc: [],
|
||||
ddd: [null],
|
||||
eee: [undefined],
|
||||
fff: [[]],
|
||||
ggg: ['x', ['y', ['z']]]});
|
||||
|
||||
var spanFunc = function () {
|
||||
return SPAN(HTML.Attrs(
|
||||
function () { return R.get(); }));
|
||||
};
|
||||
|
||||
test.equal(Blaze.toHTML(spanFunc()), '<span id="foo" ggg="xyz"></span>');
|
||||
test.equal(toCode(SPAN(R.get())),
|
||||
'HTML.SPAN({id: "foo", ggg: ["x", ["y", ["z"]]]})');
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
Blaze.render(spanFunc, div);
|
||||
var span = div.firstChild;
|
||||
test.equal(span.nodeName, 'SPAN');
|
||||
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span ggg="xyz" id="foo"></span>');
|
||||
R.set({id: 'foo', ggg: [[], [], []]});
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span id="foo"></span>');
|
||||
|
||||
R.set({id: 'foo', ggg: null});
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span id="foo"></span>');
|
||||
|
||||
R.set({id: 'foo', ggg: ''});
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span ggg="" id="foo"></span>');
|
||||
|
||||
$(div).remove();
|
||||
|
||||
test.equal(R._numListeners(), 0);
|
||||
})();
|
||||
});
|
||||
|
||||
Tinytest.add("blaze - render - templates and views", function (test) {
|
||||
(function () {
|
||||
var counter = 1;
|
||||
var buf = [];
|
||||
|
||||
var myTemplate = Blaze.Template(
|
||||
'myTemplate',
|
||||
function () {
|
||||
return [String(this.number),
|
||||
(this.number < 3 ? makeView() : HR())];
|
||||
});
|
||||
|
||||
myTemplate.constructView = function (number) {
|
||||
var view = Template.prototype.constructView.call(this);
|
||||
view.number = number;
|
||||
return view;
|
||||
};
|
||||
|
||||
myTemplate.created = function () {
|
||||
test.isFalse(Tracker.active);
|
||||
var view = this.view;
|
||||
var parent = Blaze.getView(view, 'myTemplate');
|
||||
if (parent) {
|
||||
buf.push('parent of ' + view.number + ' is ' +
|
||||
parent.number);
|
||||
}
|
||||
|
||||
buf.push('created ' + Template.currentData());
|
||||
};
|
||||
|
||||
myTemplate.onRendered(function () {
|
||||
test.isFalse(Tracker.active);
|
||||
var nodeDescr = function (node) {
|
||||
if (node.nodeType === 8) // comment
|
||||
return '';
|
||||
if (node.nodeType === 3) // text
|
||||
return node.nodeValue;
|
||||
|
||||
return node.nodeName;
|
||||
};
|
||||
|
||||
var view = this.view;
|
||||
var start = view.firstNode();
|
||||
var end = view.lastNode();
|
||||
// skip marker nodes
|
||||
while (start !== end && ! nodeDescr(start))
|
||||
start = start.nextSibling;
|
||||
while (end !== start && ! nodeDescr(end))
|
||||
end = end.previousSibling;
|
||||
|
||||
buf.push('dom-' + Template.currentData() +
|
||||
' is ' + nodeDescr(start) +'..' +
|
||||
nodeDescr(end));
|
||||
});
|
||||
|
||||
myTemplate.onDestroyed(function () {
|
||||
test.isFalse(Tracker.active);
|
||||
buf.push('destroyed ' + Template.currentData());
|
||||
});
|
||||
|
||||
var makeView = function () {
|
||||
var number = counter++;
|
||||
return Blaze.With(number, function () {
|
||||
return myTemplate.constructView(number);
|
||||
});
|
||||
};
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
|
||||
Blaze.render(makeView, div);
|
||||
buf.push('---flush---');
|
||||
Tracker.flush();
|
||||
test.equal(buf, ['created 1',
|
||||
'parent of 2 is 1',
|
||||
'created 2',
|
||||
'parent of 3 is 2',
|
||||
'created 3',
|
||||
'---flush---',
|
||||
// (proper order for these has not be thought out:)
|
||||
'dom-3 is 3..HR',
|
||||
'dom-2 is 2..HR',
|
||||
'dom-1 is 1..HR']);
|
||||
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '123<hr>');
|
||||
|
||||
buf.length = 0;
|
||||
$(div).remove();
|
||||
buf.sort();
|
||||
test.equal(buf, ['destroyed 1', 'destroyed 2', 'destroyed 3']);
|
||||
|
||||
// Now use toHTML. Should still get most of the callbacks (not `rendered`).
|
||||
|
||||
buf.length = 0;
|
||||
counter = 1;
|
||||
|
||||
var html = Blaze.toHTML(makeView());
|
||||
|
||||
test.equal(buf, ['created 1',
|
||||
'parent of 2 is 1',
|
||||
'created 2',
|
||||
'parent of 3 is 2',
|
||||
'created 3',
|
||||
'destroyed 3',
|
||||
'destroyed 2',
|
||||
'destroyed 1']);
|
||||
|
||||
test.equal(html, '123<hr>');
|
||||
})();
|
||||
});
|
||||
|
||||
Tinytest.add("blaze - render - findAll", function (test) {
|
||||
var found = null;
|
||||
var $found = null;
|
||||
|
||||
var myTemplate = new Template(
|
||||
'findAllTest',
|
||||
function() {
|
||||
return DIV([P('first'), P('second')]);
|
||||
});
|
||||
myTemplate.rendered = function() {
|
||||
found = this.findAll('p');
|
||||
$found = this.$('p');
|
||||
};
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
|
||||
Blaze.render(myTemplate, div);
|
||||
Tracker.flush();
|
||||
|
||||
test.equal(_.isArray(found), true);
|
||||
test.equal(_.isArray($found), false);
|
||||
test.equal(found.length, 2);
|
||||
test.equal($found.length, 2);
|
||||
});
|
||||
|
||||
Tinytest.add("blaze - render - reactive attributes 2", function (test) {
|
||||
var R1 = ReactiveVar(['foo']);
|
||||
var R2 = ReactiveVar(['bar']);
|
||||
|
||||
var spanFunc = function () {
|
||||
return SPAN(HTML.Attrs(
|
||||
{ blah: function () { return R1.get(); } },
|
||||
function () { return { blah: R2.get() }; }));
|
||||
};
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
Blaze.render(spanFunc, div);
|
||||
var check = function (expected) {
|
||||
test.equal(Blaze.toHTML(spanFunc()), expected);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), expected);
|
||||
};
|
||||
check('<span blah="bar"></span>');
|
||||
|
||||
test.equal(R1._numListeners(), 1);
|
||||
test.equal(R2._numListeners(), 1);
|
||||
|
||||
R2.set([[]]);
|
||||
Tracker.flush();
|
||||
// We combine `['foo']` with what evaluates to `[[[]]]`, which is nully.
|
||||
check('<span blah="foo"></span>');
|
||||
|
||||
R2.set([['']]);
|
||||
Tracker.flush();
|
||||
// We combine `['foo']` with what evaluates to `[[['']]]`, which is non-nully.
|
||||
check('<span blah=""></span>');
|
||||
|
||||
R2.set(null);
|
||||
Tracker.flush();
|
||||
// We combine `['foo']` with `[null]`, which is nully.
|
||||
check('<span blah="foo"></span>');
|
||||
|
||||
R1.set([[], []]);
|
||||
Tracker.flush();
|
||||
// We combine two nully values.
|
||||
check('<span></span>');
|
||||
|
||||
R1.set([[], ['foo']]);
|
||||
Tracker.flush();
|
||||
check('<span blah="foo"></span>');
|
||||
|
||||
// clean up
|
||||
|
||||
$(div).remove();
|
||||
|
||||
test.equal(R1._numListeners(), 0);
|
||||
test.equal(R2._numListeners(), 0);
|
||||
});
|
||||
|
||||
Tinytest.add("blaze - render - SVG", function (test) {
|
||||
if (! document.createElementNS) {
|
||||
// IE 8
|
||||
return;
|
||||
}
|
||||
|
||||
var fillColor = ReactiveVar('red');
|
||||
var classes = ReactiveVar('one two');
|
||||
|
||||
var content = DIV({'class': 'container'}, HTML.SVG(
|
||||
{width: 100, height: 100},
|
||||
HTML.CIRCLE({cx: 50, cy: 50, r: 40,
|
||||
stroke: 'black', 'stroke-width': 3,
|
||||
'class': function () { return classes.get(); },
|
||||
fill: function () { return fillColor.get(); }})));
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
materialize(content, div);
|
||||
|
||||
var circle = div.querySelector('.container > svg > circle');
|
||||
test.equal(circle.getAttribute('fill'), 'red');
|
||||
test.equal(circle.className.baseVal, 'one two');
|
||||
|
||||
fillColor.set('green');
|
||||
classes.set('two three');
|
||||
Tracker.flush();
|
||||
test.equal(circle.getAttribute('fill'), 'green');
|
||||
test.equal(circle.className.baseVal, 'two three');
|
||||
|
||||
test.equal(circle.nodeName, 'circle');
|
||||
test.equal(circle.namespaceURI, "http://www.w3.org/2000/svg");
|
||||
test.equal(circle.parentNode.namespaceURI, "http://www.w3.org/2000/svg");
|
||||
});
|
||||
|
||||
Tinytest.add("ui - 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')),
|
||||
'<span title="M&Ms">M&M candies</span>');
|
||||
});
|
||||
@@ -1,565 +0,0 @@
|
||||
// [new] Blaze.Template([viewName], renderFunction)
|
||||
//
|
||||
// `Blaze.Template` is the class of templates, like `Template.foo` in
|
||||
// Meteor, which is `instanceof Template`.
|
||||
//
|
||||
// `viewKind` is a string that looks like "Template.foo" for templates
|
||||
// defined by the compiler.
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @summary Constructor for a Template, which is used to construct Views with particular name and content.
|
||||
* @locus Client
|
||||
* @param {String} [viewName] Optional. A name for Views constructed by this Template. See [`view.name`](#view_name).
|
||||
* @param {Function} renderFunction A function that returns [*renderable content*](#renderable_content). This function is used as the `renderFunction` for Views constructed by this Template.
|
||||
*/
|
||||
Blaze.Template = function (viewName, renderFunction) {
|
||||
if (! (this instanceof Blaze.Template))
|
||||
// called without `new`
|
||||
return new Blaze.Template(viewName, renderFunction);
|
||||
|
||||
if (typeof viewName === 'function') {
|
||||
// omitted "viewName" argument
|
||||
renderFunction = viewName;
|
||||
viewName = '';
|
||||
}
|
||||
if (typeof viewName !== 'string')
|
||||
throw new Error("viewName must be a String (or omitted)");
|
||||
if (typeof renderFunction !== 'function')
|
||||
throw new Error("renderFunction must be a function");
|
||||
|
||||
this.viewName = viewName;
|
||||
this.renderFunction = renderFunction;
|
||||
|
||||
this.__helpers = new HelperMap;
|
||||
this.__eventMaps = [];
|
||||
|
||||
this._callbacks = {
|
||||
created: [],
|
||||
rendered: [],
|
||||
destroyed: []
|
||||
};
|
||||
};
|
||||
var Template = Blaze.Template;
|
||||
|
||||
var HelperMap = function () {};
|
||||
HelperMap.prototype.get = function (name) {
|
||||
return this[' '+name];
|
||||
};
|
||||
HelperMap.prototype.set = function (name, helper) {
|
||||
this[' '+name] = helper;
|
||||
};
|
||||
HelperMap.prototype.has = function (name) {
|
||||
return (' '+name) in this;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Returns true if `value` is a template object like `Template.myTemplate`.
|
||||
* @locus Client
|
||||
* @param {Any} value The value to test.
|
||||
*/
|
||||
Blaze.isTemplate = function (t) {
|
||||
return (t instanceof Blaze.Template);
|
||||
};
|
||||
|
||||
/**
|
||||
* @name onCreated
|
||||
* @instance
|
||||
* @memberOf Template
|
||||
* @summary Register a function to be called when an instance of this template is created.
|
||||
* @param {Function} callback A function to be added as a callback.
|
||||
* @locus Client
|
||||
* @importFromPackage templating
|
||||
*/
|
||||
Template.prototype.onCreated = function (cb) {
|
||||
this._callbacks.created.push(cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* @name onRendered
|
||||
* @instance
|
||||
* @memberOf Template
|
||||
* @summary Register a function to be called when an instance of this template is inserted into the DOM.
|
||||
* @param {Function} callback A function to be added as a callback.
|
||||
* @locus Client
|
||||
* @importFromPackage templating
|
||||
*/
|
||||
Template.prototype.onRendered = function (cb) {
|
||||
this._callbacks.rendered.push(cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* @name onDestroyed
|
||||
* @instance
|
||||
* @memberOf Template
|
||||
* @summary Register a function to be called when an instance of this template is removed from the DOM and destroyed.
|
||||
* @param {Function} callback A function to be added as a callback.
|
||||
* @locus Client
|
||||
* @importFromPackage templating
|
||||
*/
|
||||
Template.prototype.onDestroyed = function (cb) {
|
||||
this._callbacks.destroyed.push(cb);
|
||||
};
|
||||
|
||||
Template.prototype._getCallbacks = function (which) {
|
||||
var self = this;
|
||||
var callbacks = self[which] ? [self[which]] : [];
|
||||
// Fire all callbacks added with the new API (Template.onRendered())
|
||||
// as well as the old-style callback (e.g. Template.rendered) for
|
||||
// backwards-compatibility.
|
||||
callbacks = callbacks.concat(self._callbacks[which]);
|
||||
return callbacks;
|
||||
};
|
||||
|
||||
var fireCallbacks = function (callbacks, template) {
|
||||
Template._withTemplateInstanceFunc(
|
||||
function () { return template; },
|
||||
function () {
|
||||
for (var i = 0, N = callbacks.length; i < N; i++) {
|
||||
callbacks[i].call(template);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Template.prototype.constructView = function (contentFunc, elseFunc) {
|
||||
var self = this;
|
||||
var view = Blaze.View(self.viewName, self.renderFunction);
|
||||
view.template = self;
|
||||
|
||||
view.templateContentBlock = (
|
||||
contentFunc ? new Template('(contentBlock)', contentFunc) : null);
|
||||
view.templateElseBlock = (
|
||||
elseFunc ? new Template('(elseBlock)', elseFunc) : null);
|
||||
|
||||
if (self.__eventMaps || typeof self.events === 'object') {
|
||||
view._onViewRendered(function () {
|
||||
if (view.renderCount !== 1)
|
||||
return;
|
||||
|
||||
if (! self.__eventMaps.length && typeof self.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(self, self.events);
|
||||
}
|
||||
|
||||
_.each(self.__eventMaps, function (m) {
|
||||
Blaze._addEventMap(view, m, view);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
view._templateInstance = new Blaze.TemplateInstance(view);
|
||||
view.templateInstance = function () {
|
||||
// Update data, firstNode, and lastNode, and return the TemplateInstance
|
||||
// object.
|
||||
var inst = view._templateInstance;
|
||||
|
||||
/**
|
||||
* @instance
|
||||
* @memberOf Blaze.TemplateInstance
|
||||
* @name data
|
||||
* @summary The data context of this instance's latest invocation.
|
||||
* @locus Client
|
||||
*/
|
||||
inst.data = Blaze.getData(view);
|
||||
|
||||
if (view._domrange && !view.isDestroyed) {
|
||||
inst.firstNode = view._domrange.firstNode();
|
||||
inst.lastNode = view._domrange.lastNode();
|
||||
} else {
|
||||
// on 'created' or 'destroyed' callbacks we don't have a DomRange
|
||||
inst.firstNode = null;
|
||||
inst.lastNode = null;
|
||||
}
|
||||
|
||||
return inst;
|
||||
};
|
||||
|
||||
/**
|
||||
* @name created
|
||||
* @instance
|
||||
* @memberOf Template
|
||||
* @summary Provide a callback when an instance of a template is created.
|
||||
* @locus Client
|
||||
* @deprecated in 1.1
|
||||
*/
|
||||
// To avoid situations when new callbacks are added in between view
|
||||
// instantiation and event being fired, decide on all callbacks to fire
|
||||
// immediately and then fire them on the event.
|
||||
var createdCallbacks = self._getCallbacks('created');
|
||||
view.onViewCreated(function () {
|
||||
fireCallbacks(createdCallbacks, view.templateInstance());
|
||||
});
|
||||
|
||||
/**
|
||||
* @name rendered
|
||||
* @instance
|
||||
* @memberOf Template
|
||||
* @summary Provide a callback when an instance of a template is rendered.
|
||||
* @locus Client
|
||||
* @deprecated in 1.1
|
||||
*/
|
||||
var renderedCallbacks = self._getCallbacks('rendered');
|
||||
view.onViewReady(function () {
|
||||
fireCallbacks(renderedCallbacks, view.templateInstance());
|
||||
});
|
||||
|
||||
/**
|
||||
* @name destroyed
|
||||
* @instance
|
||||
* @memberOf Template
|
||||
* @summary Provide a callback when an instance of a template is destroyed.
|
||||
* @locus Client
|
||||
* @deprecated in 1.1
|
||||
*/
|
||||
var destroyedCallbacks = self._getCallbacks('destroyed');
|
||||
view.onViewDestroyed(function () {
|
||||
fireCallbacks(destroyedCallbacks, view.templateInstance());
|
||||
});
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @summary The class for template instances
|
||||
* @param {Blaze.View} view
|
||||
* @instanceName template
|
||||
*/
|
||||
Blaze.TemplateInstance = function (view) {
|
||||
if (! (this instanceof Blaze.TemplateInstance))
|
||||
// called without `new`
|
||||
return new Blaze.TemplateInstance(view);
|
||||
|
||||
if (! (view instanceof Blaze.View))
|
||||
throw new Error("View required");
|
||||
|
||||
view._templateInstance = this;
|
||||
|
||||
/**
|
||||
* @name view
|
||||
* @memberOf Blaze.TemplateInstance
|
||||
* @instance
|
||||
* @summary The [View](#blaze_view) object for this invocation of the template.
|
||||
* @locus Client
|
||||
* @type {Blaze.View}
|
||||
*/
|
||||
this.view = view;
|
||||
this.data = null;
|
||||
|
||||
/**
|
||||
* @name firstNode
|
||||
* @memberOf Blaze.TemplateInstance
|
||||
* @instance
|
||||
* @summary The first top-level DOM node in this template instance.
|
||||
* @locus Client
|
||||
* @type {DOMNode}
|
||||
*/
|
||||
this.firstNode = null;
|
||||
|
||||
/**
|
||||
* @name lastNode
|
||||
* @memberOf Blaze.TemplateInstance
|
||||
* @instance
|
||||
* @summary The last top-level DOM node in this template instance.
|
||||
* @locus Client
|
||||
* @type {DOMNode}
|
||||
*/
|
||||
this.lastNode = null;
|
||||
|
||||
// This dependency is used to identify state transitions in
|
||||
// _subscriptionHandles which could cause the result of
|
||||
// TemplateInstance#subscriptionsReady to change. Basically this is triggered
|
||||
// whenever a new subscription handle is added or when a subscription handle
|
||||
// is removed and they are not ready.
|
||||
this._allSubsReadyDep = new Tracker.Dependency();
|
||||
this._allSubsReady = false;
|
||||
|
||||
this._subscriptionHandles = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Find all elements matching `selector` in this template instance, and return them as a JQuery object.
|
||||
* @locus Client
|
||||
* @param {String} selector The CSS selector to match, scoped to the template contents.
|
||||
* @returns {DOMNode[]}
|
||||
*/
|
||||
Blaze.TemplateInstance.prototype.$ = function (selector) {
|
||||
var view = this.view;
|
||||
if (! view._domrange)
|
||||
throw new Error("Can't use $ on template instance with no DOM");
|
||||
return view._domrange.$(selector);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Find all elements matching `selector` in this template instance.
|
||||
* @locus Client
|
||||
* @param {String} selector The CSS selector to match, scoped to the template contents.
|
||||
* @returns {DOMElement[]}
|
||||
*/
|
||||
Blaze.TemplateInstance.prototype.findAll = function (selector) {
|
||||
return Array.prototype.slice.call(this.$(selector));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Find one element matching `selector` in this template instance.
|
||||
* @locus Client
|
||||
* @param {String} selector The CSS selector to match, scoped to the template contents.
|
||||
* @returns {DOMElement}
|
||||
*/
|
||||
Blaze.TemplateInstance.prototype.find = function (selector) {
|
||||
var result = this.$(selector);
|
||||
return result[0] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary A version of [Tracker.autorun](#tracker_autorun) that is stopped when the template is destroyed.
|
||||
* @locus Client
|
||||
* @param {Function} runFunc The function to run. It receives one argument: a Tracker.Computation object.
|
||||
*/
|
||||
Blaze.TemplateInstance.prototype.autorun = function (f) {
|
||||
return this.view.autorun(f);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary A version of [Meteor.subscribe](#meteor_subscribe) that is stopped
|
||||
* when the template is destroyed.
|
||||
* @return {SubscriptionHandle} The subscription handle to the newly made
|
||||
* subscription. Call `handle.stop()` to manually stop the subscription, or
|
||||
* `handle.ready()` to find out if this particular subscription has loaded all
|
||||
* of its inital data.
|
||||
* @locus Client
|
||||
* @param {String} name Name of the subscription. Matches the name of the
|
||||
* server's `publish()` call.
|
||||
* @param {Any} [arg1,arg2...] Optional arguments passed to publisher function
|
||||
* on server.
|
||||
* @param {Function|Object} [options] If a function is passed instead of an
|
||||
* object, it is interpreted as an `onReady` callback.
|
||||
* @param {Function} [options.onReady] Passed to [`Meteor.subscribe`](#meteor_subscribe).
|
||||
* @param {Function} [options.onStop] Passed to [`Meteor.subscribe`](#meteor_subscribe).
|
||||
* @param {DDP.Connection} [options.connection] The connection on which to make the
|
||||
* subscription.
|
||||
*/
|
||||
Blaze.TemplateInstance.prototype.subscribe = function (/* arguments */) {
|
||||
var self = this;
|
||||
|
||||
var subHandles = self._subscriptionHandles;
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
// Duplicate logic from Meteor.subscribe
|
||||
var options = {};
|
||||
if (args.length) {
|
||||
var lastParam = _.last(args);
|
||||
|
||||
// Match pattern to check if the last arg is an options argument
|
||||
var lastParamOptionsPattern = {
|
||||
onReady: Match.Optional(Function),
|
||||
// XXX COMPAT WITH 1.0.3.1 onError used to exist, but now we use
|
||||
// onStop with an error callback instead.
|
||||
onError: Match.Optional(Function),
|
||||
onStop: Match.Optional(Function),
|
||||
connection: Match.Optional(Match.Any)
|
||||
};
|
||||
|
||||
if (_.isFunction(lastParam)) {
|
||||
options.onReady = args.pop();
|
||||
} else if (lastParam && ! _.isEmpty(lastParam) && Match.test(lastParam, lastParamOptionsPattern)) {
|
||||
options = args.pop();
|
||||
}
|
||||
}
|
||||
|
||||
var subHandle;
|
||||
var oldStopped = options.onStop;
|
||||
options.onStop = function (error) {
|
||||
// When the subscription is stopped, remove it from the set of tracked
|
||||
// subscriptions to avoid this list growing without bound
|
||||
delete subHandles[subHandle.subscriptionId];
|
||||
|
||||
// Removing a subscription can only change the result of subscriptionsReady
|
||||
// if we are not ready (that subscription could be the one blocking us being
|
||||
// ready).
|
||||
if (! self._allSubsReady) {
|
||||
self._allSubsReadyDep.changed();
|
||||
}
|
||||
|
||||
if (oldStopped) {
|
||||
oldStopped(error);
|
||||
}
|
||||
};
|
||||
|
||||
var connection = options.connection;
|
||||
var callbacks = _.pick(options, ["onReady", "onError", "onStop"]);
|
||||
|
||||
// The callbacks are passed as the last item in the arguments array passed to
|
||||
// View#subscribe
|
||||
args.push(callbacks);
|
||||
|
||||
// View#subscribe takes the connection as one of the options in the last
|
||||
// argument
|
||||
subHandle = self.view.subscribe.call(self.view, args, {
|
||||
connection: connection
|
||||
});
|
||||
|
||||
if (! _.has(subHandles, subHandle.subscriptionId)) {
|
||||
subHandles[subHandle.subscriptionId] = subHandle;
|
||||
|
||||
// Adding a new subscription will always cause us to transition from ready
|
||||
// to not ready, but if we are already not ready then this can't make us
|
||||
// ready.
|
||||
if (self._allSubsReady) {
|
||||
self._allSubsReadyDep.changed();
|
||||
}
|
||||
}
|
||||
|
||||
return subHandle;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary A reactive function that returns true when all of the subscriptions
|
||||
* called with [this.subscribe](#TemplateInstance-subscribe) are ready.
|
||||
* @return {Boolean} True if all subscriptions on this template instance are
|
||||
* ready.
|
||||
*/
|
||||
Blaze.TemplateInstance.prototype.subscriptionsReady = function () {
|
||||
this._allSubsReadyDep.depend();
|
||||
|
||||
this._allSubsReady = _.all(this._subscriptionHandles, function (handle) {
|
||||
return handle.ready();
|
||||
});
|
||||
|
||||
return this._allSubsReady;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Specify template helpers available to this template.
|
||||
* @locus Client
|
||||
* @param {Object} helpers Dictionary of helper functions by name.
|
||||
* @importFromPackage templating
|
||||
*/
|
||||
Template.prototype.helpers = function (dict) {
|
||||
if (! _.isObject(dict)) {
|
||||
throw new Error("Helpers dictionary has to be an object");
|
||||
}
|
||||
|
||||
for (var k in dict)
|
||||
this.__helpers.set(k, dict[k]);
|
||||
};
|
||||
|
||||
// Kind of like Blaze.currentView but for the template instance.
|
||||
// This is a function, not a value -- so that not all helpers
|
||||
// are implicitly dependent on the current template instance's `data` property,
|
||||
// which would make them dependenct on the data context of the template
|
||||
// inclusion.
|
||||
Template._currentTemplateInstanceFunc = null;
|
||||
|
||||
Template._withTemplateInstanceFunc = function (templateInstanceFunc, func) {
|
||||
if (typeof func !== 'function')
|
||||
throw new Error("Expected function, got: " + func);
|
||||
var oldTmplInstanceFunc = Template._currentTemplateInstanceFunc;
|
||||
try {
|
||||
Template._currentTemplateInstanceFunc = templateInstanceFunc;
|
||||
return func();
|
||||
} finally {
|
||||
Template._currentTemplateInstanceFunc = oldTmplInstanceFunc;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Specify event handlers for this template.
|
||||
* @locus Client
|
||||
* @param {EventMap} eventMap Event handlers to associate with this template.
|
||||
* @importFromPackage templating
|
||||
*/
|
||||
Template.prototype.events = function (eventMap) {
|
||||
if (! _.isObject(eventMap)) {
|
||||
throw new Error("Event map has to be an object");
|
||||
}
|
||||
|
||||
var template = this;
|
||||
var eventMap2 = {};
|
||||
for (var k in eventMap) {
|
||||
eventMap2[k] = (function (k, v) {
|
||||
return function (event/*, ...*/) {
|
||||
var view = this; // passed by EventAugmenter
|
||||
var data = Blaze.getData(event.currentTarget);
|
||||
if (data == null)
|
||||
data = {};
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var tmplInstanceFunc = _.bind(view.templateInstance, view);
|
||||
args.splice(1, 0, tmplInstanceFunc());
|
||||
|
||||
return Template._withTemplateInstanceFunc(tmplInstanceFunc, function () {
|
||||
return v.apply(data, args);
|
||||
});
|
||||
};
|
||||
})(k, eventMap[k]);
|
||||
}
|
||||
|
||||
template.__eventMaps.push(eventMap2);
|
||||
};
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @name instance
|
||||
* @memberOf Template
|
||||
* @summary The [template instance](#template_inst) corresponding to the current template helper, event handler, callback, or autorun. If there isn't one, `null`.
|
||||
* @locus Client
|
||||
* @returns {Blaze.TemplateInstance}
|
||||
* @importFromPackage templating
|
||||
*/
|
||||
Template.instance = function () {
|
||||
return Template._currentTemplateInstanceFunc
|
||||
&& Template._currentTemplateInstanceFunc();
|
||||
};
|
||||
|
||||
// Note: Template.currentData() is documented to take zero arguments,
|
||||
// while Blaze.getData takes up to one.
|
||||
|
||||
/**
|
||||
* @summary
|
||||
*
|
||||
* - Inside an `onCreated`, `onRendered`, or `onDestroyed` callback, returns
|
||||
* the data context of the template.
|
||||
* - Inside an event handler, returns the data context of the template on which
|
||||
* this event handler was defined.
|
||||
* - Inside a helper, returns the data context of the DOM node where the helper
|
||||
* was used.
|
||||
*
|
||||
* Establishes a reactive dependency on the result.
|
||||
* @locus Client
|
||||
* @function
|
||||
* @importFromPackage templating
|
||||
*/
|
||||
Template.currentData = Blaze.getData;
|
||||
|
||||
/**
|
||||
* @summary Accesses other data contexts that enclose the current data context.
|
||||
* @locus Client
|
||||
* @function
|
||||
* @param {Integer} [numLevels] The number of levels beyond the current data context to look. Defaults to 1.
|
||||
* @importFromPackage templating
|
||||
*/
|
||||
Template.parentData = Blaze._parentData;
|
||||
|
||||
/**
|
||||
* @summary Defines a [helper function](#template_helpers) which can be used from all templates.
|
||||
* @locus Client
|
||||
* @function
|
||||
* @param {String} name The name of the helper function you are defining.
|
||||
* @param {Function} function The helper function itself.
|
||||
* @importFromPackage templating
|
||||
*/
|
||||
Template.registerHelper = Blaze.registerHelper;
|
||||
|
||||
/**
|
||||
* @summary Removes a global [helper function](#template_helpers).
|
||||
* @locus Client
|
||||
* @function
|
||||
* @param {String} name The name of the helper function you are defining.
|
||||
* @importFromPackage templating
|
||||
*/
|
||||
Template.deregisterHelper = Blaze.deregisterHelper;
|
||||
@@ -1,900 +0,0 @@
|
||||
/// [new] Blaze.View([name], renderMethod)
|
||||
///
|
||||
/// Blaze.View is the building block of reactive DOM. Views have
|
||||
/// the following features:
|
||||
///
|
||||
/// * lifecycle callbacks - Views are created, rendered, and destroyed,
|
||||
/// and callbacks can be registered to fire when these things happen.
|
||||
///
|
||||
/// * parent pointer - A View points to its parentView, which is the
|
||||
/// View that caused it to be rendered. These pointers form a
|
||||
/// hierarchy or tree of Views.
|
||||
///
|
||||
/// * render() method - A View's render() method specifies the DOM
|
||||
/// (or HTML) content of the View. If the method establishes
|
||||
/// reactive dependencies, it may be re-run.
|
||||
///
|
||||
/// * a DOMRange - If a View is rendered to DOM, its position and
|
||||
/// extent in the DOM are tracked using a DOMRange object.
|
||||
///
|
||||
/// When a View is constructed by calling Blaze.View, the View is
|
||||
/// not yet considered "created." It doesn't have a parentView yet,
|
||||
/// and no logic has been run to initialize the View. All real
|
||||
/// work is deferred until at least creation time, when the onViewCreated
|
||||
/// callbacks are fired, which happens when the View is "used" in
|
||||
/// some way that requires it to be rendered.
|
||||
///
|
||||
/// ...more lifecycle stuff
|
||||
///
|
||||
/// `name` is an optional string tag identifying the View. The only
|
||||
/// time it's used is when looking in the View tree for a View of a
|
||||
/// particular name; for example, data contexts are stored on Views
|
||||
/// of name "with". Names are also useful when debugging, so in
|
||||
/// general it's good for functions that create Views to set the name.
|
||||
/// Views associated with templates have names of the form "Template.foo".
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @summary Constructor for a View, which represents a reactive region of DOM.
|
||||
* @locus Client
|
||||
* @param {String} [name] Optional. A name for this type of View. See [`view.name`](#view_name).
|
||||
* @param {Function} renderFunction A function that returns [*renderable content*](#renderable_content). In this function, `this` is bound to the View.
|
||||
*/
|
||||
Blaze.View = function (name, render) {
|
||||
if (! (this instanceof Blaze.View))
|
||||
// called without `new`
|
||||
return new Blaze.View(name, render);
|
||||
|
||||
if (typeof name === 'function') {
|
||||
// omitted "name" argument
|
||||
render = name;
|
||||
name = '';
|
||||
}
|
||||
this.name = name;
|
||||
this._render = render;
|
||||
|
||||
this._callbacks = {
|
||||
created: null,
|
||||
rendered: null,
|
||||
destroyed: null
|
||||
};
|
||||
|
||||
// Setting all properties here is good for readability,
|
||||
// and also may help Chrome optimize the code by keeping
|
||||
// the View object from changing shape too much.
|
||||
this.isCreated = false;
|
||||
this._isCreatedForExpansion = false;
|
||||
this.isRendered = false;
|
||||
this._isAttached = false;
|
||||
this.isDestroyed = false;
|
||||
this._isInRender = false;
|
||||
this.parentView = null;
|
||||
this._domrange = null;
|
||||
// This flag is normally set to false except for the cases when view's parent
|
||||
// was generated as part of expanding some syntactic sugar expressions or
|
||||
// methods.
|
||||
// Ex.: Blaze.renderWithData is an equivalent to creating a view with regular
|
||||
// Blaze.render and wrapping it into {{#with data}}{{/with}} view. Since the
|
||||
// users don't know anything about these generated parent views, Blaze needs
|
||||
// this information to be available on views to make smarter decisions. For
|
||||
// example: removing the generated parent view with the view on Blaze.remove.
|
||||
this._hasGeneratedParent = false;
|
||||
// Bindings accessible to children views (via view.lookup('name')) within the
|
||||
// closest template view.
|
||||
this._scopeBindings = {};
|
||||
|
||||
this.renderCount = 0;
|
||||
};
|
||||
|
||||
Blaze.View.prototype._render = function () { return null; };
|
||||
|
||||
Blaze.View.prototype.onViewCreated = function (cb) {
|
||||
this._callbacks.created = this._callbacks.created || [];
|
||||
this._callbacks.created.push(cb);
|
||||
};
|
||||
|
||||
Blaze.View.prototype._onViewRendered = function (cb) {
|
||||
this._callbacks.rendered = this._callbacks.rendered || [];
|
||||
this._callbacks.rendered.push(cb);
|
||||
};
|
||||
|
||||
Blaze.View.prototype.onViewReady = function (cb) {
|
||||
var self = this;
|
||||
var fire = function () {
|
||||
Tracker.afterFlush(function () {
|
||||
if (! self.isDestroyed) {
|
||||
Blaze._withCurrentView(self, function () {
|
||||
cb.call(self);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
self._onViewRendered(function onViewRendered() {
|
||||
if (self.isDestroyed)
|
||||
return;
|
||||
if (! self._domrange.attached)
|
||||
self._domrange.onAttached(fire);
|
||||
else
|
||||
fire();
|
||||
});
|
||||
};
|
||||
|
||||
Blaze.View.prototype.onViewDestroyed = function (cb) {
|
||||
this._callbacks.destroyed = this._callbacks.destroyed || [];
|
||||
this._callbacks.destroyed.push(cb);
|
||||
};
|
||||
Blaze.View.prototype.removeViewDestroyedListener = function (cb) {
|
||||
var destroyed = this._callbacks.destroyed;
|
||||
if (! destroyed)
|
||||
return;
|
||||
var index = _.lastIndexOf(destroyed, cb);
|
||||
if (index !== -1) {
|
||||
// XXX You'd think the right thing to do would be splice, but _fireCallbacks
|
||||
// gets sad if you remove callbacks while iterating over the list. Should
|
||||
// change this to use callback-hook or EventEmitter or something else that
|
||||
// properly supports removal.
|
||||
destroyed[index] = null;
|
||||
}
|
||||
};
|
||||
|
||||
/// View#autorun(func)
|
||||
///
|
||||
/// Sets up a Tracker autorun that is "scoped" to this View in two
|
||||
/// important ways: 1) Blaze.currentView is automatically set
|
||||
/// on every re-run, and 2) the autorun is stopped when the
|
||||
/// View is destroyed. As with Tracker.autorun, the first run of
|
||||
/// the function is immediate, and a Computation object that can
|
||||
/// be used to stop the autorun is returned.
|
||||
///
|
||||
/// View#autorun is meant to be called from View callbacks like
|
||||
/// onViewCreated, or from outside the rendering process. It may not
|
||||
/// be called before the onViewCreated callbacks are fired (too early),
|
||||
/// or from a render() method (too confusing).
|
||||
///
|
||||
/// Typically, autoruns that update the state
|
||||
/// of the View (as in Blaze.With) should be started from an onViewCreated
|
||||
/// callback. Autoruns that update the DOM should be started
|
||||
/// from either onViewCreated (guarded against the absence of
|
||||
/// view._domrange), or onViewReady.
|
||||
Blaze.View.prototype.autorun = function (f, _inViewScope, displayName) {
|
||||
var self = this;
|
||||
|
||||
// The restrictions on when View#autorun can be called are in order
|
||||
// to avoid bad patterns, like creating a Blaze.View and immediately
|
||||
// calling autorun on it. A freshly created View is not ready to
|
||||
// have logic run on it; it doesn't have a parentView, for example.
|
||||
// It's when the View is materialized or expanded that the onViewCreated
|
||||
// handlers are fired and the View starts up.
|
||||
//
|
||||
// Letting the render() method call `this.autorun()` is problematic
|
||||
// because of re-render. The best we can do is to stop the old
|
||||
// autorun and start a new one for each render, but that's a pattern
|
||||
// we try to avoid internally because it leads to helpers being
|
||||
// called extra times, in the case where the autorun causes the
|
||||
// view to re-render (and thus the autorun to be torn down and a
|
||||
// new one established).
|
||||
//
|
||||
// We could lift these restrictions in various ways. One interesting
|
||||
// idea is to allow you to call `view.autorun` after instantiating
|
||||
// `view`, and automatically wrap it in `view.onViewCreated`, deferring
|
||||
// the autorun so that it starts at an appropriate time. However,
|
||||
// then we can't return the Computation object to the caller, because
|
||||
// it doesn't exist yet.
|
||||
if (! self.isCreated) {
|
||||
throw new Error("View#autorun must be called from the created callback at the earliest");
|
||||
}
|
||||
if (this._isInRender) {
|
||||
throw new Error("Can't call View#autorun from inside render(); try calling it from the created or rendered callback");
|
||||
}
|
||||
if (Tracker.active) {
|
||||
throw new Error("Can't call View#autorun from a Tracker Computation; try calling it from the created or rendered callback");
|
||||
}
|
||||
|
||||
var templateInstanceFunc = Blaze.Template._currentTemplateInstanceFunc;
|
||||
|
||||
var func = function viewAutorun(c) {
|
||||
return Blaze._withCurrentView(_inViewScope || self, function () {
|
||||
return Blaze.Template._withTemplateInstanceFunc(
|
||||
templateInstanceFunc, function () {
|
||||
return f.call(self, c);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Give the autorun function a better name for debugging and profiling.
|
||||
// The `displayName` property is not part of the spec but browsers like Chrome
|
||||
// and Firefox prefer it in debuggers over the name function was declared by.
|
||||
func.displayName =
|
||||
(self.name || 'anonymous') + ':' + (displayName || 'anonymous');
|
||||
var comp = Tracker.autorun(func);
|
||||
|
||||
var stopComputation = function () { comp.stop(); };
|
||||
self.onViewDestroyed(stopComputation);
|
||||
comp.onStop(function () {
|
||||
self.removeViewDestroyedListener(stopComputation);
|
||||
});
|
||||
|
||||
return comp;
|
||||
};
|
||||
|
||||
Blaze.View.prototype._errorIfShouldntCallSubscribe = function () {
|
||||
var self = this;
|
||||
|
||||
if (! self.isCreated) {
|
||||
throw new Error("View#subscribe must be called from the created callback at the earliest");
|
||||
}
|
||||
if (self._isInRender) {
|
||||
throw new Error("Can't call View#subscribe from inside render(); try calling it from the created or rendered callback");
|
||||
}
|
||||
if (self.isDestroyed) {
|
||||
throw new Error("Can't call View#subscribe from inside the destroyed callback, try calling it inside created or rendered.");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Just like Blaze.View#autorun, but with Meteor.subscribe instead of
|
||||
* Tracker.autorun. Stop the subscription when the view is destroyed.
|
||||
* @return {SubscriptionHandle} A handle to the subscription so that you can
|
||||
* see if it is ready, or stop it manually
|
||||
*/
|
||||
Blaze.View.prototype.subscribe = function (args, options) {
|
||||
var self = this;
|
||||
options = options || {};
|
||||
|
||||
self._errorIfShouldntCallSubscribe();
|
||||
|
||||
var subHandle;
|
||||
if (options.connection) {
|
||||
subHandle = options.connection.subscribe.apply(options.connection, args);
|
||||
} else {
|
||||
subHandle = Meteor.subscribe.apply(Meteor, args);
|
||||
}
|
||||
|
||||
self.onViewDestroyed(function () {
|
||||
subHandle.stop();
|
||||
});
|
||||
|
||||
return subHandle;
|
||||
};
|
||||
|
||||
Blaze.View.prototype.firstNode = function () {
|
||||
if (! this._isAttached)
|
||||
throw new Error("View must be attached before accessing its DOM");
|
||||
|
||||
return this._domrange.firstNode();
|
||||
};
|
||||
|
||||
Blaze.View.prototype.lastNode = function () {
|
||||
if (! this._isAttached)
|
||||
throw new Error("View must be attached before accessing its DOM");
|
||||
|
||||
return this._domrange.lastNode();
|
||||
};
|
||||
|
||||
Blaze._fireCallbacks = function (view, which) {
|
||||
Blaze._withCurrentView(view, function () {
|
||||
Tracker.nonreactive(function fireCallbacks() {
|
||||
var cbs = view._callbacks[which];
|
||||
for (var i = 0, N = (cbs && cbs.length); i < N; i++)
|
||||
cbs[i] && cbs[i].call(view);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Blaze._createView = function (view, parentView, forExpansion) {
|
||||
if (view.isCreated)
|
||||
throw new Error("Can't render the same View twice");
|
||||
|
||||
view.parentView = (parentView || null);
|
||||
view.isCreated = true;
|
||||
if (forExpansion)
|
||||
view._isCreatedForExpansion = true;
|
||||
|
||||
Blaze._fireCallbacks(view, 'created');
|
||||
};
|
||||
|
||||
var doFirstRender = function (view, initialContent) {
|
||||
var domrange = new Blaze._DOMRange(initialContent);
|
||||
view._domrange = domrange;
|
||||
domrange.view = view;
|
||||
view.isRendered = true;
|
||||
Blaze._fireCallbacks(view, 'rendered');
|
||||
|
||||
var teardownHook = null;
|
||||
|
||||
domrange.onAttached(function attached(range, element) {
|
||||
view._isAttached = true;
|
||||
|
||||
teardownHook = Blaze._DOMBackend.Teardown.onElementTeardown(
|
||||
element, function teardown() {
|
||||
Blaze._destroyView(view, true /* _skipNodes */);
|
||||
});
|
||||
});
|
||||
|
||||
// tear down the teardown hook
|
||||
view.onViewDestroyed(function () {
|
||||
teardownHook && teardownHook.stop();
|
||||
teardownHook = null;
|
||||
});
|
||||
|
||||
return domrange;
|
||||
};
|
||||
|
||||
// Take an uncreated View `view` and create and render it to DOM,
|
||||
// setting up the autorun that updates the View. Returns a new
|
||||
// DOMRange, which has been associated with the View.
|
||||
//
|
||||
// The private arguments `_workStack` and `_intoArray` are passed in
|
||||
// by Blaze._materializeDOM and are only present for recursive calls
|
||||
// (when there is some other _materializeView on the stack). If
|
||||
// provided, then we avoid the mutual recursion of calling back into
|
||||
// Blaze._materializeDOM so that deep View hierarchies don't blow the
|
||||
// stack. Instead, we push tasks onto workStack for the initial
|
||||
// rendering and subsequent setup of the View, and they are done after
|
||||
// we return. When there is a _workStack, we do not return the new
|
||||
// DOMRange, but instead push it into _intoArray from a _workStack
|
||||
// task.
|
||||
Blaze._materializeView = function (view, parentView, _workStack, _intoArray) {
|
||||
Blaze._createView(view, parentView);
|
||||
|
||||
var domrange;
|
||||
var lastHtmljs;
|
||||
// We don't expect to be called in a Computation, but just in case,
|
||||
// wrap in Tracker.nonreactive.
|
||||
Tracker.nonreactive(function () {
|
||||
view.autorun(function doRender(c) {
|
||||
// `view.autorun` sets the current view.
|
||||
view.renderCount++;
|
||||
view._isInRender = true;
|
||||
// Any dependencies that should invalidate this Computation come
|
||||
// from this line:
|
||||
var htmljs = view._render();
|
||||
view._isInRender = false;
|
||||
|
||||
if (! c.firstRun) {
|
||||
Tracker.nonreactive(function doMaterialize() {
|
||||
// re-render
|
||||
var rangesAndNodes = Blaze._materializeDOM(htmljs, [], view);
|
||||
if (! Blaze._isContentEqual(lastHtmljs, htmljs)) {
|
||||
domrange.setMembers(rangesAndNodes);
|
||||
Blaze._fireCallbacks(view, 'rendered');
|
||||
}
|
||||
});
|
||||
}
|
||||
lastHtmljs = htmljs;
|
||||
|
||||
// Causes any nested views to stop immediately, not when we call
|
||||
// `setMembers` the next time around the autorun. Otherwise,
|
||||
// helpers in the DOM tree to be replaced might be scheduled
|
||||
// to re-run before we have a chance to stop them.
|
||||
Tracker.onInvalidate(function () {
|
||||
if (domrange) {
|
||||
domrange.destroyMembers();
|
||||
}
|
||||
});
|
||||
}, undefined, 'materialize');
|
||||
|
||||
// first render. lastHtmljs is the first htmljs.
|
||||
var initialContents;
|
||||
if (! _workStack) {
|
||||
initialContents = Blaze._materializeDOM(lastHtmljs, [], view);
|
||||
domrange = doFirstRender(view, initialContents);
|
||||
initialContents = null; // help GC because we close over this scope a lot
|
||||
} else {
|
||||
// We're being called from Blaze._materializeDOM, so to avoid
|
||||
// recursion and save stack space, provide a description of the
|
||||
// work to be done instead of doing it. Tasks pushed onto
|
||||
// _workStack will be done in LIFO order after we return.
|
||||
// The work will still be done within a Tracker.nonreactive,
|
||||
// because it will be done by some call to Blaze._materializeDOM
|
||||
// (which is always called in a Tracker.nonreactive).
|
||||
initialContents = [];
|
||||
// push this function first so that it happens last
|
||||
_workStack.push(function () {
|
||||
domrange = doFirstRender(view, initialContents);
|
||||
initialContents = null; // help GC because of all the closures here
|
||||
_intoArray.push(domrange);
|
||||
});
|
||||
// now push the task that calculates initialContents
|
||||
_workStack.push(_.bind(Blaze._materializeDOM, null,
|
||||
lastHtmljs, initialContents, view, _workStack));
|
||||
}
|
||||
});
|
||||
|
||||
if (! _workStack) {
|
||||
return domrange;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Expands a View to HTMLjs, calling `render` recursively on all
|
||||
// Views and evaluating any dynamic attributes. Calls the `created`
|
||||
// callback, but not the `materialized` or `rendered` callbacks.
|
||||
// Destroys the view immediately, unless called in a Tracker Computation,
|
||||
// in which case the view will be destroyed when the Computation is
|
||||
// invalidated. If called in a Tracker Computation, the result is a
|
||||
// reactive string; that is, the Computation will be invalidated
|
||||
// if any changes are made to the view or subviews that might affect
|
||||
// the HTML.
|
||||
Blaze._expandView = function (view, parentView) {
|
||||
Blaze._createView(view, parentView, true /*forExpansion*/);
|
||||
|
||||
view._isInRender = true;
|
||||
var htmljs = Blaze._withCurrentView(view, function () {
|
||||
return view._render();
|
||||
});
|
||||
view._isInRender = false;
|
||||
|
||||
var result = Blaze._expand(htmljs, view);
|
||||
|
||||
if (Tracker.active) {
|
||||
Tracker.onInvalidate(function () {
|
||||
Blaze._destroyView(view);
|
||||
});
|
||||
} else {
|
||||
Blaze._destroyView(view);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Options: `parentView`
|
||||
Blaze._HTMLJSExpander = HTML.TransformingVisitor.extend();
|
||||
Blaze._HTMLJSExpander.def({
|
||||
visitObject: function (x) {
|
||||
if (x instanceof Blaze.Template)
|
||||
x = x.constructView();
|
||||
if (x instanceof Blaze.View)
|
||||
return Blaze._expandView(x, this.parentView);
|
||||
|
||||
// this will throw an error; other objects are not allowed!
|
||||
return HTML.TransformingVisitor.prototype.visitObject.call(this, x);
|
||||
},
|
||||
visitAttributes: function (attrs) {
|
||||
// expand dynamic attributes
|
||||
if (typeof attrs === 'function')
|
||||
attrs = Blaze._withCurrentView(this.parentView, attrs);
|
||||
|
||||
// call super (e.g. for case where `attrs` is an array)
|
||||
return HTML.TransformingVisitor.prototype.visitAttributes.call(this, attrs);
|
||||
},
|
||||
visitAttribute: function (name, value, tag) {
|
||||
// expand attribute values that are functions. Any attribute value
|
||||
// that contains Views must be wrapped in a function.
|
||||
if (typeof value === 'function')
|
||||
value = Blaze._withCurrentView(this.parentView, value);
|
||||
|
||||
return HTML.TransformingVisitor.prototype.visitAttribute.call(
|
||||
this, name, value, tag);
|
||||
}
|
||||
});
|
||||
|
||||
// Return Blaze.currentView, but only if it is being rendered
|
||||
// (i.e. we are in its render() method).
|
||||
var currentViewIfRendering = function () {
|
||||
var view = Blaze.currentView;
|
||||
return (view && view._isInRender) ? view : null;
|
||||
};
|
||||
|
||||
Blaze._expand = function (htmljs, parentView) {
|
||||
parentView = parentView || currentViewIfRendering();
|
||||
return (new Blaze._HTMLJSExpander(
|
||||
{parentView: parentView})).visit(htmljs);
|
||||
};
|
||||
|
||||
Blaze._expandAttributes = function (attrs, parentView) {
|
||||
parentView = parentView || currentViewIfRendering();
|
||||
return (new Blaze._HTMLJSExpander(
|
||||
{parentView: parentView})).visitAttributes(attrs);
|
||||
};
|
||||
|
||||
Blaze._destroyView = function (view, _skipNodes) {
|
||||
if (view.isDestroyed)
|
||||
return;
|
||||
view.isDestroyed = true;
|
||||
|
||||
Blaze._fireCallbacks(view, 'destroyed');
|
||||
|
||||
// Destroy views and elements recursively. If _skipNodes,
|
||||
// only recurse up to views, not elements, for the case where
|
||||
// the backend (jQuery) is recursing over the elements already.
|
||||
|
||||
if (view._domrange)
|
||||
view._domrange.destroyMembers(_skipNodes);
|
||||
};
|
||||
|
||||
Blaze._destroyNode = function (node) {
|
||||
if (node.nodeType === 1)
|
||||
Blaze._DOMBackend.Teardown.tearDownElement(node);
|
||||
};
|
||||
|
||||
// Are the HTMLjs entities `a` and `b` the same? We could be
|
||||
// more elaborate here but the point is to catch the most basic
|
||||
// cases.
|
||||
Blaze._isContentEqual = 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'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary The View corresponding to the current template helper, event handler, callback, or autorun. If there isn't one, `null`.
|
||||
* @locus Client
|
||||
* @type {Blaze.View}
|
||||
*/
|
||||
Blaze.currentView = null;
|
||||
|
||||
Blaze._withCurrentView = function (view, func) {
|
||||
var oldView = Blaze.currentView;
|
||||
try {
|
||||
Blaze.currentView = view;
|
||||
return func();
|
||||
} finally {
|
||||
Blaze.currentView = oldView;
|
||||
}
|
||||
};
|
||||
|
||||
// Blaze.render publicly takes a View or a Template.
|
||||
// Privately, it takes any HTMLJS (extended with Views and Templates)
|
||||
// except null or undefined, or a function that returns any extended
|
||||
// HTMLJS.
|
||||
var checkRenderContent = function (content) {
|
||||
if (content === null)
|
||||
throw new Error("Can't render null");
|
||||
if (typeof content === 'undefined')
|
||||
throw new Error("Can't render undefined");
|
||||
|
||||
if ((content instanceof Blaze.View) ||
|
||||
(content instanceof Blaze.Template) ||
|
||||
(typeof content === 'function'))
|
||||
return;
|
||||
|
||||
try {
|
||||
// Throw if content doesn't look like HTMLJS at the top level
|
||||
// (i.e. verify that this is an HTML.Tag, or an array,
|
||||
// or a primitive, etc.)
|
||||
(new HTML.Visitor).visit(content);
|
||||
} catch (e) {
|
||||
// Make error message suitable for public API
|
||||
throw new Error("Expected Template or View");
|
||||
}
|
||||
};
|
||||
|
||||
// For Blaze.render and Blaze.toHTML, take content and
|
||||
// wrap it in a View, unless it's a single View or
|
||||
// Template already.
|
||||
var contentAsView = function (content) {
|
||||
checkRenderContent(content);
|
||||
|
||||
if (content instanceof Blaze.Template) {
|
||||
return content.constructView();
|
||||
} else if (content instanceof Blaze.View) {
|
||||
return content;
|
||||
} else {
|
||||
var func = content;
|
||||
if (typeof func !== 'function') {
|
||||
func = function () {
|
||||
return content;
|
||||
};
|
||||
}
|
||||
return Blaze.View('render', func);
|
||||
}
|
||||
};
|
||||
|
||||
// For Blaze.renderWithData and Blaze.toHTMLWithData, wrap content
|
||||
// in a function, if necessary, so it can be a content arg to
|
||||
// a Blaze.With.
|
||||
var contentAsFunc = function (content) {
|
||||
checkRenderContent(content);
|
||||
|
||||
if (typeof content !== 'function') {
|
||||
return function () {
|
||||
return content;
|
||||
};
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Renders a template or View to DOM nodes and inserts it into the DOM, returning a rendered [View](#blaze_view) which can be passed to [`Blaze.remove`](#blaze_remove).
|
||||
* @locus Client
|
||||
* @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object to render. If a template, a View object is [constructed](#template_constructview). If a View, it must be an unrendered View, which becomes a rendered View and is returned.
|
||||
* @param {DOMNode} parentNode The node that will be the parent of the rendered template. It must be an Element node.
|
||||
* @param {DOMNode} [nextNode] Optional. If provided, must be a child of <em>parentNode</em>; the template will be inserted before this node. If not provided, the template will be inserted as the last child of parentNode.
|
||||
* @param {Blaze.View} [parentView] Optional. If provided, it will be set as the rendered View's [`parentView`](#view_parentview).
|
||||
*/
|
||||
Blaze.render = function (content, parentElement, nextNode, parentView) {
|
||||
if (! parentElement) {
|
||||
Blaze._warn("Blaze.render without a parent element is deprecated. " +
|
||||
"You must specify where to insert the rendered content.");
|
||||
}
|
||||
|
||||
if (nextNode instanceof Blaze.View) {
|
||||
// handle omitted nextNode
|
||||
parentView = nextNode;
|
||||
nextNode = null;
|
||||
}
|
||||
|
||||
// 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");
|
||||
|
||||
parentView = parentView || currentViewIfRendering();
|
||||
|
||||
var view = contentAsView(content);
|
||||
Blaze._materializeView(view, parentView);
|
||||
|
||||
if (parentElement) {
|
||||
view._domrange.attach(parentElement, nextNode);
|
||||
}
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
Blaze.insert = function (view, parentElement, nextNode) {
|
||||
Blaze._warn("Blaze.insert has been deprecated. Specify where to insert the " +
|
||||
"rendered content in the call to Blaze.render.");
|
||||
|
||||
if (! (view && (view._domrange instanceof Blaze._DOMRange)))
|
||||
throw new Error("Expected template rendered with Blaze.render");
|
||||
|
||||
view._domrange.attach(parentElement, nextNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Renders a template or View to DOM nodes with a data context. Otherwise identical to `Blaze.render`.
|
||||
* @locus Client
|
||||
* @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object to render.
|
||||
* @param {Object|Function} data The data context to use, or a function returning a data context. If a function is provided, it will be reactively re-run.
|
||||
* @param {DOMNode} parentNode The node that will be the parent of the rendered template. It must be an Element node.
|
||||
* @param {DOMNode} [nextNode] Optional. If provided, must be a child of <em>parentNode</em>; the template will be inserted before this node. If not provided, the template will be inserted as the last child of parentNode.
|
||||
* @param {Blaze.View} [parentView] Optional. If provided, it will be set as the rendered View's [`parentView`](#view_parentview).
|
||||
*/
|
||||
Blaze.renderWithData = function (content, data, parentElement, nextNode, parentView) {
|
||||
// We defer the handling of optional arguments to Blaze.render. At this point,
|
||||
// `nextNode` may actually be `parentView`.
|
||||
return Blaze.render(Blaze._TemplateWith(data, contentAsFunc(content)),
|
||||
parentElement, nextNode, parentView);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Removes a rendered View from the DOM, stopping all reactive updates and event listeners on it. Also destroys the Blaze.Template instance associated with the view.
|
||||
* @locus Client
|
||||
* @param {Blaze.View} renderedView The return value from `Blaze.render` or `Blaze.renderWithData`, or the `view` property of a Blaze.Template instance. Calling `Blaze.remove(Template.instance().view)` from within a template event handler will destroy the view as well as that template and trigger the template's `onDestroyed` handlers.
|
||||
*/
|
||||
Blaze.remove = function (view) {
|
||||
if (! (view && (view._domrange instanceof Blaze._DOMRange)))
|
||||
throw new Error("Expected template rendered with Blaze.render");
|
||||
|
||||
while (view) {
|
||||
if (! view.isDestroyed) {
|
||||
var range = view._domrange;
|
||||
if (range.attached && ! range.parentRange)
|
||||
range.detach();
|
||||
range.destroy();
|
||||
}
|
||||
|
||||
view = view._hasGeneratedParent && view.parentView;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Renders a template or View to a string of HTML.
|
||||
* @locus Client
|
||||
* @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object from which to generate HTML.
|
||||
*/
|
||||
Blaze.toHTML = function (content, parentView) {
|
||||
parentView = parentView || currentViewIfRendering();
|
||||
|
||||
return HTML.toHTML(Blaze._expandView(contentAsView(content), parentView));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Renders a template or View to HTML with a data context. Otherwise identical to `Blaze.toHTML`.
|
||||
* @locus Client
|
||||
* @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object from which to generate HTML.
|
||||
* @param {Object|Function} data The data context to use, or a function returning a data context.
|
||||
*/
|
||||
Blaze.toHTMLWithData = function (content, data, parentView) {
|
||||
parentView = parentView || currentViewIfRendering();
|
||||
|
||||
return HTML.toHTML(Blaze._expandView(Blaze._TemplateWith(
|
||||
data, contentAsFunc(content)), parentView));
|
||||
};
|
||||
|
||||
Blaze._toText = function (htmljs, parentView, textMode) {
|
||||
if (typeof htmljs === 'function')
|
||||
throw new Error("Blaze._toText doesn't take a function, just HTMLjs");
|
||||
|
||||
if ((parentView != null) && ! (parentView instanceof Blaze.View)) {
|
||||
// omitted parentView argument
|
||||
textMode = parentView;
|
||||
parentView = null;
|
||||
}
|
||||
parentView = parentView || currentViewIfRendering();
|
||||
|
||||
if (! textMode)
|
||||
throw new Error("textMode required");
|
||||
if (! (textMode === HTML.TEXTMODE.STRING ||
|
||||
textMode === HTML.TEXTMODE.RCDATA ||
|
||||
textMode === HTML.TEXTMODE.ATTRIBUTE))
|
||||
throw new Error("Unknown textMode: " + textMode);
|
||||
|
||||
return HTML.toText(Blaze._expand(htmljs, parentView), textMode);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Returns the current data context, or the data context that was used when rendering a particular DOM element or View from a Meteor template.
|
||||
* @locus Client
|
||||
* @param {DOMElement|Blaze.View} [elementOrView] Optional. An element that was rendered by a Meteor, or a View.
|
||||
*/
|
||||
Blaze.getData = function (elementOrView) {
|
||||
var theWith;
|
||||
|
||||
if (! elementOrView) {
|
||||
theWith = Blaze.getView('with');
|
||||
} else if (elementOrView instanceof Blaze.View) {
|
||||
var view = elementOrView;
|
||||
theWith = (view.name === 'with' ? view :
|
||||
Blaze.getView(view, 'with'));
|
||||
} else if (typeof elementOrView.nodeType === 'number') {
|
||||
if (elementOrView.nodeType !== 1)
|
||||
throw new Error("Expected DOM element");
|
||||
theWith = Blaze.getView(elementOrView, 'with');
|
||||
} else {
|
||||
throw new Error("Expected DOM element or View");
|
||||
}
|
||||
|
||||
return theWith ? theWith.dataVar.get() : null;
|
||||
};
|
||||
|
||||
// For back-compat
|
||||
Blaze.getElementData = function (element) {
|
||||
Blaze._warn("Blaze.getElementData has been deprecated. Use " +
|
||||
"Blaze.getData(element) instead.");
|
||||
|
||||
if (element.nodeType !== 1)
|
||||
throw new Error("Expected DOM element");
|
||||
|
||||
return Blaze.getData(element);
|
||||
};
|
||||
|
||||
// Both arguments are optional.
|
||||
|
||||
/**
|
||||
* @summary Gets either the current View, or the View enclosing the given DOM element.
|
||||
* @locus Client
|
||||
* @param {DOMElement} [element] Optional. If specified, the View enclosing `element` is returned.
|
||||
*/
|
||||
Blaze.getView = function (elementOrView, _viewName) {
|
||||
var viewName = _viewName;
|
||||
|
||||
if ((typeof elementOrView) === 'string') {
|
||||
// omitted elementOrView; viewName present
|
||||
viewName = elementOrView;
|
||||
elementOrView = null;
|
||||
}
|
||||
|
||||
// We could eventually shorten the code by folding the logic
|
||||
// from the other methods into this method.
|
||||
if (! elementOrView) {
|
||||
return Blaze._getCurrentView(viewName);
|
||||
} else if (elementOrView instanceof Blaze.View) {
|
||||
return Blaze._getParentView(elementOrView, viewName);
|
||||
} else if (typeof elementOrView.nodeType === 'number') {
|
||||
return Blaze._getElementView(elementOrView, viewName);
|
||||
} else {
|
||||
throw new Error("Expected DOM element or View");
|
||||
}
|
||||
};
|
||||
|
||||
// Gets the current view or its nearest ancestor of name
|
||||
// `name`.
|
||||
Blaze._getCurrentView = function (name) {
|
||||
var view = Blaze.currentView;
|
||||
// Better to fail in cases where it doesn't make sense
|
||||
// to use Blaze._getCurrentView(). There will be a current
|
||||
// view anywhere it does. You can check Blaze.currentView
|
||||
// if you want to know whether there is one or not.
|
||||
if (! view)
|
||||
throw new Error("There is no current view");
|
||||
|
||||
if (name) {
|
||||
while (view && view.name !== name)
|
||||
view = view.parentView;
|
||||
return view || null;
|
||||
} else {
|
||||
// Blaze._getCurrentView() with no arguments just returns
|
||||
// Blaze.currentView.
|
||||
return view;
|
||||
}
|
||||
};
|
||||
|
||||
Blaze._getParentView = function (view, name) {
|
||||
var v = view.parentView;
|
||||
|
||||
if (name) {
|
||||
while (v && v.name !== name)
|
||||
v = v.parentView;
|
||||
}
|
||||
|
||||
return v || null;
|
||||
};
|
||||
|
||||
Blaze._getElementView = function (elem, name) {
|
||||
var range = Blaze._DOMRange.forElement(elem);
|
||||
var view = null;
|
||||
while (range && ! view) {
|
||||
view = (range.view || null);
|
||||
if (! view) {
|
||||
if (range.parentRange)
|
||||
range = range.parentRange;
|
||||
else
|
||||
range = Blaze._DOMRange.forElement(range.parentElement);
|
||||
}
|
||||
}
|
||||
|
||||
if (name) {
|
||||
while (view && view.name !== name)
|
||||
view = view.parentView;
|
||||
return view || null;
|
||||
} else {
|
||||
return view;
|
||||
}
|
||||
};
|
||||
|
||||
Blaze._addEventMap = function (view, eventMap, thisInHandler) {
|
||||
thisInHandler = (thisInHandler || null);
|
||||
var handles = [];
|
||||
|
||||
if (! view._domrange)
|
||||
throw new Error("View must have a DOMRange");
|
||||
|
||||
view._domrange.onAttached(function attached_eventMaps(range, element) {
|
||||
_.each(eventMap, function (handler, spec) {
|
||||
var clauses = spec.split(/,\s+/);
|
||||
// iterate over clauses of spec, e.g. ['click .foo', 'click .bar']
|
||||
_.each(clauses, function (clause) {
|
||||
var parts = clause.split(/\s+/);
|
||||
if (parts.length === 0)
|
||||
return;
|
||||
|
||||
var newEvents = parts.shift();
|
||||
var selector = parts.join(' ');
|
||||
handles.push(Blaze._EventSupport.listen(
|
||||
element, newEvents, selector,
|
||||
function (evt) {
|
||||
if (! range.containsElement(evt.currentTarget))
|
||||
return null;
|
||||
var handlerThis = thisInHandler || this;
|
||||
var handlerArgs = arguments;
|
||||
return Blaze._withCurrentView(view, function () {
|
||||
return handler.apply(handlerThis, handlerArgs);
|
||||
});
|
||||
},
|
||||
range, function (r) {
|
||||
return r.parentRange;
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
view.onViewDestroyed(function () {
|
||||
_.each(handles, function (h) {
|
||||
h.stop();
|
||||
});
|
||||
handles.length = 0;
|
||||
});
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
if (Meteor.isClient) {
|
||||
|
||||
Tinytest.add("blaze - view - callbacks", function (test) {
|
||||
var R = ReactiveVar('foo');
|
||||
|
||||
var buf = '';
|
||||
|
||||
var v = Blaze.View(function () {
|
||||
return R.get();
|
||||
});
|
||||
|
||||
v.onViewCreated(function () {
|
||||
buf += 'c' + v.renderCount;
|
||||
});
|
||||
v._onViewRendered(function () {
|
||||
buf += 'r' + v.renderCount;
|
||||
});
|
||||
v.onViewReady(function () {
|
||||
buf += 'y' + v.renderCount;
|
||||
});
|
||||
v.onViewDestroyed(function () {
|
||||
buf += 'd' + v.renderCount;
|
||||
});
|
||||
|
||||
test.equal(buf, '');
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
test.isFalse(v.isRendered);
|
||||
test.isFalse(v._isAttached);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
test.throws(function () { v.firstNode(); }, /View must be attached/);
|
||||
test.throws(function () { v.lastNode(); }, /View must be attached/);
|
||||
Blaze.render(v, div);
|
||||
test.equal(buf, 'c0r1');
|
||||
test.equal(typeof (v.firstNode().nodeType), "number");
|
||||
test.equal(typeof (v.lastNode().nodeType), "number");
|
||||
test.isTrue(v.isRendered);
|
||||
test.isTrue(v._isAttached);
|
||||
test.equal(buf, 'c0r1');
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "foo");
|
||||
Tracker.flush();
|
||||
test.equal(buf, 'c0r1y1');
|
||||
|
||||
R.set("bar");
|
||||
Tracker.flush();
|
||||
test.equal(buf, 'c0r1y1r2y2');
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "bar");
|
||||
|
||||
Blaze.remove(v);
|
||||
test.equal(buf, 'c0r1y1r2y2d2');
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
|
||||
buf = "";
|
||||
R.set("baz");
|
||||
Tracker.flush();
|
||||
test.equal(buf, "");
|
||||
});
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
# caching-html-compiler
|
||||
|
||||
Provides a pluggable class used to compile HTML-style templates in Meteor build plugins. This abstracts out a lot of the functionality you would need to implement the following plugins:
|
||||
|
||||
1. `templating`
|
||||
2. `static-html`
|
||||
3. `simple:markdown-templating`
|
||||
|
||||
It provides automatic caching and handles communicating with the build plugin APIs. The actual functions that convert HTML into compiled form are passed in as arguments into the constructor, allowing those functions to be unit tested separately from the caching and file system functionality.
|
||||
|
||||
-------
|
||||
|
||||
### new CachingHtmlCompiler(name, tagScannerFunc, tagHandlerFunc)
|
||||
|
||||
Constructs a new CachingHtmlCompiler that can be passed into `Plugin.registerCompiler`.
|
||||
|
||||
#### Arguments
|
||||
|
||||
1. `name` The name of the compiler, used when printing errors. Should probably be the same as the name of the build plugin and package it is used in.
|
||||
2. `tagScannerFunc` A function that takes a string representing a template file as input, and returns an array of Tag objects. See the README for `templating-tools` for more information about the Tag object.
|
||||
3. `tagHandlerFunc` A function that takes an array of Tag objects (the output of the previous argument) and returns an object with `js`, `body`, `head`, and `bodyAttr` properties, which will be added to the app through the build plugin API.
|
||||
|
||||
#### Example
|
||||
|
||||
Here is some example code from the `templating` package:
|
||||
|
||||
```js
|
||||
Plugin.registerCompiler({
|
||||
extensions: ['html'],
|
||||
archMatching: 'web',
|
||||
isTemplate: true
|
||||
}, () => new CachingHtmlCompiler(
|
||||
"templating",
|
||||
TemplatingTools.scanHtmlForTags,
|
||||
TemplatingTools.compileTagsWithSpacebars
|
||||
));
|
||||
```
|
||||
@@ -1,141 +0,0 @@
|
||||
const path = Plugin.path;
|
||||
|
||||
// The CompileResult type for this CachingCompiler is the return value of
|
||||
// htmlScanner.scan: a {js, head, body, bodyAttrs} object.
|
||||
CachingHtmlCompiler = class CachingHtmlCompiler extends CachingCompiler {
|
||||
/**
|
||||
* Constructor for CachingHtmlCompiler
|
||||
* @param {String} name The name of the compiler, printed in errors -
|
||||
* should probably always be the same as the name of the build
|
||||
* plugin/package
|
||||
* @param {Function} tagScannerFunc Transforms a template file (commonly
|
||||
* .html) into an array of Tags
|
||||
* @param {Function} tagHandlerFunc Transforms an array of tags into a
|
||||
* results object with js, body, head, and bodyAttrs properties
|
||||
*/
|
||||
constructor(name, tagScannerFunc, tagHandlerFunc) {
|
||||
super({
|
||||
compilerName: name,
|
||||
defaultCacheSize: 1024*1024*10,
|
||||
});
|
||||
|
||||
this._bodyAttrInfo = null;
|
||||
|
||||
this.tagScannerFunc = tagScannerFunc;
|
||||
this.tagHandlerFunc = tagHandlerFunc;
|
||||
}
|
||||
|
||||
// Implements method from CachingCompilerBase
|
||||
compileResultSize(compileResult) {
|
||||
function lengthOrZero(field) {
|
||||
return field ? field.length : 0;
|
||||
}
|
||||
return lengthOrZero(compileResult.head) + lengthOrZero(compileResult.body) +
|
||||
lengthOrZero(compileResult.js);
|
||||
}
|
||||
|
||||
// Overrides method from CachingCompiler
|
||||
processFilesForTarget(inputFiles) {
|
||||
this._bodyAttrInfo = {};
|
||||
super.processFilesForTarget(inputFiles);
|
||||
}
|
||||
|
||||
// Implements method from CachingCompilerBase
|
||||
getCacheKey(inputFile) {
|
||||
// Note: the path is only used for errors, so it doesn't have to be part
|
||||
// of the cache key.
|
||||
return inputFile.getSourceHash();
|
||||
}
|
||||
|
||||
// Implements method from CachingCompiler
|
||||
compileOneFile(inputFile) {
|
||||
const contents = inputFile.getContentsAsString();
|
||||
const inputPath = inputFile.getPathInPackage();
|
||||
try {
|
||||
const tags = this.tagScannerFunc({
|
||||
sourceName: inputPath,
|
||||
contents: contents,
|
||||
tagNames: ["body", "head", "template"]
|
||||
});
|
||||
|
||||
return this.tagHandlerFunc(tags);
|
||||
} catch (e) {
|
||||
if (e instanceof TemplatingTools.CompileError) {
|
||||
inputFile.error({
|
||||
message: e.message,
|
||||
line: e.line
|
||||
});
|
||||
return null;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implements method from CachingCompilerBase
|
||||
addCompileResult(inputFile, compileResult) {
|
||||
let allJavaScript = "";
|
||||
|
||||
if (compileResult.head) {
|
||||
inputFile.addHtml({ section: "head", data: compileResult.head });
|
||||
}
|
||||
|
||||
if (compileResult.body) {
|
||||
inputFile.addHtml({ section: "body", data: compileResult.body });
|
||||
}
|
||||
|
||||
if (compileResult.js) {
|
||||
allJavaScript += compileResult.js;
|
||||
}
|
||||
|
||||
if (! _.isEmpty(compileResult.bodyAttrs)) {
|
||||
Object.keys(compileResult.bodyAttrs).forEach((attr) => {
|
||||
const value = compileResult.bodyAttrs[attr];
|
||||
if (this._bodyAttrInfo.hasOwnProperty(attr) &&
|
||||
this._bodyAttrInfo[attr].value !== value) {
|
||||
// two conflicting attributes on <body> tags in two different template
|
||||
// files
|
||||
inputFile.error({
|
||||
message:
|
||||
`<body> declarations have conflicting values for the '${ attr }' ` +
|
||||
`attribute in the following files: ` +
|
||||
this._bodyAttrInfo[attr].inputFile.getPathInPackage() +
|
||||
`, ${ inputFile.getPathInPackage() }`
|
||||
});
|
||||
} else {
|
||||
this._bodyAttrInfo[attr] = {inputFile, value};
|
||||
}
|
||||
});
|
||||
|
||||
// Add JavaScript code to set attributes on body
|
||||
allJavaScript +=
|
||||
`Meteor.startup(function() {
|
||||
var attrs = ${JSON.stringify(compileResult.bodyAttrs)};
|
||||
for (var prop in attrs) {
|
||||
document.body.setAttribute(prop, attrs[prop]);
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
if (allJavaScript) {
|
||||
const filePath = inputFile.getPathInPackage();
|
||||
// XXX this path manipulation may be unnecessarily complex
|
||||
let pathPart = path.dirname(filePath);
|
||||
if (pathPart === '.')
|
||||
pathPart = '';
|
||||
if (pathPart.length && pathPart !== path.sep)
|
||||
pathPart = pathPart + path.sep;
|
||||
const ext = path.extname(filePath);
|
||||
const basename = path.basename(filePath, ext);
|
||||
|
||||
// XXX generate a source map
|
||||
|
||||
inputFile.addJavaScript({
|
||||
path: path.join(pathPart, "template." + basename + ".js"),
|
||||
data: allJavaScript
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
Package.describe({
|
||||
version: '1.0.6',
|
||||
// Brief, one-line summary of the package.
|
||||
summary: 'Pluggable class for compiling HTML into templates',
|
||||
// By default, Meteor will default to using README.md for documentation.
|
||||
// To avoid submitting documentation, set this field to null.
|
||||
documentation: 'README.md'
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.use([
|
||||
'underscore',
|
||||
'caching-compiler',
|
||||
'templating-tools',
|
||||
'ecmascript'
|
||||
]);
|
||||
|
||||
api.addFiles('caching-html-compiler.js', 'server');
|
||||
|
||||
api.export("CachingHtmlCompiler", 'server');
|
||||
});
|
||||
1
packages/html-tools/.gitignore
vendored
1
packages/html-tools/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.build*
|
||||
@@ -1,139 +0,0 @@
|
||||
# html-tools
|
||||
|
||||
A lightweight HTML tokenizer and parser which outputs to the HTMLjs
|
||||
object representation. Special hooks allow the syntax to be extended
|
||||
to parse an HTML-like template language like Spacebars.
|
||||
|
||||
```
|
||||
HTMLTools.parseFragment("<div class=greeting>Hello<br>World</div>")
|
||||
|
||||
=> HTML.DIV({'class':'greeting'}, "Hello", HTML.BR(), "World"))
|
||||
```
|
||||
|
||||
This package is used by the Spacebars compiler, which normally only
|
||||
runs at bundle time but can also be used at runtime on the client or
|
||||
server.
|
||||
|
||||
## Invoking the Parser
|
||||
|
||||
`HTMLTools.parseFragment(input, options)` - Takes an input string or Scanner object and returns HTMLjs.
|
||||
|
||||
In the basic case, where no options are passed, `parseFragment` will consume the entire input (the full string or the rest of the Scanner).
|
||||
|
||||
The options are as follows:
|
||||
|
||||
#### getTemplateTag
|
||||
|
||||
This option extends the HTML parser to parse template tags such as `{{foo}}`.
|
||||
|
||||
`getTemplateTag: function (scanner, templateTagPosition) { ... }` - A function for the parser to call after every HTML token and at various positions within tags. If the function returns an instanceof `HTMLTools.TemplateTag`, it is inserted into the HTMLjs tree at the appropriate location. The constructor is `HTMLTools.TemplateTag(props)`, where props is an object whose properties are copied to the `TemplateTag` instance. You can also call the constructor with no arguments and assign whatever properties you want, or you can subclass `TemplateTag`.
|
||||
|
||||
There are four possible outcomes when `getTemplateTag` is called:
|
||||
|
||||
* Not a template tag - Leave the scanner as is, and return `null`. A quick peek at the next character should bail to this case if the start of a template tag is not seen.
|
||||
* Bad template tag - Call `scanner.fatal`, which aborts parsing completely. Once the beginning of a template tag is seen, `getTemplateTag` will generally want to commit, and either succeed or fail trying).
|
||||
* Good template tag - Advance the scanner to the end of the template tag and return an `HTMLTools.TemplateTag` object.
|
||||
* Comment tag - Advance the scanner and return `null`. For example, a Spacebars comment is `{{! foo}}`.
|
||||
|
||||
The `templateTagPosition` argument to `getTemplateTag` is one of:
|
||||
|
||||
* `HTMLTools.TEMPLATE_TAG_POSITION.ELEMENT` - At "element level," meaning somewhere an HTML tag could be.
|
||||
* `HTMLTools.TEMPLATE_TAG_POSITION.IN_START_TAG` - Inside a start tag, as in `<div {{foo}}>`, where you might otherwise find `name=value`.
|
||||
* `HTMLTools.TEMPLATE_TAG_POSITION.IN_ATTRIBUTE` - Inside the value of an HTML attribute, as in `<div class={{foo}}>`.
|
||||
* `HTMLTools.TEMPLATE_TAG_POSITION.IN_RCDATA` - Inside a TEXTAREA or a block helper inside an attribute, where character references are allowed ("replaced character data") but not tags.
|
||||
* `HTMLTools.TEMPLATE_TAG_POSITION.IN_RAWTEXT` - In a context where character references are not parsed, such as a script tag, style tag, or markdown helper.
|
||||
|
||||
It's completely normal for `getTemplateTag` to invoke `HTMLTools.parseFragment` recursively on the same scanner (see `shouldStop`). If it does so, the same value of `getTemplateTag` must be passed to the second invocation.
|
||||
|
||||
At the moment, template tags must begin with `{`. The parser does not try calling `getTemplateTag` for every character of an HTML document, only at token boundaries, and it knows to always end a token at `{`.
|
||||
|
||||
#### textMode
|
||||
|
||||
The `textMode` option, if present, causes the parser to parse text (such as the contents of a `<textarea>` tag or part of an attribute) instead of HTML. In a text mode, for example, the input `"<"` is not a parse error (because a bare `<` is allowed in a textarea or attribute).
|
||||
|
||||
The value of `textMode` must be one of:
|
||||
|
||||
* `HTML.TEXTMODE.RCDATA` - Interpret character references (the usual case)
|
||||
* `HTML.TEXTMODE.STRING` - Don't interpret character references (the RAWTEXT case)
|
||||
|
||||
#### shouldStop
|
||||
|
||||
`shouldStop: function (scanner) { ... }` - A function that the parser invokes between tokens to check whether it should stop parsing. The function should return a boolean value.
|
||||
|
||||
The `shouldStop` function provides a way to put a "wall" in the input stream for the purpose of parsing HTML content embedded in a template tag. For example, take the template `{{#if happy}}yay{{/if}}`. The scanner will be advanced to the start of the word `yay` before `parseFragment` is called to parse the contents of the tag. (Note that the caller happens to be the `getTemplateTag` function of an enclosing `parseFragment`.) When parsing from `yay`, the `shouldStop` function is used to end the fragment at `{{/if}}`, which, like `{{/blah}}` or `{{else}}`, couldn't possibly be actual content that belongs in the fragment. Even if HTML tags are not closed, as in the malformed template `{{#if foo}}<div>{{else}}`, the fragment stops at the `{{else}}`, and the error is an unclosed `<div>` (before the parser notices the unclosed `{{#if}}`).
|
||||
|
||||
## HTMLTools.Scanner class
|
||||
|
||||
To write `getTemplateTag` and `shouldStop` functions, you have to
|
||||
interface with the `HTMLTools.Scanner` class used by html-tools. It's a
|
||||
general class that could be used by any parser/lexer/tokenizer.
|
||||
|
||||
A Scanner has an immutable source document and a mutable pointer into
|
||||
the document.
|
||||
|
||||
* `new Scanner(input)` - constructs a Scanner with source string `input`
|
||||
* `scanner.input` (read-only) - the entire source string
|
||||
* `scanner.pos` (read/write) - the current index into the source string
|
||||
|
||||
Scanners provide these methods for convenience:
|
||||
|
||||
* `scanner.rest()` - `input.slice(pos)` (the rest of the document)
|
||||
* `scanner.peek()` - `input.charAt(pos)` (the next character)
|
||||
* `scanner.isEOF()` - true if `pos` is at or beyond the end of `input`
|
||||
* `scanner.fatal(msg)` - throw an error indicating a problem at `pos`
|
||||
|
||||
Even though `scanner.rest()` performs a substring operation, it should be considered fast and O(1), because all known JavaScript runtimes in use have constant-time substring. It would be possible, but extremely clumsy, to avoid such a substring operation while performing the usual business of a parser, which is to try to match a regex anchored at a particular index.
|
||||
|
||||
Functions that take scanners generally have three possible outcomes:
|
||||
|
||||
* Success: Advance `scanner.pos` and return some truthy value
|
||||
* Failure: Leave `scanner.pos` alone and return `null`
|
||||
* Fatal: Throw an exception via `scanner.fatal`
|
||||
|
||||
It's particularly important that in the Failure case, the function restores the scanner to the state it found it. This makes it possible to immediately try another parse function when one fails and form alternations such as `foo(scanner) || bar(scanner)`.
|
||||
|
||||
It's often easiest to avoid the Failure case altogether, writing parse functions that always succeed or throw. This requires less bookkeeping and leads to good error messages. A Failure case may be added if it is simple to check for up front and makes the function easier to use in an alternation. We say a function has "committed" or "will succeed or fail fatally trying" when it has reached a point where it must return a value or throw. Any parse function that has moved the scanner position and not remembered the original position is necessarily committed. Usually, committing is completely natural in the context of the language being parsed; for example, `{{` in a template always starts a template tag or throws an error about a malformed template tag.
|
||||
|
||||
## HTML Dialect
|
||||
|
||||
HTML has many dialects and potential degrees of permissiveness. We
|
||||
use the WHATWG syntax spec and are pretty strict, failing on any
|
||||
"parse error" cases, which basically means the input has to be
|
||||
valid "HTML5" (except for the template tags).
|
||||
|
||||
HTML syntax references:
|
||||
|
||||
* [Human-readable syntax guide](http://developers.whatwg.org/syntax.html#syntax)
|
||||
* [Tokenization state machine](http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html)
|
||||
|
||||
The WHATWG parser without error recovery is strict compared to
|
||||
browsers (which will recover from almost anything), but lenient
|
||||
compared to the now-defunct XHTML spec (which required lowercase tag
|
||||
names and lots more escaping of special characters).
|
||||
|
||||
The following are examples of **errors**:
|
||||
|
||||
* A stray or unclosed `<` character
|
||||
* An unknown character reference like `&asdf;`
|
||||
* Self-closing tags like `<div/>` (except for BR, HR, INPUT, and other "void" elements)
|
||||
* End tags for void elements (BR, HR, INPUT, etc.)
|
||||
* Missing end tags, in most cases (e.g. missing `</div>`)
|
||||
|
||||
The following are **permitted**:
|
||||
|
||||
* Bare `>` characters
|
||||
* Bare `&` that can't be confused with a character reference
|
||||
* Uppercase or lowercase tag and attribute names (case insensitive)
|
||||
* Unquoted and valueless attributes - `<input type=checkbox checked>`
|
||||
* Most characters in attribute values - `<img alt=x,y>`
|
||||
* Embedded SVG elements
|
||||
|
||||
Note: Currently you have to close your Ps, LIs, and other tags for
|
||||
which the spec allows the end tag to be omitted in many cases
|
||||
|
||||
## Character References
|
||||
|
||||
This package contains a lookup table for all known named character references in HTML, of which there are over 2,000, from `Á` (capital A, acute accent) to `‌` (zero-width non-joiner), as well as code for interpreting numeric character entities like `A`.
|
||||
|
||||
Since character references are parsed into `HTML.CharRef` objects which contain both the raw and interpreted form, we never have to convert between the forms except at parse time.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,114 +0,0 @@
|
||||
var Scanner = HTMLTools.Scanner;
|
||||
var getCharacterReference = HTMLTools.Parse.getCharacterReference;
|
||||
|
||||
Tinytest.add("html-tools - entities", function (test) {
|
||||
var succeed = function (input, match, codepoints) {
|
||||
if (typeof input === 'string')
|
||||
input = {input: input};
|
||||
|
||||
// match arg is optional; codepoints is never a string
|
||||
if (typeof match !== 'string') {
|
||||
codepoints = match;
|
||||
match = input.input;
|
||||
}
|
||||
|
||||
var scanner = new Scanner(input.input);
|
||||
var result = getCharacterReference(scanner, input.inAttribute, input.allowedChar);
|
||||
test.isTrue(result);
|
||||
test.equal(scanner.pos, match.length);
|
||||
test.equal(result, {
|
||||
t: 'CharRef',
|
||||
v: match,
|
||||
cp: _.map(codepoints,
|
||||
function (x) { return (typeof x === 'string' ?
|
||||
x.charCodeAt(0) : x); })
|
||||
});
|
||||
};
|
||||
|
||||
var ignore = function (input) {
|
||||
if (typeof input === 'string')
|
||||
input = {input: input};
|
||||
|
||||
var scanner = new Scanner(input.input);
|
||||
var result = getCharacterReference(scanner, input.inAttribute, input.allowedChar);
|
||||
test.isFalse(result);
|
||||
test.equal(scanner.pos, 0);
|
||||
};
|
||||
|
||||
var fatal = function (input, messageContains) {
|
||||
if (typeof input === 'string')
|
||||
input = {input: input};
|
||||
|
||||
var scanner = new Scanner(input.input);
|
||||
var error;
|
||||
try {
|
||||
getCharacterReference(scanner, input.inAttribute, input.allowedChar);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
test.isTrue(error);
|
||||
if (error)
|
||||
test.isTrue(messageContains && error.message.indexOf(messageContains) >= 0, error.message);
|
||||
};
|
||||
|
||||
ignore('a');
|
||||
ignore('&');
|
||||
ignore('&&');
|
||||
ignore('&\t');
|
||||
ignore('& ');
|
||||
fatal('&#', 'Invalid numerical character reference starting with &#');
|
||||
ignore('&a');
|
||||
fatal('&a;', 'Invalid character reference: &a;');
|
||||
ignore({input: '&"', allowedChar: '"'});
|
||||
ignore('&"');
|
||||
|
||||
succeed('>', ['>']);
|
||||
fatal('>', 'Character reference requires semicolon');
|
||||
ignore('&aaa');
|
||||
fatal('>a', 'Character reference requires semicolon');
|
||||
ignore({input: '>a', inAttribute: true});
|
||||
fatal({input: '>=', inAttribute: true}, 'Character reference requires semicolon: >');
|
||||
|
||||
succeed('>;', '>', ['>']);
|
||||
|
||||
fatal('&asdflkj;', 'Invalid character reference: &asdflkj;');
|
||||
fatal('&A0asdflkj;', 'Invalid character reference: &A0asdflkj;');
|
||||
ignore('&A0asdflkj');
|
||||
|
||||
succeed('𝕫', [120171]);
|
||||
succeed('∾̳', [8766, 819]);
|
||||
|
||||
succeed(' ', [10]);
|
||||
fatal('
', 'Invalid numerical character reference starting with &#');
|
||||
fatal('&#xg;', 'Invalid numerical character reference starting with &#');
|
||||
fatal('&#;', 'Invalid numerical character reference starting with &#');
|
||||
fatal('&#a;', 'Invalid numerical character reference starting with &#');
|
||||
fatal('&#a', 'Invalid numerical character reference starting with &#');
|
||||
fatal('&#z', 'Invalid numerical character reference starting with &#');
|
||||
succeed('
', [10]);
|
||||
fatal('�', 'Numerical character reference too large: 1000000000010');
|
||||
succeed('
', [10]);
|
||||
fatal('�', 'Numerical character reference too large: 0x100000000000a');
|
||||
succeed('
', [10]);
|
||||
succeed('
', [10]);
|
||||
succeed('
', [10]);
|
||||
succeed('
', [10]);
|
||||
succeed('
', [10]);
|
||||
|
||||
fatal('�', 'Illegal codepoint in numerical character reference: �');
|
||||
fatal('�', 'Illegal codepoint in numerical character reference: �');
|
||||
|
||||
fatal('', 'Illegal codepoint in numerical character reference: ');
|
||||
succeed('', [12]);
|
||||
fatal('', 'Illegal codepoint in numerical character reference: ');
|
||||
succeed('', [12]);
|
||||
|
||||
fatal('', 'Illegal codepoint in numerical character reference');
|
||||
fatal('', 'Illegal codepoint in numerical character reference');
|
||||
succeed('􏿽', [0x10fffd]);
|
||||
|
||||
fatal('', 'Illegal codepoint in numerical character reference');
|
||||
fatal('', 'Illegal codepoint in numerical character reference');
|
||||
succeed('􏿽', [0x10fffd]);
|
||||
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
Package.describe({
|
||||
summary: "Standards-compliant HTML tools",
|
||||
version: '1.0.10'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use('htmljs');
|
||||
api.imply('htmljs');
|
||||
|
||||
api.export('HTMLTools');
|
||||
|
||||
api.addFiles(['utils.js',
|
||||
'scanner.js',
|
||||
'charref.js',
|
||||
'tokenize.js',
|
||||
'templatetag.js',
|
||||
'parse.js']);
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use('html-tools');
|
||||
api.use('underscore');
|
||||
api.use('htmljs');
|
||||
api.use('blaze-tools'); // for `toJS`
|
||||
api.addFiles(['charref_tests.js',
|
||||
'tokenize_tests.js',
|
||||
'parse_tests.js']);
|
||||
});
|
||||
@@ -1,358 +0,0 @@
|
||||
|
||||
// Parse a "fragment" of HTML, up to the end of the input or a particular
|
||||
// template tag (using the "shouldStop" option).
|
||||
HTMLTools.parseFragment = function (input, options) {
|
||||
var scanner;
|
||||
if (typeof input === 'string')
|
||||
scanner = new Scanner(input);
|
||||
else
|
||||
// input can be a scanner. We'd better not have a different
|
||||
// value for the "getTemplateTag" option as when the scanner
|
||||
// was created, because we don't do anything special to reset
|
||||
// the value (which is attached to the scanner).
|
||||
scanner = input;
|
||||
|
||||
// ```
|
||||
// { getTemplateTag: function (scanner, templateTagPosition) {
|
||||
// if (templateTagPosition === HTMLTools.TEMPLATE_TAG_POSITION.ELEMENT) {
|
||||
// ...
|
||||
// ```
|
||||
if (options && options.getTemplateTag)
|
||||
scanner.getTemplateTag = options.getTemplateTag;
|
||||
|
||||
// function (scanner) -> boolean
|
||||
var shouldStop = options && options.shouldStop;
|
||||
|
||||
var result;
|
||||
if (options && options.textMode) {
|
||||
if (options.textMode === HTML.TEXTMODE.STRING) {
|
||||
result = getRawText(scanner, null, shouldStop);
|
||||
} else if (options.textMode === HTML.TEXTMODE.RCDATA) {
|
||||
result = getRCData(scanner, null, shouldStop);
|
||||
} else {
|
||||
throw new Error("Unsupported textMode: " + options.textMode);
|
||||
}
|
||||
} else {
|
||||
result = getContent(scanner, shouldStop);
|
||||
}
|
||||
if (! scanner.isEOF()) {
|
||||
// If we aren't at the end of the input, we either stopped at an unmatched
|
||||
// HTML end tag or at a template tag (like `{{else}}` or `{{/if}}`).
|
||||
// Detect the former case (stopped at an HTML end tag) and throw a good
|
||||
// error.
|
||||
|
||||
var posBefore = scanner.pos;
|
||||
|
||||
try {
|
||||
var endTag = getHTMLToken(scanner);
|
||||
} catch (e) {
|
||||
// ignore errors from getTemplateTag
|
||||
}
|
||||
|
||||
// XXX we make some assumptions about shouldStop here, like that it
|
||||
// won't tell us to stop at an HTML end tag. Should refactor
|
||||
// `shouldStop` into something more suitable.
|
||||
if (endTag && endTag.t === 'Tag' && endTag.isEnd) {
|
||||
var closeTag = endTag.n;
|
||||
var isVoidElement = HTML.isVoidElement(closeTag);
|
||||
scanner.fatal("Unexpected HTML close tag" +
|
||||
(isVoidElement ?
|
||||
'. <' + endTag.n + '> should have no close tag.' : ''));
|
||||
}
|
||||
|
||||
scanner.pos = posBefore; // rewind, we'll continue parsing as usual
|
||||
|
||||
// If no "shouldStop" option was provided, we should have consumed the whole
|
||||
// input.
|
||||
if (! shouldStop)
|
||||
scanner.fatal("Expected EOF");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Take a numeric Unicode code point, which may be larger than 16 bits,
|
||||
// and encode it as a JavaScript UTF-16 string.
|
||||
//
|
||||
// Adapted from
|
||||
// http://stackoverflow.com/questions/7126384/expressing-utf-16-unicode-characters-in-javascript/7126661.
|
||||
codePointToString = HTMLTools.codePointToString = function(cp) {
|
||||
if (cp >= 0 && cp <= 0xD7FF || cp >= 0xE000 && cp <= 0xFFFF) {
|
||||
return String.fromCharCode(cp);
|
||||
} else if (cp >= 0x10000 && cp <= 0x10FFFF) {
|
||||
|
||||
// we substract 0x10000 from cp to get a 20-bit number
|
||||
// in the range 0..0xFFFF
|
||||
cp -= 0x10000;
|
||||
|
||||
// we add 0xD800 to the number formed by the first 10 bits
|
||||
// to give the first byte
|
||||
var first = ((0xffc00 & cp) >> 10) + 0xD800;
|
||||
|
||||
// we add 0xDC00 to the number formed by the low 10 bits
|
||||
// to give the second byte
|
||||
var second = (0x3ff & cp) + 0xDC00;
|
||||
|
||||
return String.fromCharCode(first) + String.fromCharCode(second);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
getContent = HTMLTools.Parse.getContent = function (scanner, shouldStopFunc) {
|
||||
var items = [];
|
||||
|
||||
while (! scanner.isEOF()) {
|
||||
if (shouldStopFunc && shouldStopFunc(scanner))
|
||||
break;
|
||||
|
||||
var posBefore = scanner.pos;
|
||||
var token = getHTMLToken(scanner);
|
||||
if (! token)
|
||||
// tokenizer reached EOF on its own, e.g. while scanning
|
||||
// template comments like `{{! foo}}`.
|
||||
continue;
|
||||
|
||||
if (token.t === 'Doctype') {
|
||||
scanner.fatal("Unexpected Doctype");
|
||||
} else if (token.t === 'Chars') {
|
||||
pushOrAppendString(items, token.v);
|
||||
} else if (token.t === 'CharRef') {
|
||||
items.push(convertCharRef(token));
|
||||
} else if (token.t === 'Comment') {
|
||||
items.push(HTML.Comment(token.v));
|
||||
} else if (token.t === 'TemplateTag') {
|
||||
items.push(token.v);
|
||||
} else if (token.t === 'Tag') {
|
||||
if (token.isEnd) {
|
||||
// Stop when we encounter an end tag at the top level.
|
||||
// Rewind; we'll re-parse the end tag later.
|
||||
scanner.pos = posBefore;
|
||||
break;
|
||||
}
|
||||
|
||||
var tagName = token.n;
|
||||
// is this an element with no close tag (a BR, HR, IMG, etc.) based
|
||||
// on its name?
|
||||
var isVoid = HTML.isVoidElement(tagName);
|
||||
if (token.isSelfClosing) {
|
||||
if (! (isVoid || HTML.isKnownSVGElement(tagName) || tagName.indexOf(':') >= 0))
|
||||
scanner.fatal('Only certain elements like BR, HR, IMG, etc. (and foreign elements like SVG) are allowed to self-close');
|
||||
}
|
||||
|
||||
// result of parseAttrs may be null
|
||||
var attrs = parseAttrs(token.attrs);
|
||||
// arrays need to be wrapped in HTML.Attrs(...)
|
||||
// when used to construct tags
|
||||
if (HTML.isArray(attrs))
|
||||
attrs = HTML.Attrs.apply(null, attrs);
|
||||
|
||||
var tagFunc = HTML.getTag(tagName);
|
||||
if (isVoid || token.isSelfClosing) {
|
||||
items.push(attrs ? tagFunc(attrs) : tagFunc());
|
||||
} else {
|
||||
// parse HTML tag contents.
|
||||
|
||||
// HTML treats a final `/` in a tag as part of an attribute, as in `<a href=/foo/>`, but the template author who writes `<circle r={{r}}/>`, say, may not be thinking about that, so generate a good error message in the "looks like self-close" case.
|
||||
var looksLikeSelfClose = (scanner.input.substr(scanner.pos - 2, 2) === '/>');
|
||||
|
||||
var content = null;
|
||||
if (token.n === 'textarea') {
|
||||
if (scanner.peek() === '\n')
|
||||
scanner.pos++;
|
||||
var textareaValue = getRCData(scanner, token.n, shouldStopFunc);
|
||||
if (textareaValue) {
|
||||
if (attrs instanceof HTML.Attrs) {
|
||||
attrs = HTML.Attrs.apply(
|
||||
null, attrs.value.concat([{value: textareaValue}]));
|
||||
} else {
|
||||
attrs = (attrs || {});
|
||||
attrs.value = textareaValue;
|
||||
}
|
||||
}
|
||||
} else if (token.n === 'script' || token.n === 'style') {
|
||||
content = getRawText(scanner, token.n, shouldStopFunc);
|
||||
} else {
|
||||
content = getContent(scanner, shouldStopFunc);
|
||||
}
|
||||
|
||||
var endTag = getHTMLToken(scanner);
|
||||
|
||||
if (! (endTag && endTag.t === 'Tag' && endTag.isEnd && endTag.n === tagName))
|
||||
scanner.fatal('Expected "' + tagName + '" end tag' + (looksLikeSelfClose ? ' -- if the "<' + token.n + ' />" tag was supposed to self-close, try adding a space before the "/"' : ''));
|
||||
|
||||
// XXX support implied end tags in cases allowed by the spec
|
||||
|
||||
// make `content` into an array suitable for applying tag constructor
|
||||
// as in `FOO.apply(null, content)`.
|
||||
if (content == null)
|
||||
content = [];
|
||||
else if (! (content instanceof Array))
|
||||
content = [content];
|
||||
|
||||
items.push(HTML.getTag(tagName).apply(
|
||||
null, (attrs ? [attrs] : []).concat(content)));
|
||||
}
|
||||
} else {
|
||||
scanner.fatal("Unknown token type: " + token.t);
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0)
|
||||
return null;
|
||||
else if (items.length === 1)
|
||||
return items[0];
|
||||
else
|
||||
return items;
|
||||
};
|
||||
|
||||
var pushOrAppendString = function (items, string) {
|
||||
if (items.length &&
|
||||
typeof items[items.length - 1] === 'string')
|
||||
items[items.length - 1] += string;
|
||||
else
|
||||
items.push(string);
|
||||
};
|
||||
|
||||
// get RCDATA to go in the lowercase (or camel case) tagName (e.g. "textarea")
|
||||
getRCData = HTMLTools.Parse.getRCData = function (scanner, tagName, shouldStopFunc) {
|
||||
var items = [];
|
||||
|
||||
while (! scanner.isEOF()) {
|
||||
// break at appropriate end tag
|
||||
if (tagName && isLookingAtEndTag(scanner, tagName))
|
||||
break;
|
||||
|
||||
if (shouldStopFunc && shouldStopFunc(scanner))
|
||||
break;
|
||||
|
||||
var token = getHTMLToken(scanner, 'rcdata');
|
||||
if (! token)
|
||||
// tokenizer reached EOF on its own, e.g. while scanning
|
||||
// template comments like `{{! foo}}`.
|
||||
continue;
|
||||
|
||||
if (token.t === 'Chars') {
|
||||
pushOrAppendString(items, token.v);
|
||||
} else if (token.t === 'CharRef') {
|
||||
items.push(convertCharRef(token));
|
||||
} else if (token.t === 'TemplateTag') {
|
||||
items.push(token.v);
|
||||
} else {
|
||||
// (can't happen)
|
||||
scanner.fatal("Unknown or unexpected token type: " + token.t);
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0)
|
||||
return null;
|
||||
else if (items.length === 1)
|
||||
return items[0];
|
||||
else
|
||||
return items;
|
||||
};
|
||||
|
||||
var getRawText = function (scanner, tagName, shouldStopFunc) {
|
||||
var items = [];
|
||||
|
||||
while (! scanner.isEOF()) {
|
||||
// break at appropriate end tag
|
||||
if (tagName && isLookingAtEndTag(scanner, tagName))
|
||||
break;
|
||||
|
||||
if (shouldStopFunc && shouldStopFunc(scanner))
|
||||
break;
|
||||
|
||||
var token = getHTMLToken(scanner, 'rawtext');
|
||||
if (! token)
|
||||
// tokenizer reached EOF on its own, e.g. while scanning
|
||||
// template comments like `{{! foo}}`.
|
||||
continue;
|
||||
|
||||
if (token.t === 'Chars') {
|
||||
pushOrAppendString(items, token.v);
|
||||
} else if (token.t === 'TemplateTag') {
|
||||
items.push(token.v);
|
||||
} else {
|
||||
// (can't happen)
|
||||
scanner.fatal("Unknown or unexpected token type: " + token.t);
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0)
|
||||
return null;
|
||||
else if (items.length === 1)
|
||||
return items[0];
|
||||
else
|
||||
return items;
|
||||
};
|
||||
|
||||
// Input: A token like `{ t: 'CharRef', v: '&', cp: [38] }`.
|
||||
//
|
||||
// Output: A tag like `HTML.CharRef({ html: '&', str: '&' })`.
|
||||
var convertCharRef = function (token) {
|
||||
var codePoints = token.cp;
|
||||
var str = '';
|
||||
for (var i = 0; i < codePoints.length; i++)
|
||||
str += codePointToString(codePoints[i]);
|
||||
return HTML.CharRef({ html: token.v, str: str });
|
||||
};
|
||||
|
||||
// Input is always a dictionary (even if zero attributes) and each
|
||||
// value in the dictionary is an array of `Chars`, `CharRef`,
|
||||
// and maybe `TemplateTag` tokens.
|
||||
//
|
||||
// Output is null if there are zero attributes, and otherwise a
|
||||
// dictionary, or an array of dictionaries and template tags.
|
||||
// Each value in the dictionary is HTMLjs (e.g. a
|
||||
// string or an array of `Chars`, `CharRef`, and `TemplateTag`
|
||||
// nodes).
|
||||
//
|
||||
// An attribute value with no input tokens is represented as "",
|
||||
// not an empty array, in order to prop open empty attributes
|
||||
// with no template tags.
|
||||
var parseAttrs = function (attrs) {
|
||||
var result = null;
|
||||
|
||||
if (HTML.isArray(attrs)) {
|
||||
// first element is nondynamic attrs, rest are template tags
|
||||
var nondynamicAttrs = parseAttrs(attrs[0]);
|
||||
if (nondynamicAttrs) {
|
||||
result = (result || []);
|
||||
result.push(nondynamicAttrs);
|
||||
}
|
||||
for (var i = 1; i < attrs.length; i++) {
|
||||
var token = attrs[i];
|
||||
if (token.t !== 'TemplateTag')
|
||||
throw new Error("Expected TemplateTag token");
|
||||
result = (result || []);
|
||||
result.push(token.v);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
for (var k in attrs) {
|
||||
if (! result)
|
||||
result = {};
|
||||
|
||||
var inValue = attrs[k];
|
||||
var outParts = [];
|
||||
for (var i = 0; i < inValue.length; i++) {
|
||||
var token = inValue[i];
|
||||
if (token.t === 'CharRef') {
|
||||
outParts.push(convertCharRef(token));
|
||||
} else if (token.t === 'TemplateTag') {
|
||||
outParts.push(token.v);
|
||||
} else if (token.t === 'Chars') {
|
||||
pushOrAppendString(outParts, token.v);
|
||||
}
|
||||
}
|
||||
|
||||
var outValue = (inValue.length === 0 ? '' :
|
||||
(outParts.length === 1 ? outParts[0] : outParts));
|
||||
var properKey = HTMLTools.properCaseAttributeName(k);
|
||||
result[properKey] = outValue;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -1,404 +0,0 @@
|
||||
var Scanner = HTMLTools.Scanner;
|
||||
var getContent = HTMLTools.Parse.getContent;
|
||||
|
||||
var CharRef = HTML.CharRef;
|
||||
var Comment = HTML.Comment;
|
||||
var TemplateTag = HTMLTools.TemplateTag;
|
||||
var Attrs = HTML.Attrs;
|
||||
|
||||
var BR = HTML.BR;
|
||||
var HR = HTML.HR;
|
||||
var INPUT = HTML.INPUT;
|
||||
var A = HTML.A;
|
||||
var DIV = HTML.DIV;
|
||||
var P = HTML.P;
|
||||
var TEXTAREA = HTML.TEXTAREA;
|
||||
var SCRIPT = HTML.SCRIPT;
|
||||
var STYLE = HTML.STYLE;
|
||||
|
||||
Tinytest.add("html-tools - parser getContent", function (test) {
|
||||
|
||||
var succeed = function (input, expected) {
|
||||
var endPos = input.indexOf('^^^');
|
||||
if (endPos < 0)
|
||||
endPos = input.length;
|
||||
|
||||
var scanner = new Scanner(input.replace('^^^', ''));
|
||||
var result = getContent(scanner);
|
||||
test.equal(scanner.pos, endPos);
|
||||
test.equal(BlazeTools.toJS(result), BlazeTools.toJS(expected));
|
||||
};
|
||||
|
||||
var fatal = function (input, messageContains) {
|
||||
var scanner = new Scanner(input);
|
||||
var error;
|
||||
try {
|
||||
getContent(scanner);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
test.isTrue(error);
|
||||
if (messageContains)
|
||||
test.isTrue(messageContains && error.message.indexOf(messageContains) >= 0, error.message);
|
||||
};
|
||||
|
||||
|
||||
succeed('', null);
|
||||
succeed('abc', 'abc');
|
||||
succeed('abc^^^</x>', 'abc');
|
||||
succeed('a<b', ['a', CharRef({html: '<', str: '<'}), 'b']);
|
||||
succeed('<!-- x -->', Comment(' x '));
|
||||
succeed('∾̳', CharRef({html: '∾̳', str: '\u223e\u0333'}));
|
||||
succeed('𝕫', CharRef({html: '𝕫', str: '\ud835\udd6b'}));
|
||||
succeed('&&>&g>;', ['&&>&g', CharRef({html: '>', str: '>'}), ';']);
|
||||
|
||||
// Can't have an unescaped `&` if followed by certain names like `gt`
|
||||
fatal('>&');
|
||||
// tests for other failure cases
|
||||
fatal('<');
|
||||
|
||||
succeed('<br>', BR());
|
||||
succeed('<br/>', BR());
|
||||
fatal('<div/>', 'self-close');
|
||||
|
||||
succeed('<hr id=foo>', HR({id:'foo'}));
|
||||
succeed('<hr id=<foo>>', HR({id:[CharRef({html:'<', str:'<'}),
|
||||
'foo',
|
||||
CharRef({html:'>', str:'>'})]}));
|
||||
succeed('<input selected>', INPUT({selected: ''}));
|
||||
succeed('<input selected/>', INPUT({selected: ''}));
|
||||
succeed('<input selected />', INPUT({selected: ''}));
|
||||
var FOO = HTML.getTag('foo');
|
||||
succeed('<foo bar></foo>', FOO({bar: ''}));
|
||||
succeed('<foo bar baz ></foo>', FOO({bar: '', baz: ''}));
|
||||
succeed('<foo bar=x baz qux=y blah ></foo>',
|
||||
FOO({bar: 'x', baz: '', qux: 'y', blah: ''}));
|
||||
succeed('<foo bar="x" baz qux="y" blah ></foo>',
|
||||
FOO({bar: 'x', baz: '', qux: 'y', blah: ''}));
|
||||
fatal('<input bar"baz">');
|
||||
fatal('<input x="y"z >');
|
||||
fatal('<input x=\'y\'z >');
|
||||
succeed('<br x=&&&>', BR({x: '&&&'}));
|
||||
succeed('<br><br><br>', [BR(), BR(), BR()]);
|
||||
succeed('aaa<br>\nbbb<br>\nccc<br>', ['aaa', BR(), '\nbbb', BR(), '\nccc', BR()]);
|
||||
|
||||
succeed('<a></a>', A());
|
||||
fatal('<');
|
||||
fatal('<a');
|
||||
fatal('<a>');
|
||||
fatal('<a><');
|
||||
fatal('<a></');
|
||||
fatal('<a></a');
|
||||
|
||||
succeed('<a href="http://www.apple.com/">Apple</a>',
|
||||
A({href: "http://www.apple.com/"}, 'Apple'));
|
||||
|
||||
(function () {
|
||||
var A = HTML.getTag('a');
|
||||
var B = HTML.getTag('b');
|
||||
var C = HTML.getTag('c');
|
||||
var D = HTML.getTag('d');
|
||||
|
||||
succeed('<a>1<b>2<c>3<d>4</d>5</c>6</b>7</a>8',
|
||||
[A('1', B('2', C('3', D('4'), '5'), '6'), '7'), '8']);
|
||||
})();
|
||||
|
||||
fatal('<b>hello <i>there</b> world</i>');
|
||||
|
||||
// XXX support implied end tags in cases allowed by the spec
|
||||
fatal('<p>');
|
||||
|
||||
fatal('<a>Foo</a/>');
|
||||
fatal('<a>Foo</a b=c>');
|
||||
|
||||
succeed('<textarea>asdf</textarea>', TEXTAREA({value: "asdf"}));
|
||||
succeed('<textarea x=y>asdf</textarea>', TEXTAREA({x: "y", value: "asdf"}));
|
||||
succeed('<textarea><p></textarea>', TEXTAREA({value: "<p>"}));
|
||||
succeed('<textarea>a&b</textarea>',
|
||||
TEXTAREA({value: ["a", CharRef({html: '&', str: '&'}), "b"]}));
|
||||
succeed('<textarea></textarea</textarea>', TEXTAREA({value: "</textarea"}));
|
||||
// absorb up to one initial newline, as per HTML parsing spec
|
||||
succeed('<textarea>\n</textarea>', TEXTAREA());
|
||||
succeed('<textarea>\nasdf</textarea>', TEXTAREA({value: "asdf"}));
|
||||
succeed('<textarea>\n\nasdf</textarea>', TEXTAREA({value: "\nasdf"}));
|
||||
succeed('<textarea>\n\n</textarea>', TEXTAREA({value: "\n"}));
|
||||
succeed('<textarea>\nasdf\n</textarea>', TEXTAREA({value: "asdf\n"}));
|
||||
succeed('<textarea><!-- --></textarea>', TEXTAREA({value: "<!-- -->"}));
|
||||
succeed('<tExTaReA>asdf</TEXTarea>', TEXTAREA({value: "asdf"}));
|
||||
fatal('<textarea>asdf');
|
||||
fatal('<textarea>asdf</textarea');
|
||||
fatal('<textarea>&davidgreenspan;</textarea>');
|
||||
succeed('<textarea>&</textarea>', TEXTAREA({value: "&"}));
|
||||
succeed('<textarea></textarea \n<</textarea \n>asdf',
|
||||
[TEXTAREA({value: "</textarea \n<"}), "asdf"]);
|
||||
// regression test for a bug that happened with textarea content
|
||||
// handling after an element with content
|
||||
succeed('<div>x</div><textarea></textarea>', [DIV("x"), TEXTAREA()]);
|
||||
|
||||
// CR/LF behavior
|
||||
succeed('<br\r\n x>', BR({x:''}));
|
||||
succeed('<br\r x>', BR({x:''}));
|
||||
succeed('<br x="y"\r\n>', BR({x:'y'}));
|
||||
succeed('<br x="y"\r>', BR({x:'y'}));
|
||||
succeed('<br x=\r\n"y">', BR({x:'y'}));
|
||||
succeed('<br x=\r"y">', BR({x:'y'}));
|
||||
succeed('<br x\r=\r"y">', BR({x:'y'}));
|
||||
succeed('<!--\r\n-->', Comment('\n'));
|
||||
succeed('<!--\r-->', Comment('\n'));
|
||||
succeed('<textarea>a\r\nb\r\nc</textarea>', TEXTAREA({value: 'a\nb\nc'}));
|
||||
succeed('<textarea>a\rb\rc</textarea>', TEXTAREA({value: 'a\nb\nc'}));
|
||||
succeed('<br x="\r\n\r\n">', BR({x:'\n\n'}));
|
||||
succeed('<br x="\r\r">', BR({x:'\n\n'}));
|
||||
succeed('<br x=y\r>', BR({x:'y'}));
|
||||
fatal('<br x=\r>');
|
||||
|
||||
succeed('<script>var x="<div>";</script>',SCRIPT('var x="<div>";'));
|
||||
succeed('<script>var x=1 && 0;</script>',SCRIPT('var x=1 && 0;'));
|
||||
|
||||
succeed('<script>asdf</script>', SCRIPT("asdf"));
|
||||
succeed('<script x=y>asdf</script>', SCRIPT({x: "y"}, "asdf"));
|
||||
succeed('<script><p></script>', SCRIPT("<p>"));
|
||||
succeed('<script>a&b</script>', SCRIPT("a&b"));
|
||||
succeed('<script></script</script>', SCRIPT("</script"));
|
||||
succeed('<script>\n</script>', SCRIPT("\n"));
|
||||
succeed('<script><!-- --></script>', SCRIPT("<!-- -->"));
|
||||
succeed('<sCrIpT>asdf</SCRipt>', SCRIPT("asdf"));
|
||||
fatal('<script>asdf');
|
||||
fatal('<script>asdf</script');
|
||||
succeed('<script>&davidgreenspan;</script>', SCRIPT("&davidgreenspan;"));
|
||||
succeed('<script>&</script>', SCRIPT("&"));
|
||||
succeed('<script></script \n<</script \n>asdf',
|
||||
[SCRIPT("</script \n<"), "asdf"]);
|
||||
|
||||
succeed('<style>asdf</style>', STYLE("asdf"));
|
||||
succeed('<style x=y>asdf</style>', STYLE({x: "y"}, "asdf"));
|
||||
succeed('<style><p></style>', STYLE("<p>"));
|
||||
succeed('<style>a&b</style>', STYLE("a&b"));
|
||||
succeed('<style></style</style>', STYLE("</style"));
|
||||
succeed('<style>\n</style>', STYLE("\n"));
|
||||
succeed('<style><!-- --></style>', STYLE("<!-- -->"));
|
||||
succeed('<sTyLe>asdf</STYle>', STYLE("asdf"));
|
||||
fatal('<style>asdf');
|
||||
fatal('<style>asdf</style');
|
||||
succeed('<style>&davidgreenspan;</style>', STYLE("&davidgreenspan;"));
|
||||
succeed('<style>&</style>', STYLE("&"));
|
||||
succeed('<style></style \n<</style \n>asdf',
|
||||
[STYLE("</style \n<"), "asdf"]);
|
||||
});
|
||||
|
||||
Tinytest.add("html-tools - parseFragment", function (test) {
|
||||
test.equal(BlazeTools.toJS(HTMLTools.parseFragment("<div><p id=foo>Hello</p></div>")),
|
||||
BlazeTools.toJS(DIV(P({id:'foo'}, 'Hello'))));
|
||||
|
||||
_.each(['asdf</br>', '{{!foo}}</br>', '{{!foo}} </br>',
|
||||
'asdf</a>', '{{!foo}}</a>', '{{!foo}} </a>'], function (badFrag) {
|
||||
test.throws(function() {
|
||||
HTMLTools.parseFragment(badFrag);
|
||||
}, /Unexpected HTML close tag/);
|
||||
});
|
||||
|
||||
(function () {
|
||||
var p = HTMLTools.parseFragment('<p></p>');
|
||||
test.equal(p.tagName, 'p');
|
||||
test.equal(p.attrs, null);
|
||||
test.isTrue(p instanceof HTML.Tag);
|
||||
test.equal(p.children.length, 0);
|
||||
})();
|
||||
|
||||
(function () {
|
||||
var p = HTMLTools.parseFragment('<p>x</p>');
|
||||
test.equal(p.tagName, 'p');
|
||||
test.equal(p.attrs, null);
|
||||
test.isTrue(p instanceof HTML.Tag);
|
||||
test.equal(p.children.length, 1);
|
||||
test.equal(p.children[0], 'x');
|
||||
})();
|
||||
|
||||
(function () {
|
||||
var p = HTMLTools.parseFragment('<p>xA</p>');
|
||||
test.equal(p.tagName, 'p');
|
||||
test.equal(p.attrs, null);
|
||||
test.isTrue(p instanceof HTML.Tag);
|
||||
test.equal(p.children.length, 2);
|
||||
test.equal(p.children[0], 'x');
|
||||
|
||||
test.isTrue(p.children[1] instanceof HTML.CharRef);
|
||||
test.equal(p.children[1].html, 'A');
|
||||
test.equal(p.children[1].str, 'A');
|
||||
})();
|
||||
|
||||
(function () {
|
||||
var pp = HTMLTools.parseFragment('<p>x</p><p>y</p>');
|
||||
test.isTrue(pp instanceof Array);
|
||||
test.equal(pp.length, 2);
|
||||
|
||||
test.equal(pp[0].tagName, 'p');
|
||||
test.equal(pp[0].attrs, null);
|
||||
test.isTrue(pp[0] instanceof HTML.Tag);
|
||||
test.equal(pp[0].children.length, 1);
|
||||
test.equal(pp[0].children[0], 'x');
|
||||
|
||||
test.equal(pp[1].tagName, 'p');
|
||||
test.equal(pp[1].attrs, null);
|
||||
test.isTrue(pp[1] instanceof HTML.Tag);
|
||||
test.equal(pp[1].children.length, 1);
|
||||
test.equal(pp[1].children[0], 'y');
|
||||
})();
|
||||
|
||||
var scanner = new Scanner('asdf');
|
||||
scanner.pos = 1;
|
||||
test.equal(HTMLTools.parseFragment(scanner), 'sdf');
|
||||
|
||||
test.throws(function () {
|
||||
var scanner = new Scanner('asdf</p>');
|
||||
scanner.pos = 1;
|
||||
HTMLTools.parseFragment(scanner);
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("html-tools - getTemplateTag", function (test) {
|
||||
|
||||
// match a simple tag consisting of `{{`, an optional `!`, one
|
||||
// or more ASCII letters, spaces or html tags, and a closing `}}`.
|
||||
var mustache = /^\{\{(!?[a-zA-Z 0-9</>]+)\}\}/;
|
||||
|
||||
// This implementation of `getTemplateTag` looks for "{{" and if it
|
||||
// finds it, it will match the regex above or fail fatally trying.
|
||||
// The object it returns is opaque to the tokenizer/parser and can
|
||||
// be anything we want.
|
||||
var getTemplateTag = function (scanner, position) {
|
||||
if (! (scanner.peek() === '{' && // one-char peek is just an optimization
|
||||
scanner.rest().slice(0, 2) === '{{'))
|
||||
return null;
|
||||
|
||||
var match = mustache.exec(scanner.rest());
|
||||
if (! match)
|
||||
scanner.fatal("Bad mustache");
|
||||
|
||||
scanner.pos += match[0].length;
|
||||
|
||||
if (match[1].charAt(0) === '!')
|
||||
return null; // `{{!foo}}` is like a comment
|
||||
|
||||
return TemplateTag({ stuff: match[1] });
|
||||
};
|
||||
|
||||
|
||||
|
||||
var succeed = function (input, expected) {
|
||||
var endPos = input.indexOf('^^^');
|
||||
if (endPos < 0)
|
||||
endPos = input.length;
|
||||
|
||||
var scanner = new Scanner(input.replace('^^^', ''));
|
||||
scanner.getTemplateTag = getTemplateTag;
|
||||
var result;
|
||||
try {
|
||||
result = getContent(scanner);
|
||||
} catch (e) {
|
||||
result = String(e);
|
||||
}
|
||||
test.equal(scanner.pos, endPos);
|
||||
test.equal(BlazeTools.toJS(result), BlazeTools.toJS(expected));
|
||||
};
|
||||
|
||||
var fatal = function (input, messageContains) {
|
||||
var scanner = new Scanner(input);
|
||||
scanner.getTemplateTag = getTemplateTag;
|
||||
var error;
|
||||
try {
|
||||
getContent(scanner);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
test.isTrue(error);
|
||||
if (messageContains)
|
||||
test.isTrue(messageContains && error.message.indexOf(messageContains) >= 0, error.message);
|
||||
};
|
||||
|
||||
|
||||
succeed('{{foo}}', TemplateTag({stuff: 'foo'}));
|
||||
|
||||
succeed('<a href=http://www.apple.com/>{{foo}}</a>',
|
||||
A({href: "http://www.apple.com/"}, TemplateTag({stuff: 'foo'})));
|
||||
|
||||
// tags not parsed in comments
|
||||
succeed('<!--{{foo}}-->', Comment("{{foo}}"));
|
||||
succeed('<!--{{foo-->', Comment("{{foo"));
|
||||
|
||||
succeed('&am{{foo}}p;', ['&am', TemplateTag({stuff: 'foo'}), 'p;']);
|
||||
|
||||
// can't start a mustache and not finish it
|
||||
fatal('{{foo');
|
||||
fatal('<a>{{</a>');
|
||||
|
||||
// no mustache allowed in tag name
|
||||
fatal('<{{a}}>');
|
||||
fatal('<{{a}}b>');
|
||||
fatal('<a{{b}}>');
|
||||
|
||||
// single curly brace is no biggie
|
||||
succeed('a{b', 'a{b');
|
||||
succeed('<br x={ />', BR({x:'{'}));
|
||||
succeed('<br x={foo} />', BR({x:'{foo}'}));
|
||||
|
||||
succeed('<br {{x}}>', BR(Attrs(TemplateTag({stuff: 'x'}))));
|
||||
succeed('<br {{x}} {{y}}>', BR(Attrs(TemplateTag({stuff: 'x'}),
|
||||
TemplateTag({stuff: 'y'}))));
|
||||
succeed('<br {{x}} y>', BR(Attrs({y: ''}, TemplateTag({stuff: 'x'}))));
|
||||
fatal('<br {{x}}y>');
|
||||
fatal('<br {{x}}=y>');
|
||||
succeed('<br x={{y}} z>', BR({x: TemplateTag({stuff: 'y'}), z: ''}));
|
||||
succeed('<br x=y{{z}}w>', BR({x: ['y', TemplateTag({stuff: 'z'}), 'w']}));
|
||||
succeed('<br x="y{{z}}w">', BR({x: ['y', TemplateTag({stuff: 'z'}), 'w']}));
|
||||
succeed('<br x="y {{z}}{{w}} v">', BR({x: ['y ', TemplateTag({stuff: 'z'}),
|
||||
TemplateTag({stuff: 'w'}), ' v']}));
|
||||
// Slash is parsed as part of unquoted attribute! This is consistent with
|
||||
// the HTML tokenization spec. It seems odd for some inputs but is probably
|
||||
// for cases like `<a href=http://foo.com/>` or `<a href=/foo/>`.
|
||||
succeed('<br x={{y}}/>', BR({x: [TemplateTag({stuff: 'y'}), '/']}));
|
||||
succeed('<br x={{z}}{{w}}>', BR({x: [TemplateTag({stuff: 'z'}),
|
||||
TemplateTag({stuff: 'w'})]}));
|
||||
fatal('<br x="y"{{z}}>');
|
||||
|
||||
succeed('<br x=&>', BR({x:CharRef({html: '&', str: '&'})}));
|
||||
|
||||
|
||||
// check tokenization of stache tags with spaces
|
||||
succeed('<br {{x 1}}>', BR(Attrs(TemplateTag({stuff: 'x 1'}))));
|
||||
succeed('<br {{x 1}} {{y 2}}>', BR(Attrs(TemplateTag({stuff: 'x 1'}),
|
||||
TemplateTag({stuff: 'y 2'}))));
|
||||
succeed('<br {{x 1}} y>', BR(Attrs({y:''}, TemplateTag({stuff: 'x 1'}))));
|
||||
fatal('<br {{x 1}}y>');
|
||||
fatal('<br {{x 1}}=y>');
|
||||
succeed('<br x={{y 2}} z>', BR({x: TemplateTag({stuff: 'y 2'}), z: ''}));
|
||||
succeed('<br x=y{{z 3}}w>', BR({x: ['y', TemplateTag({stuff: 'z 3'}), 'w']}));
|
||||
succeed('<br x="y{{z 3}}w">', BR({x: ['y', TemplateTag({stuff: 'z 3'}), 'w']}));
|
||||
succeed('<br x="y {{z 3}}{{w 4}} v">', BR({x: ['y ', TemplateTag({stuff: 'z 3'}),
|
||||
TemplateTag({stuff: 'w 4'}), ' v']}));
|
||||
succeed('<br x={{y 2}}/>', BR({x: [TemplateTag({stuff: 'y 2'}), '/']}));
|
||||
succeed('<br x={{z 3}}{{w 4}}>', BR({x: [TemplateTag({stuff: 'z 3'}),
|
||||
TemplateTag({stuff: 'w 4'})]}));
|
||||
|
||||
succeed('<p></p>', P());
|
||||
|
||||
succeed('x{{foo}}{{bar}}y', ['x', TemplateTag({stuff: 'foo'}),
|
||||
TemplateTag({stuff: 'bar'}), 'y']);
|
||||
succeed('x{{!foo}}{{!bar}}y', 'xy');
|
||||
succeed('x{{!foo}}{{bar}}y', ['x', TemplateTag({stuff: 'bar'}), 'y']);
|
||||
succeed('x{{foo}}{{!bar}}y', ['x', TemplateTag({stuff: 'foo'}), 'y']);
|
||||
succeed('<div>{{!foo}}{{!bar}}</div>', DIV());
|
||||
succeed('<div>{{!foo}}<br />{{!bar}}</div>', DIV(BR()));
|
||||
succeed('<div> {{!foo}} {{!bar}} </div>', DIV(" "));
|
||||
succeed('<div> {{!foo}} <br /> {{!bar}}</div>', DIV(" ", BR(), " "));
|
||||
succeed('{{! <div></div> }}', null);
|
||||
succeed('{{!<div></div>}}', null);
|
||||
|
||||
succeed('', null);
|
||||
succeed('{{!foo}}', null);
|
||||
|
||||
succeed('<textarea {{a}} x=1 {{b}}></textarea>',
|
||||
TEXTAREA(Attrs({x:"1"}, TemplateTag({stuff: 'a'}),
|
||||
TemplateTag({stuff: 'b'}))));
|
||||
|
||||
});
|
||||
@@ -1,82 +0,0 @@
|
||||
// This is a Scanner class suitable for any parser/lexer/tokenizer.
|
||||
//
|
||||
// A Scanner has an immutable source document (string) `input` and a current
|
||||
// position `pos`, an index into the string, which can be set at will.
|
||||
//
|
||||
// * `new Scanner(input)` - constructs a Scanner with source string `input`
|
||||
// * `scanner.rest()` - returns the rest of the input after `pos`
|
||||
// * `scanner.peek()` - returns the character at `pos`
|
||||
// * `scanner.isEOF()` - true if `pos` is at or beyond the end of `input`
|
||||
// * `scanner.fatal(msg)` - throw an error indicating a problem at `pos`
|
||||
|
||||
Scanner = HTMLTools.Scanner = function (input) {
|
||||
this.input = input; // public, read-only
|
||||
this.pos = 0; // public, read-write
|
||||
};
|
||||
|
||||
Scanner.prototype.rest = function () {
|
||||
// Slicing a string is O(1) in modern JavaScript VMs (including old IE).
|
||||
return this.input.slice(this.pos);
|
||||
};
|
||||
|
||||
Scanner.prototype.isEOF = function () {
|
||||
return this.pos >= this.input.length;
|
||||
};
|
||||
|
||||
Scanner.prototype.fatal = function (msg) {
|
||||
// despite this default, you should always provide a message!
|
||||
msg = (msg || "Parse error");
|
||||
|
||||
var CONTEXT_AMOUNT = 20;
|
||||
|
||||
var input = this.input;
|
||||
var pos = this.pos;
|
||||
var pastInput = input.substring(pos - CONTEXT_AMOUNT - 1, pos);
|
||||
if (pastInput.length > CONTEXT_AMOUNT)
|
||||
pastInput = '...' + pastInput.substring(-CONTEXT_AMOUNT);
|
||||
|
||||
var upcomingInput = input.substring(pos, pos + CONTEXT_AMOUNT + 1);
|
||||
if (upcomingInput.length > CONTEXT_AMOUNT)
|
||||
upcomingInput = upcomingInput.substring(0, CONTEXT_AMOUNT) + '...';
|
||||
|
||||
var positionDisplay = ((pastInput + upcomingInput).replace(/\n/g, ' ') + '\n' +
|
||||
(new Array(pastInput.length + 1).join(' ')) + "^");
|
||||
|
||||
var e = new Error(msg + "\n" + positionDisplay);
|
||||
|
||||
e.offset = pos;
|
||||
var allPastInput = input.substring(0, pos);
|
||||
e.line = (1 + (allPastInput.match(/\n/g) || []).length);
|
||||
e.col = (1 + pos - allPastInput.lastIndexOf('\n'));
|
||||
e.scanner = this;
|
||||
|
||||
throw e;
|
||||
};
|
||||
|
||||
// Peek at the next character.
|
||||
//
|
||||
// If `isEOF`, returns an empty string.
|
||||
Scanner.prototype.peek = function () {
|
||||
return this.input.charAt(this.pos);
|
||||
};
|
||||
|
||||
// Constructs a `getFoo` function where `foo` is specified with a regex.
|
||||
// The regex should start with `^`. The constructed function will return
|
||||
// match group 1, if it exists and matches a non-empty string, or else
|
||||
// the entire matched string (or null if there is no match).
|
||||
//
|
||||
// A `getFoo` function tries to match and consume a foo. If it succeeds,
|
||||
// the current position of the scanner is advanced. If it fails, the
|
||||
// current position is not advanced and a falsy value (typically null)
|
||||
// is returned.
|
||||
makeRegexMatcher = function (regex) {
|
||||
return function (scanner) {
|
||||
var match = regex.exec(scanner.rest());
|
||||
|
||||
if (! match)
|
||||
return null;
|
||||
|
||||
scanner.pos += match[0].length;
|
||||
return match[1] || match[0];
|
||||
};
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
// _assign is like _.extend or the upcoming Object.assign.
|
||||
// Copy src's own, enumerable properties onto tgt and return
|
||||
// tgt.
|
||||
var _hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
var _assign = function (tgt, src) {
|
||||
for (var k in src) {
|
||||
if (_hasOwnProperty.call(src, k))
|
||||
tgt[k] = src[k];
|
||||
}
|
||||
return tgt;
|
||||
};
|
||||
|
||||
|
||||
HTMLTools.TemplateTag = function (props) {
|
||||
if (! (this instanceof HTMLTools.TemplateTag))
|
||||
// called without `new`
|
||||
return new HTMLTools.TemplateTag;
|
||||
|
||||
if (props)
|
||||
_assign(this, props);
|
||||
};
|
||||
|
||||
_assign(HTMLTools.TemplateTag.prototype, {
|
||||
constructorName: 'HTMLTools.TemplateTag',
|
||||
toJS: function (visitor) {
|
||||
return visitor.generateCall(this.constructorName,
|
||||
_assign({}, this));
|
||||
}
|
||||
});
|
||||
@@ -1,513 +0,0 @@
|
||||
// Token types:
|
||||
//
|
||||
// { t: 'Doctype',
|
||||
// v: String (entire Doctype declaration from the source),
|
||||
// name: String,
|
||||
// systemId: String (optional),
|
||||
// publicId: String (optional)
|
||||
// }
|
||||
//
|
||||
// { t: 'Comment',
|
||||
// v: String (not including "<!--" and "-->")
|
||||
// }
|
||||
//
|
||||
// { t: 'Chars',
|
||||
// v: String (pure text like you might pass to document.createTextNode,
|
||||
// no character references)
|
||||
// }
|
||||
//
|
||||
// { t: 'Tag',
|
||||
// isEnd: Boolean (optional),
|
||||
// isSelfClosing: Boolean (optional),
|
||||
// n: String (tag name, in lowercase or camel case),
|
||||
// attrs: dictionary of { String: [tokens] }
|
||||
// OR [{ String: [tokens] }, TemplateTag tokens...]
|
||||
// (only for start tags; required)
|
||||
// }
|
||||
//
|
||||
// { t: 'CharRef',
|
||||
// v: String (entire character reference from the source, e.g. "&"),
|
||||
// cp: [Integer] (array of Unicode code point numbers it expands to)
|
||||
// }
|
||||
//
|
||||
// We keep around both the original form of the character reference and its
|
||||
// expansion so that subsequent processing steps have the option to
|
||||
// re-emit it (if they are generating HTML) or interpret it. Named and
|
||||
// numerical code points may be more than 16 bits, in which case they
|
||||
// need to passed through codePointToString to make a JavaScript string.
|
||||
// Most named entities and all numeric character references are one codepoint
|
||||
// (e.g. "&" is [38]), but a few are two codepoints.
|
||||
//
|
||||
// { t: 'TemplateTag',
|
||||
// v: HTMLTools.TemplateTag
|
||||
// }
|
||||
|
||||
// The HTML tokenization spec says to preprocess the input stream to replace
|
||||
// CR(LF)? with LF. However, preprocessing `scanner` would complicate things
|
||||
// by making indexes not match the input (e.g. for error messages), so we just
|
||||
// keep in mind as we go along that an LF might be represented by CRLF or CR.
|
||||
// In most cases, it doesn't actually matter what combination of whitespace
|
||||
// characters are present (e.g. inside tags).
|
||||
var HTML_SPACE = /^[\f\n\r\t ]/;
|
||||
|
||||
var convertCRLF = function (str) {
|
||||
return str.replace(/\r\n?/g, '\n');
|
||||
};
|
||||
|
||||
getComment = HTMLTools.Parse.getComment = function (scanner) {
|
||||
if (scanner.rest().slice(0, 4) !== '<!--')
|
||||
return null;
|
||||
scanner.pos += 4;
|
||||
|
||||
// Valid comments are easy to parse; they end at the first `--`!
|
||||
// Our main job is throwing errors.
|
||||
|
||||
var rest = scanner.rest();
|
||||
if (rest.charAt(0) === '>' || rest.slice(0, 2) === '->')
|
||||
scanner.fatal("HTML comment can't start with > or ->");
|
||||
|
||||
var closePos = rest.indexOf('-->');
|
||||
if (closePos < 0)
|
||||
scanner.fatal("Unclosed HTML comment");
|
||||
|
||||
var commentContents = rest.slice(0, closePos);
|
||||
if (commentContents.slice(-1) === '-')
|
||||
scanner.fatal("HTML comment must end at first `--`");
|
||||
if (commentContents.indexOf("--") >= 0)
|
||||
scanner.fatal("HTML comment cannot contain `--` anywhere");
|
||||
if (commentContents.indexOf('\u0000') >= 0)
|
||||
scanner.fatal("HTML comment cannot contain NULL");
|
||||
|
||||
scanner.pos += closePos + 3;
|
||||
|
||||
return { t: 'Comment',
|
||||
v: convertCRLF(commentContents) };
|
||||
};
|
||||
|
||||
var skipSpaces = function (scanner) {
|
||||
while (HTML_SPACE.test(scanner.peek()))
|
||||
scanner.pos++;
|
||||
};
|
||||
|
||||
var requireSpaces = function (scanner) {
|
||||
if (! HTML_SPACE.test(scanner.peek()))
|
||||
scanner.fatal("Expected space");
|
||||
skipSpaces(scanner);
|
||||
};
|
||||
|
||||
var getDoctypeQuotedString = function (scanner) {
|
||||
var quote = scanner.peek();
|
||||
if (! (quote === '"' || quote === "'"))
|
||||
scanner.fatal("Expected single or double quote in DOCTYPE");
|
||||
scanner.pos++;
|
||||
|
||||
if (scanner.peek() === quote)
|
||||
// prevent a falsy return value (empty string)
|
||||
scanner.fatal("Malformed DOCTYPE");
|
||||
|
||||
var str = '';
|
||||
var ch;
|
||||
while ((ch = scanner.peek()), ch !== quote) {
|
||||
if ((! ch) || (ch === '\u0000') || (ch === '>'))
|
||||
scanner.fatal("Malformed DOCTYPE");
|
||||
str += ch;
|
||||
scanner.pos++;
|
||||
}
|
||||
|
||||
scanner.pos++;
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
// See http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#the-doctype.
|
||||
//
|
||||
// If `getDocType` sees "<!DOCTYPE" (case-insensitive), it will match or fail fatally.
|
||||
getDoctype = HTMLTools.Parse.getDoctype = function (scanner) {
|
||||
if (HTMLTools.asciiLowerCase(scanner.rest().slice(0, 9)) !== '<!doctype')
|
||||
return null;
|
||||
var start = scanner.pos;
|
||||
scanner.pos += 9;
|
||||
|
||||
requireSpaces(scanner);
|
||||
|
||||
var ch = scanner.peek();
|
||||
if ((! ch) || (ch === '>') || (ch === '\u0000'))
|
||||
scanner.fatal('Malformed DOCTYPE');
|
||||
var name = ch;
|
||||
scanner.pos++;
|
||||
|
||||
while ((ch = scanner.peek()), ! (HTML_SPACE.test(ch) || ch === '>')) {
|
||||
if ((! ch) || (ch === '\u0000'))
|
||||
scanner.fatal('Malformed DOCTYPE');
|
||||
name += ch;
|
||||
scanner.pos++;
|
||||
}
|
||||
name = HTMLTools.asciiLowerCase(name);
|
||||
|
||||
// Now we're looking at a space or a `>`.
|
||||
skipSpaces(scanner);
|
||||
|
||||
var systemId = null;
|
||||
var publicId = null;
|
||||
|
||||
if (scanner.peek() !== '>') {
|
||||
// Now we're essentially in the "After DOCTYPE name state" of the tokenizer,
|
||||
// but we're not looking at space or `>`.
|
||||
|
||||
// this should be "public" or "system".
|
||||
var publicOrSystem = HTMLTools.asciiLowerCase(scanner.rest().slice(0, 6));
|
||||
|
||||
if (publicOrSystem === 'system') {
|
||||
scanner.pos += 6;
|
||||
requireSpaces(scanner);
|
||||
systemId = getDoctypeQuotedString(scanner);
|
||||
skipSpaces(scanner);
|
||||
if (scanner.peek() !== '>')
|
||||
scanner.fatal("Malformed DOCTYPE");
|
||||
} else if (publicOrSystem === 'public') {
|
||||
scanner.pos += 6;
|
||||
requireSpaces(scanner);
|
||||
publicId = getDoctypeQuotedString(scanner);
|
||||
if (scanner.peek() !== '>') {
|
||||
requireSpaces(scanner);
|
||||
if (scanner.peek() !== '>') {
|
||||
systemId = getDoctypeQuotedString(scanner);
|
||||
skipSpaces(scanner);
|
||||
if (scanner.peek() !== '>')
|
||||
scanner.fatal("Malformed DOCTYPE");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scanner.fatal("Expected PUBLIC or SYSTEM in DOCTYPE");
|
||||
}
|
||||
}
|
||||
|
||||
// looking at `>`
|
||||
scanner.pos++;
|
||||
var result = { t: 'Doctype',
|
||||
v: scanner.input.slice(start, scanner.pos),
|
||||
name: name };
|
||||
|
||||
if (systemId)
|
||||
result.systemId = systemId;
|
||||
if (publicId)
|
||||
result.publicId = publicId;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// The special character `{` is only allowed as the first character
|
||||
// of a Chars, so that we have a chance to detect template tags.
|
||||
var getChars = makeRegexMatcher(/^[^&<\u0000][^&<\u0000{]*/);
|
||||
|
||||
var assertIsTemplateTag = function (x) {
|
||||
if (! (x instanceof HTMLTools.TemplateTag))
|
||||
throw new Error("Expected an instance of HTMLTools.TemplateTag");
|
||||
return x;
|
||||
};
|
||||
|
||||
// Returns the next HTML token, or `null` if we reach EOF.
|
||||
//
|
||||
// Note that if we have a `getTemplateTag` function that sometimes
|
||||
// consumes characters and emits nothing (e.g. in the case of template
|
||||
// comments), we may go from not-at-EOF to at-EOF and return `null`,
|
||||
// while otherwise we always find some token to return.
|
||||
getHTMLToken = HTMLTools.Parse.getHTMLToken = function (scanner, dataMode) {
|
||||
var result = null;
|
||||
if (scanner.getTemplateTag) {
|
||||
// Try to parse a template tag by calling out to the provided
|
||||
// `getTemplateTag` function. If the function returns `null` but
|
||||
// consumes characters, it must have parsed a comment or something,
|
||||
// so we loop and try it again. If it ever returns `null` without
|
||||
// consuming anything, that means it didn't see anything interesting
|
||||
// so we look for a normal token. If it returns a truthy value,
|
||||
// the value must be instanceof HTMLTools.TemplateTag. We wrap it
|
||||
// in a Special token.
|
||||
var lastPos = scanner.pos;
|
||||
result = scanner.getTemplateTag(
|
||||
scanner,
|
||||
(dataMode === 'rcdata' ? TEMPLATE_TAG_POSITION.IN_RCDATA :
|
||||
(dataMode === 'rawtext' ? TEMPLATE_TAG_POSITION.IN_RAWTEXT :
|
||||
TEMPLATE_TAG_POSITION.ELEMENT)));
|
||||
|
||||
if (result)
|
||||
return { t: 'TemplateTag', v: assertIsTemplateTag(result) };
|
||||
else if (scanner.pos > lastPos)
|
||||
return null;
|
||||
}
|
||||
|
||||
var chars = getChars(scanner);
|
||||
if (chars)
|
||||
return { t: 'Chars',
|
||||
v: convertCRLF(chars) };
|
||||
|
||||
var ch = scanner.peek();
|
||||
if (! ch)
|
||||
return null; // EOF
|
||||
|
||||
if (ch === '\u0000')
|
||||
scanner.fatal("Illegal NULL character");
|
||||
|
||||
if (ch === '&') {
|
||||
if (dataMode !== 'rawtext') {
|
||||
var charRef = getCharacterReference(scanner);
|
||||
if (charRef)
|
||||
return charRef;
|
||||
}
|
||||
|
||||
scanner.pos++;
|
||||
return { t: 'Chars',
|
||||
v: '&' };
|
||||
}
|
||||
|
||||
// If we're here, we're looking at `<`.
|
||||
|
||||
if (scanner.peek() === '<' && dataMode) {
|
||||
// don't interpret tags
|
||||
scanner.pos++;
|
||||
return { t: 'Chars',
|
||||
v: '<' };
|
||||
}
|
||||
|
||||
// `getTag` will claim anything starting with `<` not followed by `!`.
|
||||
// `getComment` takes `<!--` and getDoctype takes `<!doctype`.
|
||||
result = (getTagToken(scanner) || getComment(scanner) || getDoctype(scanner));
|
||||
|
||||
if (result)
|
||||
return result;
|
||||
|
||||
scanner.fatal("Unexpected `<!` directive.");
|
||||
};
|
||||
|
||||
var getTagName = makeRegexMatcher(/^[a-zA-Z][^\f\n\r\t />{]*/);
|
||||
var getClangle = makeRegexMatcher(/^>/);
|
||||
var getSlash = makeRegexMatcher(/^\//);
|
||||
var getAttributeName = makeRegexMatcher(/^[^>/\u0000"'<=\f\n\r\t ][^\f\n\r\t /=>"'<\u0000]*/);
|
||||
|
||||
// Try to parse `>` or `/>`, mutating `tag` to be self-closing in the latter
|
||||
// case (and failing fatally if `/` isn't followed by `>`).
|
||||
// Return tag if successful.
|
||||
var handleEndOfTag = function (scanner, tag) {
|
||||
if (getClangle(scanner))
|
||||
return tag;
|
||||
|
||||
if (getSlash(scanner)) {
|
||||
if (! getClangle(scanner))
|
||||
scanner.fatal("Expected `>` after `/`");
|
||||
tag.isSelfClosing = true;
|
||||
return tag;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Scan a quoted or unquoted attribute value (omit `quote` for unquoted).
|
||||
var getAttributeValue = function (scanner, quote) {
|
||||
if (quote) {
|
||||
if (scanner.peek() !== quote)
|
||||
return null;
|
||||
scanner.pos++;
|
||||
}
|
||||
|
||||
var tokens = [];
|
||||
var charsTokenToExtend = null;
|
||||
|
||||
var charRef;
|
||||
while (true) {
|
||||
var ch = scanner.peek();
|
||||
var templateTag;
|
||||
var curPos = scanner.pos;
|
||||
if (quote && ch === quote) {
|
||||
scanner.pos++;
|
||||
return tokens;
|
||||
} else if ((! quote) && (HTML_SPACE.test(ch) || ch === '>')) {
|
||||
return tokens;
|
||||
} else if (! ch) {
|
||||
scanner.fatal("Unclosed attribute in tag");
|
||||
} else if (quote ? ch === '\u0000' : ('\u0000"\'<=`'.indexOf(ch) >= 0)) {
|
||||
scanner.fatal("Unexpected character in attribute value");
|
||||
} else if (ch === '&' &&
|
||||
(charRef = getCharacterReference(scanner, true,
|
||||
quote || '>'))) {
|
||||
tokens.push(charRef);
|
||||
charsTokenToExtend = null;
|
||||
} else if (scanner.getTemplateTag &&
|
||||
((templateTag = scanner.getTemplateTag(
|
||||
scanner, TEMPLATE_TAG_POSITION.IN_ATTRIBUTE)) ||
|
||||
scanner.pos > curPos /* `{{! comment}}` */)) {
|
||||
if (templateTag) {
|
||||
tokens.push({t: 'TemplateTag',
|
||||
v: assertIsTemplateTag(templateTag)});
|
||||
charsTokenToExtend = null;
|
||||
}
|
||||
} else {
|
||||
if (! charsTokenToExtend) {
|
||||
charsTokenToExtend = { t: 'Chars', v: '' };
|
||||
tokens.push(charsTokenToExtend);
|
||||
}
|
||||
charsTokenToExtend.v += (ch === '\r' ? '\n' : ch);
|
||||
scanner.pos++;
|
||||
if (quote && ch === '\r' && scanner.peek() === '\n')
|
||||
scanner.pos++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
getTagToken = HTMLTools.Parse.getTagToken = function (scanner) {
|
||||
if (! (scanner.peek() === '<' && scanner.rest().charAt(1) !== '!'))
|
||||
return null;
|
||||
scanner.pos++;
|
||||
|
||||
var tag = { t: 'Tag' };
|
||||
|
||||
// now looking at the character after `<`, which is not a `!`
|
||||
if (scanner.peek() === '/') {
|
||||
tag.isEnd = true;
|
||||
scanner.pos++;
|
||||
}
|
||||
|
||||
var tagName = getTagName(scanner);
|
||||
if (! tagName)
|
||||
scanner.fatal("Expected tag name after `<`");
|
||||
tag.n = HTMLTools.properCaseTagName(tagName);
|
||||
|
||||
if (scanner.peek() === '/' && tag.isEnd)
|
||||
scanner.fatal("End tag can't have trailing slash");
|
||||
if (handleEndOfTag(scanner, tag))
|
||||
return tag;
|
||||
|
||||
if (scanner.isEOF())
|
||||
scanner.fatal("Unclosed `<`");
|
||||
|
||||
if (! HTML_SPACE.test(scanner.peek()))
|
||||
// e.g. `<a{{b}}>`
|
||||
scanner.fatal("Expected space after tag name");
|
||||
|
||||
// we're now in "Before attribute name state" of the tokenizer
|
||||
skipSpaces(scanner);
|
||||
|
||||
if (scanner.peek() === '/' && tag.isEnd)
|
||||
scanner.fatal("End tag can't have trailing slash");
|
||||
if (handleEndOfTag(scanner, tag))
|
||||
return tag;
|
||||
|
||||
if (tag.isEnd)
|
||||
scanner.fatal("End tag can't have attributes");
|
||||
|
||||
tag.attrs = {};
|
||||
var nondynamicAttrs = tag.attrs;
|
||||
|
||||
while (true) {
|
||||
// Note: at the top of this loop, we've already skipped any spaces.
|
||||
|
||||
// This will be set to true if after parsing the attribute, we should
|
||||
// require spaces (or else an end of tag, i.e. `>` or `/>`).
|
||||
var spacesRequiredAfter = false;
|
||||
|
||||
// first, try for a template tag.
|
||||
var curPos = scanner.pos;
|
||||
var templateTag = (scanner.getTemplateTag &&
|
||||
scanner.getTemplateTag(
|
||||
scanner, TEMPLATE_TAG_POSITION.IN_START_TAG));
|
||||
if (templateTag || (scanner.pos > curPos)) {
|
||||
if (templateTag) {
|
||||
if (tag.attrs === nondynamicAttrs)
|
||||
tag.attrs = [nondynamicAttrs];
|
||||
tag.attrs.push({ t: 'TemplateTag',
|
||||
v: assertIsTemplateTag(templateTag) });
|
||||
} // else, must have scanned a `{{! comment}}`
|
||||
|
||||
spacesRequiredAfter = true;
|
||||
} else {
|
||||
|
||||
var attributeName = getAttributeName(scanner);
|
||||
if (! attributeName)
|
||||
scanner.fatal("Expected attribute name in tag");
|
||||
// Throw error on `{` in attribute name. This provides *some* error message
|
||||
// if someone writes `<a x{{y}}>` or `<a x{{y}}=z>`. The HTML tokenization
|
||||
// spec doesn't say that `{` is invalid, but the DOM API (setAttribute) won't
|
||||
// allow it, so who cares.
|
||||
if (attributeName.indexOf('{') >= 0)
|
||||
scanner.fatal("Unexpected `{` in attribute name.");
|
||||
attributeName = HTMLTools.properCaseAttributeName(attributeName);
|
||||
|
||||
if (hasOwnProperty.call(nondynamicAttrs, attributeName))
|
||||
scanner.fatal("Duplicate attribute in tag: " + attributeName);
|
||||
|
||||
nondynamicAttrs[attributeName] = [];
|
||||
|
||||
skipSpaces(scanner);
|
||||
|
||||
if (handleEndOfTag(scanner, tag))
|
||||
return tag;
|
||||
|
||||
var ch = scanner.peek();
|
||||
if (! ch)
|
||||
scanner.fatal("Unclosed <");
|
||||
if ('\u0000"\'<'.indexOf(ch) >= 0)
|
||||
scanner.fatal("Unexpected character after attribute name in tag");
|
||||
|
||||
if (ch === '=') {
|
||||
scanner.pos++;
|
||||
|
||||
skipSpaces(scanner);
|
||||
|
||||
ch = scanner.peek();
|
||||
if (! ch)
|
||||
scanner.fatal("Unclosed <");
|
||||
if ('\u0000><=`'.indexOf(ch) >= 0)
|
||||
scanner.fatal("Unexpected character after = in tag");
|
||||
|
||||
if ((ch === '"') || (ch === "'"))
|
||||
nondynamicAttrs[attributeName] = getAttributeValue(scanner, ch);
|
||||
else
|
||||
nondynamicAttrs[attributeName] = getAttributeValue(scanner);
|
||||
|
||||
spacesRequiredAfter = true;
|
||||
}
|
||||
}
|
||||
// now we are in the "post-attribute" position, whether it was a template tag
|
||||
// attribute (like `{{x}}`) or a normal one (like `x` or `x=y`).
|
||||
|
||||
if (handleEndOfTag(scanner, tag))
|
||||
return tag;
|
||||
|
||||
if (scanner.isEOF())
|
||||
scanner.fatal("Unclosed `<`");
|
||||
|
||||
if (spacesRequiredAfter)
|
||||
requireSpaces(scanner);
|
||||
else
|
||||
skipSpaces(scanner);
|
||||
|
||||
if (handleEndOfTag(scanner, tag))
|
||||
return tag;
|
||||
}
|
||||
};
|
||||
|
||||
TEMPLATE_TAG_POSITION = HTMLTools.TEMPLATE_TAG_POSITION = {
|
||||
ELEMENT: 1,
|
||||
IN_START_TAG: 2,
|
||||
IN_ATTRIBUTE: 3,
|
||||
IN_RCDATA: 4,
|
||||
IN_RAWTEXT: 5
|
||||
};
|
||||
|
||||
// tagName must be proper case
|
||||
isLookingAtEndTag = function (scanner, tagName) {
|
||||
var rest = scanner.rest();
|
||||
var pos = 0; // into rest
|
||||
var firstPart = /^<\/([a-zA-Z]+)/.exec(rest);
|
||||
if (firstPart &&
|
||||
HTMLTools.properCaseTagName(firstPart[1]) === tagName) {
|
||||
// we've seen `</foo`, now see if the end tag continues
|
||||
pos += firstPart[0].length;
|
||||
while (pos < rest.length && HTML_SPACE.test(rest.charAt(pos)))
|
||||
pos++;
|
||||
if (pos < rest.length && rest.charAt(pos) === '>')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -1,344 +0,0 @@
|
||||
var Scanner = HTMLTools.Scanner;
|
||||
var getComment = HTMLTools.Parse.getComment;
|
||||
var getDoctype = HTMLTools.Parse.getDoctype;
|
||||
var getHTMLToken = HTMLTools.Parse.getHTMLToken;
|
||||
|
||||
// "tokenize" is not really a great operation for real use, because
|
||||
// it ignores the special content rules for tags like "style" and
|
||||
// "script".
|
||||
var tokenize = function (input) {
|
||||
var scanner = new Scanner(input);
|
||||
var tokens = [];
|
||||
while (! scanner.isEOF()) {
|
||||
var token = getHTMLToken(scanner);
|
||||
if (token)
|
||||
tokens.push(token);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
};
|
||||
|
||||
|
||||
Tinytest.add("html-tools - comments", function (test) {
|
||||
var succeed = function (input, content) {
|
||||
var scanner = new Scanner(input);
|
||||
var result = getComment(scanner);
|
||||
test.isTrue(result);
|
||||
test.equal(scanner.pos, content.length + 7);
|
||||
test.equal(result, {
|
||||
t: 'Comment',
|
||||
v: content
|
||||
});
|
||||
};
|
||||
|
||||
var ignore = function (input) {
|
||||
var scanner = new Scanner(input);
|
||||
var result = getComment(scanner);;
|
||||
test.isFalse(result);
|
||||
test.equal(scanner.pos, 0);
|
||||
};
|
||||
|
||||
var fatal = function (input, messageContains) {
|
||||
var scanner = new Scanner(input);
|
||||
var error;
|
||||
try {
|
||||
getComment(scanner);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
test.isTrue(error);
|
||||
if (error)
|
||||
test.isTrue(messageContains && error.message.indexOf(messageContains) >= 0, error.message);
|
||||
};
|
||||
|
||||
test.equal(getComment(new Scanner("<!-- hello -->")),
|
||||
{ t: 'Comment', v: ' hello ' });
|
||||
|
||||
ignore("<!DOCTYPE>");
|
||||
ignore("<!-a");
|
||||
ignore("<--");
|
||||
ignore("<!");
|
||||
ignore("abc");
|
||||
ignore("<a");
|
||||
|
||||
fatal('<!--', 'Unclosed');
|
||||
fatal('<!---', 'Unclosed');
|
||||
fatal('<!----', 'Unclosed');
|
||||
fatal('<!-- -', 'Unclosed');
|
||||
fatal('<!-- --', 'Unclosed');
|
||||
fatal('<!-- -- abcd', 'Unclosed');
|
||||
fatal('<!-- ->', 'Unclosed');
|
||||
fatal('<!-- a--b -->', 'cannot contain');
|
||||
fatal('<!--x--->', 'must end at first');
|
||||
|
||||
fatal('<!-- a\u0000b -->', 'cannot contain');
|
||||
fatal('<!--\u0000 x-->', 'cannot contain');
|
||||
|
||||
succeed('<!---->', '');
|
||||
succeed('<!---x-->', '-x');
|
||||
succeed('<!--x-->', 'x');
|
||||
succeed('<!-- hello - - world -->', ' hello - - world ');
|
||||
});
|
||||
|
||||
Tinytest.add("html-tools - doctype", function (test) {
|
||||
var succeed = function (input, expectedProps) {
|
||||
var scanner = new Scanner(input);
|
||||
var result = getDoctype(scanner);
|
||||
test.isTrue(result);
|
||||
test.equal(scanner.pos, result.v.length);
|
||||
test.equal(input.slice(0, result.v.length), result.v);
|
||||
var actualProps = _.extend({}, result);
|
||||
delete actualProps.t;
|
||||
delete actualProps.v;
|
||||
test.equal(actualProps, expectedProps);
|
||||
};
|
||||
|
||||
var fatal = function (input, messageContains) {
|
||||
var scanner = new Scanner(input);
|
||||
var error;
|
||||
try {
|
||||
getDoctype(scanner);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
test.isTrue(error);
|
||||
if (messageContains)
|
||||
test.isTrue(error.message.indexOf(messageContains) >= 0, error.message);
|
||||
};
|
||||
|
||||
test.equal(getDoctype(new Scanner("<!DOCTYPE html>x")),
|
||||
{ t: 'Doctype',
|
||||
v: '<!DOCTYPE html>',
|
||||
name: 'html' });
|
||||
|
||||
test.equal(getDoctype(new Scanner("<!DOCTYPE html SYSTEM 'about:legacy-compat'>x")),
|
||||
{ t: 'Doctype',
|
||||
v: "<!DOCTYPE html SYSTEM 'about:legacy-compat'>",
|
||||
name: 'html',
|
||||
systemId: 'about:legacy-compat' });
|
||||
|
||||
test.equal(getDoctype(new Scanner("<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0//EN'>x")),
|
||||
{ t: 'Doctype',
|
||||
v: "<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0//EN'>",
|
||||
name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN' });
|
||||
|
||||
test.equal(getDoctype(new Scanner("<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0//EN' 'http://www.w3.org/TR/html4/strict.dtd'>x")),
|
||||
{ t: 'Doctype',
|
||||
v: "<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0//EN' 'http://www.w3.org/TR/html4/strict.dtd'>",
|
||||
name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN',
|
||||
systemId: 'http://www.w3.org/TR/html4/strict.dtd' });
|
||||
|
||||
succeed('<!DOCTYPE html>', {name: 'html'});
|
||||
succeed('<!DOCTYPE htML>', {name: 'html'});
|
||||
succeed('<!DOCTYPE HTML>', {name: 'html'});
|
||||
succeed('<!doctype html>', {name: 'html'});
|
||||
succeed('<!doctYPE html>', {name: 'html'});
|
||||
succeed('<!DOCTYPE html \u000c>', {name: 'html'});
|
||||
fatal('<!DOCTYPE', 'Expected space');
|
||||
fatal('<!DOCTYPE ', 'Malformed DOCTYPE');
|
||||
fatal('<!DOCTYPE ', 'Malformed DOCTYPE');
|
||||
fatal('<!DOCTYPE>', 'Expected space');
|
||||
fatal('<!DOCTYPE >', 'Malformed DOCTYPE');
|
||||
fatal('<!DOCTYPE\u0000', 'Expected space');
|
||||
fatal('<!DOCTYPE \u0000', 'Malformed DOCTYPE');
|
||||
fatal('<!DOCTYPE html\u0000>', 'Malformed DOCTYPE');
|
||||
fatal('<!DOCTYPE html', 'Malformed DOCTYPE');
|
||||
|
||||
succeed('<!DOCTYPE html SYSTEM "about:legacy-compat">', {name: 'html', systemId: 'about:legacy-compat'});
|
||||
succeed('<!doctype HTML system "about:legacy-compat">', {name: 'html', systemId: 'about:legacy-compat'});
|
||||
succeed("<!DOCTYPE html SYSTEM 'about:legacy-compat'>", {name: 'html', systemId: 'about:legacy-compat'});
|
||||
succeed("<!dOcTyPe HtMl sYsTeM 'about:legacy-compat'>", {name: 'html', systemId: 'about:legacy-compat'});
|
||||
succeed('<!DOCTYPE html\tSYSTEM\t"about:legacy-compat" \t>', {name: 'html', systemId: 'about:legacy-compat'});
|
||||
fatal('<!DOCTYPE html SYSTE "about:legacy-compat">', 'Expected PUBLIC or SYSTEM');
|
||||
fatal('<!DOCTYPE html SYSTE', 'Expected PUBLIC or SYSTEM');
|
||||
fatal('<!DOCTYPE html SYSTEM"about:legacy-compat">', 'Expected space');
|
||||
fatal('<!DOCTYPE html SYSTEM');
|
||||
fatal('<!DOCTYPE html SYSTEM ');
|
||||
fatal('<!DOCTYPE html SYSTEM>');
|
||||
fatal('<!DOCTYPE html SYSTEM >');
|
||||
fatal('<!DOCTYPE html SYSTEM ">">');
|
||||
fatal('<!DOCTYPE html SYSTEM "\u0000about:legacy-compat">');
|
||||
fatal('<!DOCTYPE html SYSTEM "about:legacy-compat\u0000">');
|
||||
fatal('<!DOCTYPE html SYSTEM "');
|
||||
fatal('<!DOCTYPE html SYSTEM "">');
|
||||
fatal('<!DOCTYPE html SYSTEM \'');
|
||||
fatal('<!DOCTYPE html SYSTEM\'a\'>');
|
||||
fatal('<!DOCTYPE html SYSTEM about:legacy-compat>');
|
||||
|
||||
succeed('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN">',
|
||||
{ name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN'});
|
||||
succeed('<!DOCTYPE html PUBLIC \'-//W3C//DTD HTML 4.0//EN\'>',
|
||||
{ name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN'});
|
||||
succeed('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">',
|
||||
{ name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN',
|
||||
systemId: 'http://www.w3.org/TR/REC-html40/strict.dtd'});
|
||||
succeed('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN" \'http://www.w3.org/TR/REC-html40/strict.dtd\'>',
|
||||
{ name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN',
|
||||
systemId: 'http://www.w3.org/TR/REC-html40/strict.dtd'});
|
||||
succeed('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'http://www.w3.org/TR/REC-html40/strict.dtd\'>',
|
||||
{ name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN',
|
||||
systemId: 'http://www.w3.org/TR/REC-html40/strict.dtd'});
|
||||
succeed('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\'\t\'http://www.w3.org/TR/REC-html40/strict.dtd\' >',
|
||||
{ name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN',
|
||||
systemId: 'http://www.w3.org/TR/REC-html40/strict.dtd'});
|
||||
fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'http://www.w3.org/TR/REC-html40/strict.dtd\'');
|
||||
fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'http://www.w3.org/TR/REC-html40/strict.dtd\'');
|
||||
fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'http://www.w3.org/TR/REC-html40/strict.dtd');
|
||||
fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'');
|
||||
fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' ');
|
||||
fatal('<!DOCTYPE html public \'- ');
|
||||
fatal('<!DOCTYPE html public>');
|
||||
fatal('<!DOCTYPE html public "-//W3C//DTD HTML 4.0//EN""http://www.w3.org/TR/REC-html40/strict.dtd">');
|
||||
});
|
||||
|
||||
Tinytest.add("html-tools - tokenize", function (test) {
|
||||
|
||||
var fatal = function (input, messageContains) {
|
||||
var error;
|
||||
try {
|
||||
tokenize(input);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
test.isTrue(error);
|
||||
if (messageContains)
|
||||
test.isTrue(error.message.indexOf(messageContains) >= 0, error.message);
|
||||
};
|
||||
|
||||
|
||||
test.equal(tokenize(''), []);
|
||||
test.equal(tokenize('abc'), [{t: 'Chars', v: 'abc'}]);
|
||||
test.equal(tokenize('&'), [{t: 'Chars', v: '&'}]);
|
||||
test.equal(tokenize('&'), [{t: 'CharRef', v: '&', cp: [38]}]);
|
||||
test.equal(tokenize('ok fine'),
|
||||
[{t: 'Chars', v: 'ok'},
|
||||
{t: 'CharRef', v: ' ', cp: [32]},
|
||||
{t: 'Chars', v: 'fine'}]);
|
||||
|
||||
test.equal(tokenize('a<!--b-->c'),
|
||||
[{t: 'Chars',
|
||||
v: 'a'},
|
||||
{t: 'Comment',
|
||||
v: 'b'},
|
||||
{t: 'Chars',
|
||||
v: 'c'}]);
|
||||
|
||||
test.equal(tokenize('<a>'), [{t: 'Tag', n: 'a'}]);
|
||||
|
||||
fatal('<');
|
||||
fatal('<x');
|
||||
fatal('<x ');
|
||||
fatal('<x a');
|
||||
fatal('<x a ');
|
||||
fatal('<x a =');
|
||||
fatal('<x a = ');
|
||||
fatal('<x a = b');
|
||||
fatal('<x a = "b');
|
||||
fatal('<x a = \'b');
|
||||
fatal('<x a = b ');
|
||||
fatal('<x a = b /');
|
||||
test.equal(tokenize('<x a = b />'),
|
||||
[{t: 'Tag', n: 'x',
|
||||
attrs: { a: [{t: 'Chars', v: 'b'}] },
|
||||
isSelfClosing: true}]);
|
||||
|
||||
test.equal(tokenize('<a>X</a>'),
|
||||
[{t: 'Tag', n: 'a'},
|
||||
{t: 'Chars', v: 'X'},
|
||||
{t: 'Tag', n: 'a', isEnd: true}]);
|
||||
|
||||
fatal('<x a a>'); // duplicate attribute value
|
||||
test.equal(tokenize('<a b >'),
|
||||
[{t: 'Tag', n: 'a', attrs: { b: [] }}]);
|
||||
fatal('< a>');
|
||||
fatal('< /a>');
|
||||
fatal('</ a>');
|
||||
|
||||
// Slash does not end an unquoted attribute, interestingly
|
||||
test.equal(tokenize('<a b=/>'),
|
||||
[{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '/'}] }}]);
|
||||
|
||||
test.equal(tokenize('<a b="c" d=e f=\'g\' h \t>'),
|
||||
[{t: 'Tag', n: 'a',
|
||||
attrs: { b: [{t: 'Chars', v: 'c'}],
|
||||
d: [{t: 'Chars', v: 'e'}],
|
||||
f: [{t: 'Chars', v: 'g'}],
|
||||
h: [] }}]);
|
||||
|
||||
fatal('</a b="c" d=e f=\'g\' h \t\u0000>');
|
||||
fatal('</a b="c" d=ef=\'g\' h \t>');
|
||||
fatal('</a b="c"d=e f=\'g\' h \t>');
|
||||
|
||||
test.equal(tokenize('<a/>'), [{t: 'Tag', n: 'a', isSelfClosing: true}]);
|
||||
|
||||
fatal('<a/ >');
|
||||
fatal('<a/b>');
|
||||
fatal('<a b=c`>');
|
||||
fatal('<a b=c<>');
|
||||
|
||||
test.equal(tokenize('<a# b0="c@" d1=e2 f#=\'g \' h \t>'),
|
||||
[{t: 'Tag', n: 'a#',
|
||||
attrs: { b0: [{t: 'Chars', v: 'c@'}],
|
||||
d1: [{t: 'Chars', v: 'e2'}],
|
||||
'f#': [{t: 'Chars', v: 'g '}],
|
||||
h: [] }}]);
|
||||
|
||||
test.equal(tokenize('<div class=""></div>'),
|
||||
[{t: 'Tag', n: 'div', attrs: { 'class': [] }},
|
||||
{t: 'Tag', n: 'div', isEnd: true}]);
|
||||
|
||||
test.equal(tokenize('<div class="&">'),
|
||||
[{t: 'Tag', n: 'div', attrs: { 'class': [{t: 'Chars', v: '&'}] }}]);
|
||||
test.equal(tokenize('<div class=&>'),
|
||||
[{t: 'Tag', n: 'div', attrs: { 'class': [{t: 'Chars', v: '&'}] }}]);
|
||||
test.equal(tokenize('<div class=&>'),
|
||||
[{t: 'Tag', n: 'div', attrs: { 'class': [{t: 'CharRef', v: '&', cp: [38]}] }}]);
|
||||
|
||||
test.equal(tokenize('<div class=aa&𝕫∾̳&bb>'),
|
||||
[{t: 'Tag', n: 'div', attrs: { 'class': [
|
||||
{t: 'Chars', v: 'aa&'},
|
||||
{t: 'CharRef', v: '𝕫', cp: [120171]},
|
||||
{t: 'CharRef', v: '∾̳', cp: [8766, 819]},
|
||||
{t: 'Chars', v: '&bb'}
|
||||
] }}]);
|
||||
|
||||
test.equal(tokenize('<div class="aa &𝕫∾̳& bb">'),
|
||||
[{t: 'Tag', n: 'div', attrs: { 'class': [
|
||||
{t: 'Chars', v: 'aa &'},
|
||||
{t: 'CharRef', v: '𝕫', cp: [120171]},
|
||||
{t: 'CharRef', v: '∾̳', cp: [8766, 819]},
|
||||
{t: 'Chars', v: '& bb'}
|
||||
] }}]);
|
||||
|
||||
test.equal(tokenize('<a b="\'`<>&">'),
|
||||
[{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '\'`<>&'}] }}]);
|
||||
test.equal(tokenize('<a b=\'"`<>&\'>'),
|
||||
[{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '"`<>&'}] }}]);
|
||||
|
||||
fatal('>');
|
||||
fatal('>c');
|
||||
test.equal(tokenize('<a b=>c>'),
|
||||
[{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '>c' }] }}]);
|
||||
test.equal(tokenize('<a b=">c">'),
|
||||
[{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '>c' }] }}]);
|
||||
fatal('<a b=>>');
|
||||
fatal('<a b=">">');
|
||||
fatal('<a b=">=">');
|
||||
|
||||
fatal('<!');
|
||||
fatal('<!x>');
|
||||
|
||||
fatal('<a{{b}}>');
|
||||
fatal('<{{a}}>');
|
||||
fatal('</a b=c>'); // end tag can't have attributes
|
||||
fatal('</a/>'); // end tag can't be self-closing
|
||||
fatal('</a />');
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
|
||||
HTMLTools = {};
|
||||
HTMLTools.Parse = {};
|
||||
|
||||
var asciiLowerCase = HTMLTools.asciiLowerCase = function (str) {
|
||||
return str.replace(/[A-Z]/g, function (c) {
|
||||
return String.fromCharCode(c.charCodeAt(0) + 32);
|
||||
});
|
||||
};
|
||||
|
||||
var svgCamelCaseAttributes = 'attributeName attributeType baseFrequency baseProfile calcMode clipPathUnits contentScriptType contentStyleType diffuseConstant edgeMode externalResourcesRequired filterRes filterUnits glyphRef glyphRef gradientTransform gradientTransform gradientUnits gradientUnits kernelMatrix kernelUnitLength kernelUnitLength kernelUnitLength keyPoints keySplines keyTimes lengthAdjust limitingConeAngle markerHeight markerUnits markerWidth maskContentUnits maskUnits numOctaves pathLength patternContentUnits patternTransform patternUnits pointsAtX pointsAtY pointsAtZ preserveAlpha preserveAspectRatio primitiveUnits refX refY repeatCount repeatDur requiredExtensions requiredFeatures specularConstant specularExponent specularExponent spreadMethod spreadMethod startOffset stdDeviation stitchTiles surfaceScale surfaceScale systemLanguage tableValues targetX targetY textLength textLength viewBox viewTarget xChannelSelector yChannelSelector zoomAndPan'.split(' ');
|
||||
|
||||
var properAttributeCaseMap = (function (map) {
|
||||
for (var i = 0; i < svgCamelCaseAttributes.length; i++) {
|
||||
var a = svgCamelCaseAttributes[i];
|
||||
map[asciiLowerCase(a)] = a;
|
||||
}
|
||||
return map;
|
||||
})({});
|
||||
|
||||
var properTagCaseMap = (function (map) {
|
||||
var knownElements = HTML.knownElementNames;
|
||||
for (var i = 0; i < knownElements.length; i++) {
|
||||
var a = knownElements[i];
|
||||
map[asciiLowerCase(a)] = a;
|
||||
}
|
||||
return map;
|
||||
})({});
|
||||
|
||||
// Take a tag name in any case and make it the proper case for HTML.
|
||||
//
|
||||
// Modern browsers let you embed SVG in HTML, but SVG elements are special
|
||||
// in that they have a case-sensitive DOM API (nodeName, getAttribute,
|
||||
// setAttribute). For example, it has to be `setAttribute("viewBox")`,
|
||||
// not `"viewbox"`. However, the browser's HTML parser is NOT case sensitive
|
||||
// and will fix the case for you, so if you write `<svg viewbox="...">`
|
||||
// you actually get a `"viewBox"` attribute. Any HTML-parsing toolchain
|
||||
// must do the same.
|
||||
HTMLTools.properCaseTagName = function (name) {
|
||||
var lowered = asciiLowerCase(name);
|
||||
return properTagCaseMap.hasOwnProperty(lowered) ?
|
||||
properTagCaseMap[lowered] : lowered;
|
||||
};
|
||||
|
||||
// See docs for properCaseTagName.
|
||||
HTMLTools.properCaseAttributeName = function (name) {
|
||||
var lowered = asciiLowerCase(name);
|
||||
return properAttributeCaseMap.hasOwnProperty(lowered) ?
|
||||
properAttributeCaseMap[lowered] : lowered;
|
||||
};
|
||||
1
packages/htmljs/.gitignore
vendored
1
packages/htmljs/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.build*
|
||||
@@ -1,427 +0,0 @@
|
||||
# HTMLjs
|
||||
|
||||
HTMLjs is a small library for expressing HTML trees with a concise
|
||||
syntax. It is used to render content in Blaze and to represent
|
||||
templates during compilation.
|
||||
|
||||
```
|
||||
var UL = HTML.UL, LI = HTML.LI, B = HTML.B;
|
||||
|
||||
HTML.toHTML(
|
||||
UL({id: 'mylist'},
|
||||
LI({'class': 'item'}, "Hello ", B("world"), "!"),
|
||||
LI({'class': 'item'}, "Goodbye, world")))
|
||||
```
|
||||
|
||||
```
|
||||
<ul id="mylist">
|
||||
<li class="item">Hello <b>world</b>!</li>
|
||||
<li class="item">Goodbye, world</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
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 `"<a>"` renders `<a>` in HTML, and `document.createTextNode("<a>")` 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 `<p>` tag or `HTML.INPUT` for an `<input>` 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)
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
|
||||
### 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 `<foo>` for some reason,
|
||||
you have to tell HTMLjs to create the `HTML.FOO` constructor for you
|
||||
using `HTML.ensureTag` or `HTML.getTag`.
|
||||
|
||||
HTMLjs's lists of known elements are public because they are useful to
|
||||
other packages that provide additional functions not found here, like
|
||||
functions for normalizing case.
|
||||
|
||||
|
||||
|
||||
## Foreign objects
|
||||
|
||||
Arbitrary objects are allowed in HTMLjs trees, which is useful for
|
||||
adapting HTMLjs to a wide variety of uses. Such objects are called
|
||||
foreign objects.
|
||||
|
||||
The one restriction on foreign objects is that they must be
|
||||
instances of a class -- so-called "constructed objects" (see
|
||||
`HTML.isConstructedObject`) -- so that they can be distinguished
|
||||
from the vanilla JS objects that represent attributes dictionaries
|
||||
when constructing Tags.
|
||||
|
||||
Functions are also considered foreign objects.
|
||||
|
||||
## HTML.getTag(tagName)
|
||||
|
||||
* `tagName` - A string in normalized case
|
||||
|
||||
Creates a tag constructor for `tagName`, assigns it to the `HTML`
|
||||
namespace object, and returns it.
|
||||
|
||||
For example, `HTML.getTag("p")` returns `HTML.P`. `HTML.getTag("foo")`
|
||||
will create and return `HTML.FOO`.
|
||||
|
||||
It's very important that `tagName` be in normalized case, or else
|
||||
an incorrect tag constructor will be registered and used henceforth.
|
||||
|
||||
|
||||
## HTML.ensureTag(tagName)
|
||||
|
||||
* `tagName` - A string in normalized case
|
||||
|
||||
Ensures that a tag constructor (like `HTML.FOO`) exists for a tag
|
||||
name (like `"foo"`), creating it if necessary. Like `HTML.getTag`
|
||||
but does not return the tag constructor.
|
||||
|
||||
|
||||
## HTML.isTagEnsured(tagName)
|
||||
|
||||
* `tagName` - A string in normalized case
|
||||
|
||||
Returns whether a particular tag is guaranteed to be available on
|
||||
the `HTML` object (under the name returned by `HTML.getSymbolName`).
|
||||
|
||||
Useful for code generators.
|
||||
|
||||
|
||||
## HTML.getSymbolName(tagName)
|
||||
|
||||
* `tagName` - A string in normalized case
|
||||
|
||||
Returns the name of the all-caps constructor (like `"FOO"`) for a
|
||||
tag name in normalized case (like `"foo"`).
|
||||
|
||||
In addition to converting `tagName` to all-caps, hyphens (`-`) in
|
||||
tag names are converted to underscores (`_`).
|
||||
|
||||
Useful for code generators.
|
||||
|
||||
|
||||
## HTML.knownElementNames
|
||||
|
||||
An array of all known HTML5 and SVG element names in normalized case.
|
||||
|
||||
|
||||
## HTML.knownSVGElementNames
|
||||
|
||||
An array of all known SVG element names in normalized case.
|
||||
|
||||
The `"a"` element is not included because it is primarily a non-SVG
|
||||
element.
|
||||
|
||||
|
||||
## HTML.voidElementNames
|
||||
|
||||
An array of all "void" element names in normalized case. Void
|
||||
elements are elements with a start tag and no end tag, such as BR,
|
||||
HR, IMG, and INPUT.
|
||||
|
||||
The HTML spec defines a closed class of void elements.
|
||||
|
||||
|
||||
## HTML.isKnownElement(tagName)
|
||||
|
||||
* `tagName` - A string in normalized case
|
||||
|
||||
Returns whether `tagName` is a known HTML5 or SVG element.
|
||||
|
||||
|
||||
## HTML.isKnownSVGElement(tagName)
|
||||
|
||||
* `tagName` - A string in normalized case
|
||||
|
||||
Returns whether `tagName` is the name of a known SVG element.
|
||||
|
||||
|
||||
## HTML.isVoidElement(tagName)
|
||||
|
||||
* `tagName` - A string in normalized case
|
||||
|
||||
Returns whether `tagName` is the name of a void element.
|
||||
|
||||
|
||||
## HTML.CharRef({html: ..., str: ...})
|
||||
|
||||
Represents a character reference like ` `.
|
||||
|
||||
A CharRef is not required for escaping special characters like `<`,
|
||||
which are automatically escaped by HTMLjs. For example,
|
||||
`HTML.toHTML("<")` is `"<"`. Also, now that browsers speak
|
||||
Unicode, non-ASCII characters typically do not need to be expressed
|
||||
as character references either. The purpose of `CharRef` is offer
|
||||
control over the generated HTML, allowing template engines to
|
||||
preserve any character references that they come across.
|
||||
|
||||
Constructing a CharRef requires two strings, the uninterpreted
|
||||
"HTML" form and the interpreted "string" form. Both are required
|
||||
to be present, and it is up to the caller to make sure the
|
||||
information is accurate.
|
||||
|
||||
Examples of valid CharRefs:
|
||||
|
||||
* `HTML.CharRef({html: '&', str: '&'})`
|
||||
* `HTML.CharRef({html: ' ', str: '\u00A0'})
|
||||
|
||||
Instance properties: `.html`, `.str`
|
||||
|
||||
|
||||
## HTML.Comment(value)
|
||||
|
||||
* `value` - String
|
||||
|
||||
Represents an HTML Comment. For example, `HTML.Comment("foo")` represents
|
||||
the comment `<!--foo-->`.
|
||||
|
||||
The value string should not contain two consecutive hyphens (`--`) or start
|
||||
or end with a hyphen. If it does, the offending hyphens will be stripped
|
||||
before generating HTML.
|
||||
|
||||
Instance properties: `value`
|
||||
|
||||
|
||||
## HTML.Raw(value)
|
||||
|
||||
* `value` - String
|
||||
|
||||
Represents HTML code to be inserted verbatim. `value` must consist
|
||||
of a valid, complete fragment of HTML, with all tags closed and
|
||||
all required end tags present.
|
||||
|
||||
No security checks are performed, and no special characters are
|
||||
escaped. `Raw` should not be used if there are other ways of
|
||||
accomplishing the same result. HTML supplied by an application
|
||||
user should not be rendered unless the user is trusted, and even
|
||||
then, there could be strange results if required end tags are
|
||||
missing.
|
||||
|
||||
Instance properties: `value`
|
||||
|
||||
|
||||
## HTML.isArray(x)
|
||||
|
||||
Returns whether `x` is considered an array for the purposes of
|
||||
HTMLjs. An array is an object created using `[...]` or
|
||||
`new Array`.
|
||||
|
||||
This function is provided because there are several common ways to
|
||||
determine whether an object should be treated as an array in
|
||||
JavaScript.
|
||||
|
||||
|
||||
## HTML.isConstructedObject(x)
|
||||
|
||||
Returns whether `x` is a "constructed object," which is (loosely
|
||||
speaking) an object that was created with `new Foo` (for some `Foo`)
|
||||
rather than with `{...}` (a vanilla object). Vanilla objects are used
|
||||
as attribute dictionaries when constructing tags, while constructed
|
||||
objects are used as children.
|
||||
|
||||
For example, in `HTML.DIV({id:"foo"})`, `{id:"foo"}` is a vanilla
|
||||
object. In `HTML.DIV(HTML.SPAN("text"))`, the `HTML.SPAN` is a
|
||||
constructed object.
|
||||
|
||||
A simple constructed object can be created as follows:
|
||||
|
||||
```
|
||||
var Foo = function () {};
|
||||
var x = new Foo; // x is a constructed object
|
||||
```
|
||||
|
||||
In particular, the test is that `x` is an object and `x.constructor`
|
||||
is set, but on a prototype of the object, not the object itself.
|
||||
The above example works because JavaScript sets
|
||||
`Foo.prototype.constructor = Foo` when you create a function `Foo`.
|
||||
|
||||
|
||||
## HTML.isNully(content)
|
||||
|
||||
Returns true if `content` is `null`, `undefined`, an empty array,
|
||||
or an array of only "nully" elements.
|
||||
|
||||
|
||||
## HTML.isValidAttributeName(name)
|
||||
|
||||
Returns whether `name` is a valid name for an attribute of an HTML tag
|
||||
or element. `name` must:
|
||||
|
||||
* Start with `:`, `_`, `A-Z` or `a-z`
|
||||
* Consist only of those characters plus `-`, `.`, and `0-9`.
|
||||
|
||||
Discussion: The HTML spec and the DOM API (`setAttribute`) have different
|
||||
definitions of what characters are legal in an attribute. The HTML
|
||||
parser is extremely permissive (allowing, for example, `<a %=%>`), 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, we allow ASCII chars
|
||||
that are known to be valid XML, valid HTML, and settable via `setAttribute`.
|
||||
|
||||
See <http://www.w3.org/TR/REC-xml/#NT-Name> and
|
||||
<http://dev.w3.org/html5/markup/syntax.html#syntax-attributes>.
|
||||
|
||||
|
||||
## HTML.flattenAttributes(attrs)
|
||||
|
||||
If `attrs` is an array, the attribute dictionaries in the array are
|
||||
combined into a single attributes dictionary, which is returned.
|
||||
Any "nully" attribute values (see `HTML.isNully`) are ignored in
|
||||
the process. If `attrs` is a single attribute dictionary, a copy
|
||||
is returned with any nully attributes removed. If `attrs` is
|
||||
equal to null or an empty array, `null` is returned.
|
||||
|
||||
Attribute dictionaries are combined by assigning the name/value
|
||||
pairs in array order, with later values overwriting previous
|
||||
values.
|
||||
|
||||
`attrs` must not contain any foreign objects.
|
||||
|
||||
|
||||
## HTML.toHTML(content)
|
||||
|
||||
* `content` - any HTMLjs content
|
||||
|
||||
Returns a string of HTML generated from `content`.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
HTML.toHTML(HTML.HR()) // => "<hr>"
|
||||
```
|
||||
|
||||
Foreign objects are not allowed in `content`. To generate HTML
|
||||
containing foreign objects, create a subclass of
|
||||
`HTML.ToHTMLVisitor` and override `visitObject`.
|
||||
|
||||
|
||||
## 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
|
||||
@@ -1,268 +0,0 @@
|
||||
|
||||
|
||||
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([]) : [];
|
||||
HTML.Tag.prototype.htmljsType = HTML.Tag.htmljsType = ['Tag'];
|
||||
|
||||
// Given "p" create the function `HTML.P`.
|
||||
var makeTagConstructor = function (tagName) {
|
||||
// HTMLTag is the per-tagName constructor of a HTML.Tag subclass
|
||||
var HTMLTag = function (/*arguments*/) {
|
||||
// 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 HTML.Tag) ? this : new HTMLTag;
|
||||
|
||||
var i = 0;
|
||||
var attrs = arguments.length && arguments[0];
|
||||
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 = SLICE.call(arguments, i);
|
||||
|
||||
return instance;
|
||||
};
|
||||
HTMLTag.prototype = new HTML.Tag;
|
||||
HTMLTag.prototype.constructor = HTMLTag;
|
||||
HTMLTag.prototype.tagName = tagName;
|
||||
|
||||
return HTMLTag;
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
instance.value = SLICE.call(arguments);
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
////////////////////////////// KNOWN ELEMENTS
|
||||
|
||||
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];
|
||||
};
|
||||
|
||||
HTML.ensureTag = function (tagName) {
|
||||
HTML.getTag(tagName); // don't return it
|
||||
};
|
||||
|
||||
HTML.isTagEnsured = function (tagName) {
|
||||
return HTML.isKnownElement(tagName);
|
||||
};
|
||||
|
||||
HTML.getSymbolName = function (tagName) {
|
||||
// "foo-bar" -> "FOO_BAR"
|
||||
return tagName.toUpperCase().replace(/-/g, '_');
|
||||
};
|
||||
|
||||
HTML.knownElementNames = 'a abbr acronym address applet area article aside audio b base basefont bdi bdo big blockquote body br button canvas caption center cite code col colgroup command data datagrid datalist dd del details dfn dir div dl dt em embed eventsource fieldset figcaption figure font footer form frame frameset h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins isindex kbd keygen label legend li link main map mark menu meta meter nav noframes noscript object ol optgroup option output p param pre progress q rp rt ruby s samp script section select small source span strike strong style sub summary sup table tbody td textarea tfoot th thead time title tr track tt u ul var video wbr'.split(' ');
|
||||
// (we add the SVG ones below)
|
||||
|
||||
HTML.knownSVGElementNames = 'altGlyph altGlyphDef altGlyphItem animate animateColor animateMotion animateTransform circle clipPath color-profile cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset fePointLight feSpecularLighting feSpotLight feTile feTurbulence filter font font-face font-face-format font-face-name font-face-src font-face-uri foreignObject g glyph glyphRef hkern image line linearGradient marker mask metadata missing-glyph path pattern polygon polyline radialGradient rect set stop style svg switch symbol text textPath title tref tspan use view vkern'.split(' ');
|
||||
// Append SVG element names to list of known element names
|
||||
HTML.knownElementNames = HTML.knownElementNames.concat(HTML.knownSVGElementNames);
|
||||
|
||||
HTML.voidElementNames = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' ');
|
||||
|
||||
// Speed up search through lists of known elements by creating internal "sets"
|
||||
// of strings.
|
||||
var YES = {yes:true};
|
||||
var makeSet = function (array) {
|
||||
var set = {};
|
||||
for (var i = 0; i < array.length; i++)
|
||||
set[array[i]] = YES;
|
||||
return set;
|
||||
};
|
||||
var voidElementSet = makeSet(HTML.voidElementNames);
|
||||
var knownElementSet = makeSet(HTML.knownElementNames);
|
||||
var knownSVGElementSet = makeSet(HTML.knownSVGElementNames);
|
||||
|
||||
HTML.isKnownElement = function (tagName) {
|
||||
return knownElementSet[tagName] === YES;
|
||||
};
|
||||
|
||||
HTML.isKnownSVGElement = function (tagName) {
|
||||
return knownSVGElementSet[tagName] === YES;
|
||||
};
|
||||
|
||||
HTML.isVoidElement = function (tagName) {
|
||||
return voidElementSet[tagName] === YES;
|
||||
};
|
||||
|
||||
|
||||
// Ensure tags for all known elements
|
||||
for (var i = 0; i < HTML.knownElementNames.length; i++)
|
||||
HTML.ensureTag(HTML.knownElementNames[i]);
|
||||
|
||||
|
||||
var CharRef = HTML.CharRef = function (attrs) {
|
||||
if (! (this instanceof CharRef))
|
||||
// called without `new`
|
||||
return new CharRef(attrs);
|
||||
|
||||
if (! (attrs && attrs.html && attrs.str))
|
||||
throw new Error(
|
||||
"HTML.CharRef must be constructed with ({html:..., str:...})");
|
||||
|
||||
this.html = attrs.html;
|
||||
this.str = attrs.str;
|
||||
};
|
||||
CharRef.prototype.htmljsType = CharRef.htmljsType = ['CharRef'];
|
||||
|
||||
var Comment = HTML.Comment = function (value) {
|
||||
if (! (this instanceof Comment))
|
||||
// called without `new`
|
||||
return new Comment(value);
|
||||
|
||||
if (typeof value !== 'string')
|
||||
throw new Error('HTML.Comment must be constructed with a string');
|
||||
|
||||
this.value = value;
|
||||
// Kill illegal hyphens in comment value (no way to escape them in HTML)
|
||||
this.sanitizedValue = value.replace(/^-|--+|-$/g, '');
|
||||
};
|
||||
Comment.prototype.htmljsType = Comment.htmljsType = ['Comment'];
|
||||
|
||||
var Raw = HTML.Raw = function (value) {
|
||||
if (! (this instanceof Raw))
|
||||
// called without `new`
|
||||
return new Raw(value);
|
||||
|
||||
if (typeof value !== 'string')
|
||||
throw new Error('HTML.Raw must be constructed with a string');
|
||||
|
||||
this.value = value;
|
||||
};
|
||||
Raw.prototype.htmljsType = Raw.htmljsType = ['Raw'];
|
||||
|
||||
|
||||
HTML.isArray = function (x) {
|
||||
// could change this to use the more convoluted Object.prototype.toString
|
||||
// approach that works when objects are passed between frames, but does
|
||||
// it matter?
|
||||
return (x instanceof Array);
|
||||
};
|
||||
|
||||
HTML.isConstructedObject = function (x) {
|
||||
// Figure out if `x` is "an instance of some class" or just a plain
|
||||
// object literal. It correctly treats an object literal like
|
||||
// `{ constructor: ... }` as an object literal. It won't detect
|
||||
// instances of classes that lack a `constructor` property (e.g.
|
||||
// if you assign to a prototype when setting up the class as in:
|
||||
// `Foo = function () { ... }; Foo.prototype = { ... }`, then
|
||||
// `(new Foo).constructor` is `Object`, not `Foo`).
|
||||
return (x && (typeof x === 'object') &&
|
||||
(x.constructor !== Object) &&
|
||||
(typeof x.constructor === 'function') &&
|
||||
(x instanceof x.constructor));
|
||||
};
|
||||
|
||||
HTML.isNully = function (node) {
|
||||
if (node == null)
|
||||
// null or undefined
|
||||
return true;
|
||||
|
||||
if (HTML.isArray(node)) {
|
||||
// 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.isValidAttributeName = function (name) {
|
||||
return /^[:_A-Za-z][:_A-Za-z0-9.\-]*/.test(name);
|
||||
};
|
||||
|
||||
// If `attrs` is an array of attributes dictionaries, combines them
|
||||
// into one. Removes attributes that are "nully."
|
||||
HTML.flattenAttributes = function (attrs) {
|
||||
if (! attrs)
|
||||
return attrs;
|
||||
|
||||
var isArray = HTML.isArray(attrs);
|
||||
if (isArray && attrs.length === 0)
|
||||
return null;
|
||||
|
||||
var result = {};
|
||||
for (var i = 0, N = (isArray ? attrs.length : 1); i < N; i++) {
|
||||
var oneAttrs = (isArray ? attrs[i] : attrs);
|
||||
if ((typeof oneAttrs !== 'object') ||
|
||||
HTML.isConstructedObject(oneAttrs))
|
||||
throw new Error("Expected plain JS object as attrs, found: " + oneAttrs);
|
||||
for (var name in oneAttrs) {
|
||||
if (! HTML.isValidAttributeName(name))
|
||||
throw new Error("Illegal HTML attribute name: " + name);
|
||||
var value = oneAttrs[name];
|
||||
if (! HTML.isNully(value))
|
||||
result[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
|
||||
////////////////////////////// TOHTML
|
||||
|
||||
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 = 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);
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
|
||||
Tinytest.add("htmljs - getTag", function (test) {
|
||||
var FOO = HTML.getTag('foo');
|
||||
test.isTrue(HTML.FOO === FOO);
|
||||
var x = FOO();
|
||||
|
||||
test.equal(x.tagName, 'foo');
|
||||
test.isTrue(x instanceof HTML.FOO);
|
||||
test.isTrue(x instanceof HTML.Tag);
|
||||
test.equal(x.children, []);
|
||||
test.equal(x.attrs, null);
|
||||
|
||||
test.isTrue((new FOO) instanceof HTML.FOO);
|
||||
test.isTrue((new FOO) instanceof HTML.Tag);
|
||||
test.isFalse((new HTML.P) instanceof HTML.FOO);
|
||||
|
||||
var result = HTML.ensureTag('Bar');
|
||||
test.equal(typeof result, 'undefined');
|
||||
var BAR = HTML.BAR;
|
||||
test.equal(BAR().tagName, 'Bar');
|
||||
});
|
||||
|
||||
Tinytest.add("htmljs - construction", function (test) {
|
||||
var A = HTML.getTag('a');
|
||||
var B = HTML.getTag('b');
|
||||
var C = HTML.getTag('c');
|
||||
|
||||
var a = A(0, B({q:0}, C(A(B({})), 'foo')));
|
||||
test.equal(a.tagName, 'a');
|
||||
test.equal(a.attrs, null);
|
||||
test.equal(a.children.length, 2);
|
||||
test.equal(a.children[0], 0);
|
||||
var b = a.children[1];
|
||||
test.equal(b.tagName, 'b');
|
||||
test.equal(b.attrs, {q:0});
|
||||
test.equal(b.children.length, 1);
|
||||
var c = b.children[0];
|
||||
test.equal(c.tagName, 'c');
|
||||
test.equal(c.attrs, null);
|
||||
test.equal(c.children.length, 2);
|
||||
test.equal(c.children[0].tagName, 'a');
|
||||
test.equal(c.children[0].attrs, null);
|
||||
test.equal(c.children[0].children.length, 1);
|
||||
test.equal(c.children[0].children[0].tagName, 'b');
|
||||
test.equal(c.children[0].children[0].children.length, 0);
|
||||
test.equal(c.children[0].children[0].attrs, {});
|
||||
test.equal(c.children[1], 'foo');
|
||||
|
||||
var a2 = new A({m:1}, {n:2}, B(), {o:3}, 'foo');
|
||||
test.equal(a2.tagName, 'a');
|
||||
test.equal(a2.attrs, {m:1});
|
||||
test.equal(a2.children.length, 4);
|
||||
test.equal(a2.children[0], {n:2});
|
||||
test.equal(a2.children[1].tagName, 'b');
|
||||
test.equal(a2.children[2], {o:3});
|
||||
test.equal(a2.children[3], 'foo');
|
||||
|
||||
// tests of HTML.isConstructedObject (indirectly)
|
||||
test.equal(A({x:1}).children.length, 0);
|
||||
var f = function () {};
|
||||
test.equal(A(new f).children.length, 1);
|
||||
test.equal(A(new Date).children.length, 1);
|
||||
test.equal(A({constructor: 'blah'}).children.length, 0);
|
||||
test.equal(A({constructor: Object}).children.length, 0);
|
||||
|
||||
test.equal(HTML.toHTML(HTML.CharRef({html: '&', str: '&'})), '&');
|
||||
test.throws(function () {
|
||||
HTML.CharRef({html: '&'}); // no 'str'
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("htmljs - utils", function (test) {
|
||||
|
||||
test.notEqual("\u00c9".toLowerCase(), "\u00c9");
|
||||
test.equal(HTMLTools.asciiLowerCase("\u00c9"), "\u00c9");
|
||||
|
||||
test.equal(HTMLTools.asciiLowerCase("Hello There"), "hello there");
|
||||
|
||||
test.isTrue(HTML.isVoidElement("br"));
|
||||
test.isFalse(HTML.isVoidElement("div"));
|
||||
test.isTrue(HTML.isKnownElement("div"));
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("htmljs - details", function (test) {
|
||||
test.equal(HTML.toHTML(false), "false");
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
Package.describe({
|
||||
summary: "Small library for expressing HTML trees",
|
||||
version: '1.0.10'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use('deps');
|
||||
api.export('HTML');
|
||||
|
||||
api.addFiles(['preamble.js',
|
||||
'visitors.js',
|
||||
'html.js']);
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use('htmljs');
|
||||
api.use('html-tools');
|
||||
api.use('tinytest');
|
||||
api.use('underscore');
|
||||
api.addFiles(['htmljs_test.js']);
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
HTML = {};
|
||||
|
||||
IDENTITY = function (x) { return x; };
|
||||
SLICE = Array.prototype.slice;
|
||||
@@ -1,331 +0,0 @@
|
||||
////////////////////////////// VISITORS
|
||||
|
||||
// _assign is like _.extend or the upcoming Object.assign.
|
||||
// Copy src's own, enumerable properties onto tgt and return
|
||||
// tgt.
|
||||
var _hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
var _assign = function (tgt, src) {
|
||||
for (var k in src) {
|
||||
if (_hasOwnProperty.call(src, k))
|
||||
tgt[k] = src[k];
|
||||
}
|
||||
return tgt;
|
||||
};
|
||||
|
||||
HTML.Visitor = function (props) {
|
||||
_assign(this, props);
|
||||
};
|
||||
|
||||
HTML.Visitor.def = function (options) {
|
||||
_assign(this.prototype, options);
|
||||
};
|
||||
|
||||
HTML.Visitor.extend = function (options) {
|
||||
var curType = this;
|
||||
var subType = function HTMLVisitorSubtype(/*arguments*/) {
|
||||
HTML.Visitor.apply(this, arguments);
|
||||
};
|
||||
subType.prototype = new curType;
|
||||
subType.extend = curType.extend;
|
||||
subType.def = curType.def;
|
||||
if (options)
|
||||
_assign(subType.prototype, options);
|
||||
return subType;
|
||||
};
|
||||
|
||||
HTML.Visitor.def({
|
||||
visit: function (content/*, ...*/) {
|
||||
if (content == null)
|
||||
// null or undefined.
|
||||
return this.visitNull.apply(this, arguments);
|
||||
|
||||
if (typeof content === 'object') {
|
||||
if (content.htmljsType) {
|
||||
switch (content.htmljsType) {
|
||||
case HTML.Tag.htmljsType:
|
||||
return this.visitTag.apply(this, arguments);
|
||||
case HTML.CharRef.htmljsType:
|
||||
return this.visitCharRef.apply(this, arguments);
|
||||
case HTML.Comment.htmljsType:
|
||||
return this.visitComment.apply(this, arguments);
|
||||
case HTML.Raw.htmljsType:
|
||||
return this.visitRaw.apply(this, arguments);
|
||||
default:
|
||||
throw new Error("Unknown htmljs type: " + content.htmljsType);
|
||||
}
|
||||
}
|
||||
|
||||
if (HTML.isArray(content))
|
||||
return this.visitArray.apply(this, arguments);
|
||||
|
||||
return this.visitObject.apply(this, arguments);
|
||||
|
||||
} else if ((typeof content === 'string') ||
|
||||
(typeof content === 'boolean') ||
|
||||
(typeof content === 'number')) {
|
||||
return this.visitPrimitive.apply(this, arguments);
|
||||
|
||||
} else if (typeof content === 'function') {
|
||||
return this.visitFunction.apply(this, arguments);
|
||||
}
|
||||
|
||||
throw new Error("Unexpected object in htmljs: " + content);
|
||||
|
||||
},
|
||||
visitNull: function (nullOrUndefined/*, ...*/) {},
|
||||
visitPrimitive: function (stringBooleanOrNumber/*, ...*/) {},
|
||||
visitArray: function (array/*, ...*/) {},
|
||||
visitComment: function (comment/*, ...*/) {},
|
||||
visitCharRef: function (charRef/*, ...*/) {},
|
||||
visitRaw: function (raw/*, ...*/) {},
|
||||
visitTag: function (tag/*, ...*/) {},
|
||||
visitObject: function (obj/*, ...*/) {
|
||||
throw new Error("Unexpected object in htmljs: " + obj);
|
||||
},
|
||||
visitFunction: function (fn/*, ...*/) {
|
||||
throw new Error("Unexpected function in htmljs: " + fn);
|
||||
}
|
||||
});
|
||||
|
||||
HTML.TransformingVisitor = HTML.Visitor.extend();
|
||||
HTML.TransformingVisitor.def({
|
||||
visitNull: IDENTITY,
|
||||
visitPrimitive: IDENTITY,
|
||||
visitArray: function (array/*, ...*/) {
|
||||
var argsCopy = SLICE.call(arguments);
|
||||
var result = array;
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
var oldItem = array[i];
|
||||
argsCopy[0] = oldItem;
|
||||
var newItem = this.visit.apply(this, argsCopy);
|
||||
if (newItem !== oldItem) {
|
||||
// copy `array` on write
|
||||
if (result === array)
|
||||
result = array.slice();
|
||||
result[i] = newItem;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
visitComment: IDENTITY,
|
||||
visitCharRef: IDENTITY,
|
||||
visitRaw: IDENTITY,
|
||||
visitObject: IDENTITY,
|
||||
visitFunction: IDENTITY,
|
||||
visitTag: function (tag/*, ...*/) {
|
||||
var oldChildren = tag.children;
|
||||
var argsCopy = SLICE.call(arguments);
|
||||
argsCopy[0] = oldChildren;
|
||||
var newChildren = this.visitChildren.apply(this, argsCopy);
|
||||
|
||||
var oldAttrs = tag.attrs;
|
||||
argsCopy[0] = oldAttrs;
|
||||
var newAttrs = this.visitAttributes.apply(this, argsCopy);
|
||||
|
||||
if (newAttrs === oldAttrs && newChildren === oldChildren)
|
||||
return tag;
|
||||
|
||||
var newTag = HTML.getTag(tag.tagName).apply(null, newChildren);
|
||||
newTag.attrs = newAttrs;
|
||||
return newTag;
|
||||
},
|
||||
visitChildren: function (children/*, ...*/) {
|
||||
return this.visitArray.apply(this, arguments);
|
||||
},
|
||||
// Transform the `.attrs` property of a tag, which may be a dictionary,
|
||||
// an array, or in some uses, a foreign object (such as
|
||||
// a template tag).
|
||||
visitAttributes: function (attrs/*, ...*/) {
|
||||
if (HTML.isArray(attrs)) {
|
||||
var argsCopy = SLICE.call(arguments);
|
||||
var result = attrs;
|
||||
for (var i = 0; i < attrs.length; i++) {
|
||||
var oldItem = attrs[i];
|
||||
argsCopy[0] = oldItem;
|
||||
var newItem = this.visitAttributes.apply(this, argsCopy);
|
||||
if (newItem !== oldItem) {
|
||||
// copy on write
|
||||
if (result === attrs)
|
||||
result = attrs.slice();
|
||||
result[i] = newItem;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (attrs && HTML.isConstructedObject(attrs)) {
|
||||
throw new Error("The basic HTML.TransformingVisitor does not support " +
|
||||
"foreign objects in attributes. Define a custom " +
|
||||
"visitAttributes for this case.");
|
||||
}
|
||||
|
||||
var oldAttrs = attrs;
|
||||
var newAttrs = oldAttrs;
|
||||
if (oldAttrs) {
|
||||
var attrArgs = [null, null];
|
||||
attrArgs.push.apply(attrArgs, arguments);
|
||||
for (var k in oldAttrs) {
|
||||
var oldValue = oldAttrs[k];
|
||||
attrArgs[0] = k;
|
||||
attrArgs[1] = oldValue;
|
||||
var newValue = this.visitAttribute.apply(this, attrArgs);
|
||||
if (newValue !== oldValue) {
|
||||
// copy on write
|
||||
if (newAttrs === oldAttrs)
|
||||
newAttrs = _assign({}, oldAttrs);
|
||||
newAttrs[k] = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newAttrs;
|
||||
},
|
||||
// Transform the value of one attribute name/value in an
|
||||
// attributes dictionary.
|
||||
visitAttribute: function (name, value, tag/*, ...*/) {
|
||||
var args = SLICE.call(arguments, 2);
|
||||
args[0] = value;
|
||||
return this.visit.apply(this, args);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
HTML.ToTextVisitor = HTML.Visitor.extend();
|
||||
HTML.ToTextVisitor.def({
|
||||
visitNull: function (nullOrUndefined) {
|
||||
return '';
|
||||
},
|
||||
visitPrimitive: function (stringBooleanOrNumber) {
|
||||
var str = String(stringBooleanOrNumber);
|
||||
if (this.textMode === HTML.TEXTMODE.RCDATA) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<');
|
||||
} else if (this.textMode === HTML.TEXTMODE.ATTRIBUTE) {
|
||||
// escape `&` and `"` this time, not `&` and `<`
|
||||
return str.replace(/&/g, '&').replace(/"/g, '"');
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
},
|
||||
visitArray: function (array) {
|
||||
var parts = [];
|
||||
for (var i = 0; i < array.length; i++)
|
||||
parts.push(this.visit(array[i]));
|
||||
return parts.join('');
|
||||
},
|
||||
visitComment: function (comment) {
|
||||
throw new Error("Can't have a comment here");
|
||||
},
|
||||
visitCharRef: function (charRef) {
|
||||
if (this.textMode === HTML.TEXTMODE.RCDATA ||
|
||||
this.textMode === HTML.TEXTMODE.ATTRIBUTE) {
|
||||
return charRef.html;
|
||||
} else {
|
||||
return charRef.str;
|
||||
}
|
||||
},
|
||||
visitRaw: function (raw) {
|
||||
return raw.value;
|
||||
},
|
||||
visitTag: function (tag) {
|
||||
// Really we should just disallow Tags here. However, at the
|
||||
// moment it's useful to stringify any HTML we find. In
|
||||
// particular, when you include a template within `{{#markdown}}`,
|
||||
// we render the template as text, and since there's currently
|
||||
// no way to make the template be *parsed* as text (e.g. `<template
|
||||
// type="text">`), we hackishly support HTML tags in markdown
|
||||
// in templates by parsing them and stringifying them.
|
||||
return this.visit(this.toHTML(tag));
|
||||
},
|
||||
visitObject: function (x) {
|
||||
throw new Error("Unexpected object in htmljs in toText: " + x);
|
||||
},
|
||||
toHTML: function (node) {
|
||||
return HTML.toHTML(node);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
HTML.ToHTMLVisitor = HTML.Visitor.extend();
|
||||
HTML.ToHTMLVisitor.def({
|
||||
visitNull: function (nullOrUndefined) {
|
||||
return '';
|
||||
},
|
||||
visitPrimitive: function (stringBooleanOrNumber) {
|
||||
var str = String(stringBooleanOrNumber);
|
||||
return str.replace(/&/g, '&').replace(/</g, '<');
|
||||
},
|
||||
visitArray: function (array) {
|
||||
var parts = [];
|
||||
for (var i = 0; i < array.length; i++)
|
||||
parts.push(this.visit(array[i]));
|
||||
return parts.join('');
|
||||
},
|
||||
visitComment: function (comment) {
|
||||
return '<!--' + comment.sanitizedValue + '-->';
|
||||
},
|
||||
visitCharRef: function (charRef) {
|
||||
return charRef.html;
|
||||
},
|
||||
visitRaw: function (raw) {
|
||||
return raw.value;
|
||||
},
|
||||
visitTag: function (tag) {
|
||||
var attrStrs = [];
|
||||
|
||||
var tagName = tag.tagName;
|
||||
var children = tag.children;
|
||||
|
||||
var attrs = tag.attrs;
|
||||
if (attrs) {
|
||||
attrs = HTML.flattenAttributes(attrs);
|
||||
for (var k in attrs) {
|
||||
if (k === 'value' && tagName === 'textarea') {
|
||||
children = [attrs[k], children];
|
||||
} else {
|
||||
var v = this.toText(attrs[k], HTML.TEXTMODE.ATTRIBUTE);
|
||||
attrStrs.push(' ' + k + '="' + v + '"');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var startTag = '<' + tagName + attrStrs.join('') + '>';
|
||||
|
||||
var childStrs = [];
|
||||
var content;
|
||||
if (tagName === 'textarea') {
|
||||
|
||||
for (var i = 0; i < children.length; i++)
|
||||
childStrs.push(this.toText(children[i], HTML.TEXTMODE.RCDATA));
|
||||
|
||||
content = childStrs.join('');
|
||||
if (content.slice(0, 1) === '\n')
|
||||
// TEXTAREA will absorb a newline, so if we see one, add
|
||||
// another one.
|
||||
content = '\n' + content;
|
||||
|
||||
} else {
|
||||
for (var i = 0; i < children.length; i++)
|
||||
childStrs.push(this.visit(children[i]));
|
||||
|
||||
content = childStrs.join('');
|
||||
}
|
||||
|
||||
var result = startTag + content;
|
||||
|
||||
if (children.length || ! HTML.isVoidElement(tagName)) {
|
||||
// "Void" elements like BR are the only ones that don't get a close
|
||||
// tag in HTML5. They shouldn't have contents, either, so we could
|
||||
// throw an error upon seeing contents here.
|
||||
result += '</' + tagName + '>';
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
visitObject: function (x) {
|
||||
throw new Error("Unexpected object in htmljs in toHTML: " + x);
|
||||
},
|
||||
toText: function (node, textMode) {
|
||||
return HTML.toText(node, textMode);
|
||||
}
|
||||
});
|
||||
1
packages/spacebars-compiler/.gitignore
vendored
1
packages/spacebars-compiler/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.build*
|
||||
@@ -1,15 +0,0 @@
|
||||
# spacebars-compiler
|
||||
|
||||
The Spacebars compiler that is invoked at build time to compile
|
||||
templates to JavaScript.
|
||||
|
||||
While this code is not normally ever shipped to the client, it can be
|
||||
used at runtime on the server or the client by using the
|
||||
`SpacebarsCompiler` symbol from this package.
|
||||
|
||||
The `spacebars` package, in contrast, contains the `Spacebars` symbol
|
||||
and the Spacebars runtime, which is shipped to the client as part of
|
||||
the app.
|
||||
|
||||
Read more about Spacebars, Blaze, and Meteor templating on the Blaze
|
||||
[project page](https://www.meteor.com/blaze).
|
||||
@@ -1,389 +0,0 @@
|
||||
// ============================================================
|
||||
// Code-generation of template tags
|
||||
|
||||
// The `CodeGen` class currently has no instance state, but in theory
|
||||
// it could be useful to track per-function state, like whether we
|
||||
// need to emit `var self = this` or not.
|
||||
var CodeGen = SpacebarsCompiler.CodeGen = function () {};
|
||||
|
||||
var builtInBlockHelpers = SpacebarsCompiler._builtInBlockHelpers = {
|
||||
'if': 'Blaze.If',
|
||||
'unless': 'Blaze.Unless',
|
||||
'with': 'Spacebars.With',
|
||||
'each': 'Blaze.Each',
|
||||
'let': 'Blaze.Let'
|
||||
};
|
||||
|
||||
|
||||
// Mapping of "macros" which, when preceded by `Template.`, expand
|
||||
// to special code rather than following the lookup rules for dotted
|
||||
// symbols.
|
||||
var builtInTemplateMacros = {
|
||||
// `view` is a local variable defined in the generated render
|
||||
// function for the template in which `Template.contentBlock` or
|
||||
// `Template.elseBlock` is invoked.
|
||||
'contentBlock': 'view.templateContentBlock',
|
||||
'elseBlock': 'view.templateElseBlock',
|
||||
|
||||
// Confusingly, this makes `{{> Template.dynamic}}` an alias
|
||||
// for `{{> __dynamic}}`, where "__dynamic" is the template that
|
||||
// implements the dynamic template feature.
|
||||
'dynamic': 'Template.__dynamic',
|
||||
|
||||
'subscriptionsReady': 'view.templateInstance().subscriptionsReady()'
|
||||
};
|
||||
|
||||
var additionalReservedNames = ["body", "toString", "instance", "constructor",
|
||||
"toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf",
|
||||
"propertyIsEnumerable", "__defineGetter__", "__lookupGetter__",
|
||||
"__defineSetter__", "__lookupSetter__", "__proto__", "dynamic",
|
||||
"registerHelper", "currentData", "parentData"];
|
||||
|
||||
// A "reserved name" can't be used as a <template> name. This
|
||||
// function is used by the template file scanner.
|
||||
//
|
||||
// Note that the runtime imposes additional restrictions, for example
|
||||
// banning the name "body" and names of built-in object properties
|
||||
// like "toString".
|
||||
SpacebarsCompiler.isReservedName = function (name) {
|
||||
return builtInBlockHelpers.hasOwnProperty(name) ||
|
||||
builtInTemplateMacros.hasOwnProperty(name) ||
|
||||
_.indexOf(additionalReservedNames, name) > -1;
|
||||
};
|
||||
|
||||
var makeObjectLiteral = function (obj) {
|
||||
var parts = [];
|
||||
for (var k in obj)
|
||||
parts.push(BlazeTools.toObjectLiteralKey(k) + ': ' + obj[k]);
|
||||
return '{' + parts.join(', ') + '}';
|
||||
};
|
||||
|
||||
_.extend(CodeGen.prototype, {
|
||||
codeGenTemplateTag: function (tag) {
|
||||
var self = this;
|
||||
if (tag.position === HTMLTools.TEMPLATE_TAG_POSITION.IN_START_TAG) {
|
||||
// Special dynamic attributes: `<div {{attrs}}>...`
|
||||
// only `tag.type === 'DOUBLE'` allowed (by earlier validation)
|
||||
return BlazeTools.EmitCode('function () { return ' +
|
||||
self.codeGenMustache(tag.path, tag.args, 'attrMustache')
|
||||
+ '; }');
|
||||
} else {
|
||||
if (tag.type === 'DOUBLE' || tag.type === 'TRIPLE') {
|
||||
var code = self.codeGenMustache(tag.path, tag.args);
|
||||
if (tag.type === 'TRIPLE') {
|
||||
code = 'Spacebars.makeRaw(' + code + ')';
|
||||
}
|
||||
if (tag.position !== HTMLTools.TEMPLATE_TAG_POSITION.IN_ATTRIBUTE) {
|
||||
// Reactive attributes are already wrapped in a function,
|
||||
// and there's no fine-grained reactivity.
|
||||
// Anywhere else, we need to create a View.
|
||||
code = 'Blaze.View(' +
|
||||
BlazeTools.toJSLiteral('lookup:' + tag.path.join('.')) + ', ' +
|
||||
'function () { return ' + code + '; })';
|
||||
}
|
||||
return BlazeTools.EmitCode(code);
|
||||
} else if (tag.type === 'INCLUSION' || tag.type === 'BLOCKOPEN') {
|
||||
var path = tag.path;
|
||||
var args = tag.args;
|
||||
|
||||
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 (! args.length)
|
||||
throw new Error("#" + path[0] + " requires an argument");
|
||||
|
||||
var dataCode = null;
|
||||
// #each has a special treatment as it features two different forms:
|
||||
// - {{#each people}}
|
||||
// - {{#each person in people}}
|
||||
if (path[0] === 'each' && args.length >= 2 && args[1][0] === 'PATH' &&
|
||||
args[1][1].length && args[1][1][0] === 'in') {
|
||||
// minimum conditions are met for each-in. now validate this
|
||||
// isn't some weird case.
|
||||
var eachUsage = "Use either {{#each items}} or " +
|
||||
"{{#each item in items}} form of #each.";
|
||||
var inArg = args[1];
|
||||
if (! (args.length >= 3 && inArg[1].length === 1)) {
|
||||
// we don't have at least 3 space-separated parts after #each, or
|
||||
// inArg doesn't look like ['PATH',['in']]
|
||||
throw new Error("Malformed #each. " + eachUsage);
|
||||
}
|
||||
// split out the variable name and sequence arguments
|
||||
var variableArg = args[0];
|
||||
if (! (variableArg[0] === "PATH" && variableArg[1].length === 1 &&
|
||||
variableArg[1][0].replace(/\./g, ''))) {
|
||||
throw new Error("Bad variable name in #each");
|
||||
}
|
||||
var variable = variableArg[1][0];
|
||||
dataCode = 'function () { return { _sequence: ' +
|
||||
self.codeGenInclusionData(args.slice(2)) +
|
||||
', _variable: ' + BlazeTools.toJSLiteral(variable) + ' }; }';
|
||||
} else if (path[0] === 'let') {
|
||||
var dataProps = {};
|
||||
_.each(args, function (arg) {
|
||||
if (arg.length !== 3) {
|
||||
// not a keyword arg (x=y)
|
||||
throw new Error("Incorrect form of #let");
|
||||
}
|
||||
var argKey = arg[2];
|
||||
dataProps[argKey] =
|
||||
'function () { return Spacebars.call(' +
|
||||
self.codeGenArgValue(arg) + '); }';
|
||||
});
|
||||
dataCode = makeObjectLiteral(dataProps);
|
||||
}
|
||||
|
||||
if (! dataCode) {
|
||||
// `args` must exist (tag.args.length > 0)
|
||||
dataCode = self.codeGenInclusionDataFunc(args) || 'null';
|
||||
}
|
||||
|
||||
// `content` must exist
|
||||
var contentBlock = (('content' in tag) ?
|
||||
self.codeGenBlock(tag.content) : null);
|
||||
// `elseContent` may not exist
|
||||
var elseContentBlock = (('elseContent' in tag) ?
|
||||
self.codeGenBlock(tag.elseContent) : null);
|
||||
|
||||
var callArgs = [dataCode, contentBlock];
|
||||
if (elseContentBlock)
|
||||
callArgs.push(elseContentBlock);
|
||||
|
||||
return BlazeTools.EmitCode(
|
||||
builtInBlockHelpers[path[0]] + '(' + callArgs.join(', ') + ')');
|
||||
|
||||
} else {
|
||||
var compCode = self.codeGenPath(path, {lookupTemplate: true});
|
||||
if (path.length > 1) {
|
||||
// capture reactivity
|
||||
compCode = 'function () { return Spacebars.call(' + compCode +
|
||||
'); }';
|
||||
}
|
||||
|
||||
var dataCode = self.codeGenInclusionDataFunc(tag.args);
|
||||
var content = (('content' in tag) ?
|
||||
self.codeGenBlock(tag.content) : null);
|
||||
var elseContent = (('elseContent' in tag) ?
|
||||
self.codeGenBlock(tag.elseContent) : null);
|
||||
|
||||
var includeArgs = [compCode];
|
||||
if (content) {
|
||||
includeArgs.push(content);
|
||||
if (elseContent)
|
||||
includeArgs.push(elseContent);
|
||||
}
|
||||
|
||||
var includeCode =
|
||||
'Spacebars.include(' + includeArgs.join(', ') + ')';
|
||||
|
||||
// calling convention compat -- set the data context around the
|
||||
// entire inclusion, so that if the name of the inclusion is
|
||||
// a helper function, it gets the data context in `this`.
|
||||
// This makes for a pretty confusing calling convention --
|
||||
// In `{{#foo bar}}`, `foo` is evaluated in the context of `bar`
|
||||
// -- but it's what we shipped for 0.8.0. The rationale is that
|
||||
// `{{#foo bar}}` is sugar for `{{#with bar}}{{#foo}}...`.
|
||||
if (dataCode) {
|
||||
includeCode =
|
||||
'Blaze._TemplateWith(' + dataCode + ', function () { return ' +
|
||||
includeCode + '; })';
|
||||
}
|
||||
|
||||
// XXX BACK COMPAT - UI is the old name, Template is the new
|
||||
if ((path[0] === 'UI' || path[0] === 'Template') &&
|
||||
(path[1] === 'contentBlock' || path[1] === 'elseBlock')) {
|
||||
// Call contentBlock and elseBlock in the appropriate scope
|
||||
includeCode = 'Blaze._InOuterTemplateScope(view, function () { return '
|
||||
+ includeCode + '; })';
|
||||
}
|
||||
|
||||
return BlazeTools.EmitCode(includeCode);
|
||||
}
|
||||
} else if (tag.type === 'ESCAPE') {
|
||||
return tag.value;
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// `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.
|
||||
codeGenPath: function (path, opts) {
|
||||
if (builtInBlockHelpers.hasOwnProperty(path[0]))
|
||||
throw new Error("Can't use the built-in '" + path[0] + "' here");
|
||||
// Let `{{#if Template.contentBlock}}` check whether this template was
|
||||
// invoked via inclusion or as a block helper, in addition to supporting
|
||||
// `{{> Template.contentBlock}}`.
|
||||
// XXX BACK COMPAT - UI is the old name, Template is the new
|
||||
if (path.length >= 2 &&
|
||||
(path[0] === 'UI' || path[0] === 'Template')
|
||||
&& builtInTemplateMacros.hasOwnProperty(path[1])) {
|
||||
if (path.length > 2)
|
||||
throw new Error("Unexpected dotted path beginning with " +
|
||||
path[0] + '.' + path[1]);
|
||||
return builtInTemplateMacros[path[1]];
|
||||
}
|
||||
|
||||
var firstPathItem = BlazeTools.toJSLiteral(path[0]);
|
||||
var lookupMethod = 'lookup';
|
||||
if (opts && opts.lookupTemplate && path.length === 1)
|
||||
lookupMethod = 'lookupTemplate';
|
||||
var code = 'view.' + lookupMethod + '(' + firstPathItem + ')';
|
||||
|
||||
if (path.length > 1) {
|
||||
code = 'Spacebars.dot(' + code + ', ' +
|
||||
_.map(path.slice(1), BlazeTools.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.
|
||||
codeGenArgValue: function (arg) {
|
||||
var self = this;
|
||||
|
||||
var argType = arg[0];
|
||||
var argValue = arg[1];
|
||||
|
||||
var argCode;
|
||||
switch (argType) {
|
||||
case 'STRING':
|
||||
case 'NUMBER':
|
||||
case 'BOOLEAN':
|
||||
case 'NULL':
|
||||
argCode = BlazeTools.toJSLiteral(argValue);
|
||||
break;
|
||||
case 'PATH':
|
||||
argCode = self.codeGenPath(argValue);
|
||||
break;
|
||||
case 'EXPR':
|
||||
// The format of EXPR is ['EXPR', { type: 'EXPR', path: [...], args: { ... } }]
|
||||
argCode = self.codeGenMustache(argValue.path, argValue.args, 'dataMustache');
|
||||
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.
|
||||
codeGenMustache: function (path, args, mustacheType) {
|
||||
var self = this;
|
||||
|
||||
var nameCode = self.codeGenPath(path);
|
||||
var argCode = self.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.
|
||||
codeGenMustacheArgs: function (tagArgs) {
|
||||
var self = this;
|
||||
|
||||
var kwArgs = null; // source -> source
|
||||
var args = null; // [source]
|
||||
|
||||
// tagArgs may be null
|
||||
_.each(tagArgs, function (arg) {
|
||||
var argCode = self.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;
|
||||
},
|
||||
|
||||
codeGenBlock: function (content) {
|
||||
return SpacebarsCompiler.codeGen(content);
|
||||
},
|
||||
|
||||
codeGenInclusionData: function (args) {
|
||||
var self = this;
|
||||
|
||||
if (! args.length) {
|
||||
// e.g. `{{#foo}}`
|
||||
return null;
|
||||
} 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(' + self.codeGenArgValue(arg) + ')';
|
||||
});
|
||||
return 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`)
|
||||
return self.codeGenArgValue(args[0]);
|
||||
} else if (args.length === 1) {
|
||||
// one argument, must be a PATH
|
||||
return 'Spacebars.call(' + self.codeGenPath(args[0][1]) + ')';
|
||||
} else {
|
||||
// Multiple positional arguments; treat them as a nested
|
||||
// "data mustache"
|
||||
return self.codeGenMustache(args[0][1], args.slice(1),
|
||||
'dataMustache');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
codeGenInclusionDataFunc: function (args) {
|
||||
var self = this;
|
||||
var dataCode = self.codeGenInclusionData(args);
|
||||
if (dataCode) {
|
||||
return 'function () { return ' + dataCode + '; }';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,114 +0,0 @@
|
||||
Tinytest.add("spacebars-compiler - compiler output", function (test) {
|
||||
|
||||
var run = function (input, expected) {
|
||||
if (expected.fail) {
|
||||
var expectedMessage = expected.fail;
|
||||
// test for error starting with expectedMessage
|
||||
var msg = '';
|
||||
test.throws(function () {
|
||||
try {
|
||||
SpacebarsCompiler.compile(input, {isTemplate: true});
|
||||
} catch (e) {
|
||||
msg = e.message;
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
test.equal(msg.slice(0, expectedMessage.length),
|
||||
expectedMessage);
|
||||
} else {
|
||||
var output = SpacebarsCompiler.compile(input, {isTemplate: true});
|
||||
var postProcess = function (string) {
|
||||
// remove initial and trailing parens
|
||||
string = string.replace(/^\(([\S\s]*)\)$/, '$1');
|
||||
if (! (Package['minifier-js'] && Package['minifier-js'].UglifyJSMinify)) {
|
||||
// these tests work a lot better with access to beautification,
|
||||
// but let's at least do some sort of test without it.
|
||||
// These regexes may have to be adjusted if new tests are added.
|
||||
|
||||
// Remove single-line comments, including line nums from build system.
|
||||
string = string.replace(/\/\/.*$/mg, '');
|
||||
string = string.replace(/\s+/g, ''); // kill whitespace
|
||||
}
|
||||
return string;
|
||||
};
|
||||
// compare using Function .toString()!
|
||||
test._stringEqual(
|
||||
postProcess(output.toString()),
|
||||
postProcess(
|
||||
SpacebarsCompiler._beautify('(' + expected.toString() + ')')),
|
||||
input);
|
||||
}
|
||||
};
|
||||
|
||||
coffee.runCompilerOutputTests(run);
|
||||
});
|
||||
|
||||
coffee = {
|
||||
runCompilerOutputTests: null // implemented in compiler_output_tests.coffee
|
||||
};
|
||||
|
||||
|
||||
Tinytest.add("spacebars-compiler - compiler errors", function (test) {
|
||||
|
||||
var getError = function (input) {
|
||||
try {
|
||||
SpacebarsCompiler.compile(input);
|
||||
} catch (e) {
|
||||
return e.message;
|
||||
}
|
||||
test.fail("Didn't throw an error: " + input);
|
||||
return '';
|
||||
};
|
||||
|
||||
var assertStartsWith = function (a, b) {
|
||||
test.equal(a.substring(0, b.length), b);
|
||||
};
|
||||
|
||||
var isError = function (input, errorStart) {
|
||||
assertStartsWith(getError(input), errorStart);
|
||||
};
|
||||
|
||||
isError("<input></input>",
|
||||
"Unexpected HTML close tag. <input> should have no close tag.");
|
||||
isError("{{#each foo}}<input></input>{{/foo}}",
|
||||
"Unexpected HTML close tag. <input> should have no close tag.");
|
||||
|
||||
isError("{{#if}}{{/if}}", "#if requires an argument");
|
||||
isError("{{#with}}{{/with}}", "#with requires an argument");
|
||||
isError("{{#each}}{{/each}}", "#each requires an argument");
|
||||
isError("{{#unless}}{{/unless}}", "#unless requires an argument");
|
||||
|
||||
isError("{{0 0}}", "Expected IDENTIFIER");
|
||||
|
||||
isError("{{> foo 0 0}}",
|
||||
"First argument must be a function");
|
||||
isError("{{> foo 0 x=0}}",
|
||||
"First argument must be a function");
|
||||
isError("{{#foo 0 0}}{{/foo}}",
|
||||
"First argument must be a function");
|
||||
isError("{{#foo 0 x=0}}{{/foo}}",
|
||||
"First argument must be a function");
|
||||
|
||||
_.each(['asdf</br>', '{{!foo}}</br>', '{{!foo}} </br>',
|
||||
'asdf</a>', '{{!foo}}</a>', '{{!foo}} </a>'], function (badFrag) {
|
||||
isError(badFrag, "Unexpected HTML close tag");
|
||||
});
|
||||
|
||||
isError("{{#let myHelper}}{{/let}}", "Incorrect form of #let");
|
||||
isError("{{#each foo in.in bar}}{{/each}}", "Malformed #each");
|
||||
isError("{{#each foo.bar in baz}}{{/each}}", "Bad variable name in #each");
|
||||
isError("{{#each ../foo in baz}}{{/each}}", "Bad variable name in #each");
|
||||
isError("{{#each 3 in baz}}{{/each}}", "Bad variable name in #each");
|
||||
|
||||
// errors using `{{> React}}`
|
||||
isError("{{> React component=emptyComponent}}",
|
||||
"{{> React}} must be used in a container element");
|
||||
isError("<div>{{#if include}}{{> React component=emptyComponent}}{{/if}}</div>",
|
||||
"{{> React}} must be used in a container element");
|
||||
isError("<div><div>Sibling</div>{{> React component=emptyComponent}}</div>",
|
||||
"{{> React}} must be used as the only child in a container element");
|
||||
isError("<div>Sibling{{> React component=emptyComponent}}</div>",
|
||||
"{{> React}} must be used as the only child in a container element");
|
||||
isError("<div>{{#if sibling}}Sibling{{/if}}{{> React component=emptyComponent}}</div>",
|
||||
"{{> React}} must be used as the only child in a container element");
|
||||
});
|
||||
@@ -1,116 +0,0 @@
|
||||
|
||||
SpacebarsCompiler.parse = function (input) {
|
||||
|
||||
var tree = HTMLTools.parseFragment(
|
||||
input,
|
||||
{ getTemplateTag: TemplateTag.parseCompleteTag });
|
||||
|
||||
return tree;
|
||||
};
|
||||
|
||||
SpacebarsCompiler.compile = function (input, options) {
|
||||
var tree = SpacebarsCompiler.parse(input);
|
||||
return SpacebarsCompiler.codeGen(tree, options);
|
||||
};
|
||||
|
||||
SpacebarsCompiler._TemplateTagReplacer = HTML.TransformingVisitor.extend();
|
||||
SpacebarsCompiler._TemplateTagReplacer.def({
|
||||
visitObject: function (x) {
|
||||
if (x instanceof HTMLTools.TemplateTag) {
|
||||
|
||||
// Make sure all TemplateTags in attributes have the right
|
||||
// `.position` set on them. This is a bit of a hack
|
||||
// (we shouldn't be mutating that here), but it allows
|
||||
// cleaner codegen of "synthetic" attributes like TEXTAREA's
|
||||
// "value", where the template tags were originally not
|
||||
// in an attribute.
|
||||
if (this.inAttributeValue)
|
||||
x.position = HTMLTools.TEMPLATE_TAG_POSITION.IN_ATTRIBUTE;
|
||||
|
||||
return this.codegen.codeGenTemplateTag(x);
|
||||
}
|
||||
|
||||
return HTML.TransformingVisitor.prototype.visitObject.call(this, x);
|
||||
},
|
||||
visitAttributes: function (attrs) {
|
||||
if (attrs instanceof HTMLTools.TemplateTag)
|
||||
return this.codegen.codeGenTemplateTag(attrs);
|
||||
|
||||
// call super (e.g. for case where `attrs` is an array)
|
||||
return HTML.TransformingVisitor.prototype.visitAttributes.call(this, attrs);
|
||||
},
|
||||
visitAttribute: function (name, value, tag) {
|
||||
this.inAttributeValue = true;
|
||||
var result = this.visit(value);
|
||||
this.inAttributeValue = false;
|
||||
|
||||
if (result !== value) {
|
||||
// some template tags must have been replaced, because otherwise
|
||||
// we try to keep things `===` when transforming. Wrap the code
|
||||
// in a function as per the rules. You can't have
|
||||
// `{id: Blaze.View(...)}` as an attributes dict because the View
|
||||
// would be rendered more than once; you need to wrap it in a function
|
||||
// so that it's a different View each time.
|
||||
return BlazeTools.EmitCode(this.codegen.codeGenBlock(result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
SpacebarsCompiler.codeGen = function (parseTree, options) {
|
||||
// is this a template, rather than a block passed to
|
||||
// a block helper, say
|
||||
var isTemplate = (options && options.isTemplate);
|
||||
var isBody = (options && options.isBody);
|
||||
var sourceName = (options && options.sourceName);
|
||||
|
||||
var tree = parseTree;
|
||||
|
||||
// The flags `isTemplate` and `isBody` are kind of a hack.
|
||||
if (isTemplate || isBody) {
|
||||
// optimizing fragments would require being smarter about whether we are
|
||||
// in a TEXTAREA, say.
|
||||
tree = SpacebarsCompiler.optimize(tree);
|
||||
}
|
||||
|
||||
// throws an error if using `{{> React}}` with siblings
|
||||
new ReactComponentSiblingForbidder({sourceName: sourceName})
|
||||
.visit(tree);
|
||||
|
||||
var codegen = new SpacebarsCompiler.CodeGen;
|
||||
tree = (new SpacebarsCompiler._TemplateTagReplacer(
|
||||
{codegen: codegen})).visit(tree);
|
||||
|
||||
var code = '(function () { ';
|
||||
if (isTemplate || isBody) {
|
||||
code += 'var view = this; ';
|
||||
}
|
||||
code += 'return ';
|
||||
code += BlazeTools.toJS(tree);
|
||||
code += '; })';
|
||||
|
||||
code = SpacebarsCompiler._beautify(code);
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
SpacebarsCompiler._beautify = function (code) {
|
||||
if (Package['minifier-js'] && Package['minifier-js'].UglifyJSMinify) {
|
||||
var result = Package['minifier-js'].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;
|
||||
}
|
||||
};
|
||||
@@ -1,296 +0,0 @@
|
||||
coffee.runCompilerOutputTests = (run) ->
|
||||
run "abc",
|
||||
"""
|
||||
function () {
|
||||
var view = this;
|
||||
return "abc";
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{foo}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze.View("lookup:foo", function() {
|
||||
return Spacebars.mustache(view.lookup("foo"));
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{foo bar}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze.View("lookup:foo", function() {
|
||||
return Spacebars.mustache(view.lookup("foo"),
|
||||
view.lookup("bar"));
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{foo x=bar}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze.View("lookup:foo", function() {
|
||||
return Spacebars.mustache(view.lookup("foo"), Spacebars.kw({
|
||||
x: view.lookup("bar")
|
||||
}));
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{foo.bar baz}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze.View("lookup:foo.bar", function() {
|
||||
return Spacebars.mustache(Spacebars.dot(
|
||||
view.lookup("foo"), "bar"),
|
||||
view.lookup("baz"));
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{foo.bar (baz qux)}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze.View("lookup:foo.bar", function() {
|
||||
return Spacebars.mustache(Spacebars.dot(
|
||||
view.lookup("foo"), "bar"),
|
||||
Spacebars.dataMustache(view.lookup("baz"), view.lookup("qux")));
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
run "{{foo bar.baz}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze.View("lookup:foo", function() {
|
||||
return Spacebars.mustache(view.lookup("foo"),
|
||||
Spacebars.dot(view.lookup("bar"), "baz"));
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{foo x=bar.baz}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze.View("lookup:foo", function() {
|
||||
return Spacebars.mustache(view.lookup("foo"), Spacebars.kw({
|
||||
x: Spacebars.dot(view.lookup("bar"), "baz")
|
||||
}));
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{#foo}}abc{{/foo}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Spacebars.include(view.lookupTemplate("foo"), (function() {
|
||||
return "abc";
|
||||
}));
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{#if cond}}aaa{{else}}bbb{{/if}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze.If(function () {
|
||||
return Spacebars.call(view.lookup("cond"));
|
||||
}, (function() {
|
||||
return "aaa";
|
||||
}), (function() {
|
||||
return "bbb";
|
||||
}));
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{!-- --}}{{#if cond}}aaa{{!\n}}{{else}}{{!}}bbb{{!-- --}}{{/if}}{{!}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze.If(function () {
|
||||
return Spacebars.call(view.lookup("cond"));
|
||||
}, (function() {
|
||||
return "aaa";
|
||||
}), (function() {
|
||||
return "bbb";
|
||||
}));
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{> foo bar}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze._TemplateWith(function() {
|
||||
return Spacebars.call(view.lookup("bar"));
|
||||
}, function() {
|
||||
return Spacebars.include(view.lookupTemplate("foo"));
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{> foo x=bar}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze._TemplateWith(function() {
|
||||
return {
|
||||
x: Spacebars.call(view.lookup("bar"))
|
||||
};
|
||||
}, function() {
|
||||
return Spacebars.include(view.lookupTemplate("foo"));
|
||||
});
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
run "{{> foo bar.baz}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze._TemplateWith(function() {
|
||||
return Spacebars.call(Spacebars.dot(view.lookup("bar"), "baz"));
|
||||
}, function() {
|
||||
return Spacebars.include(view.lookupTemplate("foo"));
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{> foo x=bar.baz}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze._TemplateWith(function() {
|
||||
return {
|
||||
x: Spacebars.call(Spacebars.dot(view.lookup("bar"), "baz"))
|
||||
};
|
||||
}, function() {
|
||||
return Spacebars.include(view.lookupTemplate("foo"));
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{> foo bar baz}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze._TemplateWith(function() {
|
||||
return Spacebars.dataMustache(view.lookup("bar"), view.lookup("baz"));
|
||||
}, function() {
|
||||
return Spacebars.include(view.lookupTemplate("foo"));
|
||||
});
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
run "{{#foo bar baz}}aaa{{/foo}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze._TemplateWith(function() {
|
||||
return Spacebars.dataMustache(view.lookup("bar"), view.lookup("baz"));
|
||||
}, function() {
|
||||
return Spacebars.include(view.lookupTemplate("foo"), (function() {
|
||||
return "aaa";
|
||||
}));
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{#foo p.q r.s}}aaa{{/foo}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return Blaze._TemplateWith(function() {
|
||||
return Spacebars.dataMustache(Spacebars.dot(view.lookup("p"), "q"), Spacebars.dot(view.lookup("r"), "s"));
|
||||
}, function() {
|
||||
return Spacebars.include(view.lookupTemplate("foo"), (function() {
|
||||
return "aaa";
|
||||
}));
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
run "<a {{b}}></a>",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return HTML.A(HTML.Attrs(function() {
|
||||
return Spacebars.attrMustache(view.lookup("b"));
|
||||
}));
|
||||
}
|
||||
"""
|
||||
|
||||
run "<a {{b}} c=d{{e}}f></a>",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return HTML.A(HTML.Attrs({
|
||||
c: (function() { return [
|
||||
"d",
|
||||
Spacebars.mustache(view.lookup("e")),
|
||||
"f" ]; })
|
||||
}, function() {
|
||||
return Spacebars.attrMustache(view.lookup("b"));
|
||||
}));
|
||||
}
|
||||
"""
|
||||
|
||||
run "<asdf>{{foo}}</asdf>",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return HTML.getTag("asdf")(Blaze.View("lookup:foo", function() {
|
||||
return Spacebars.mustache(view.lookup("foo"));
|
||||
}));
|
||||
}
|
||||
"""
|
||||
|
||||
run "<textarea>{{foo}}</textarea>",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return HTML.TEXTAREA({value: (function () {
|
||||
return Spacebars.mustache(view.lookup("foo"));
|
||||
}) });
|
||||
}
|
||||
"""
|
||||
|
||||
run "<textarea>{{{{|{{|foo}}</textarea>",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return HTML.TEXTAREA({value: (function () {
|
||||
return [ "{{{{", "{{", "foo}}" ];
|
||||
}) });
|
||||
}
|
||||
"""
|
||||
|
||||
run "{{|foo}}",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return [ "{{", "foo}}" ];
|
||||
}
|
||||
"""
|
||||
|
||||
run "<a b={{{|></a>",
|
||||
"""
|
||||
function() {
|
||||
var view = this;
|
||||
return HTML.A({
|
||||
b: (function () {
|
||||
return "{{{";
|
||||
})
|
||||
});
|
||||
}
|
||||
"""
|
||||
@@ -1,189 +0,0 @@
|
||||
// Optimize parts of an HTMLjs tree into raw HTML strings when they don't
|
||||
// contain template tags.
|
||||
|
||||
var constant = function (value) {
|
||||
return function () { return value; };
|
||||
};
|
||||
|
||||
var OPTIMIZABLE = {
|
||||
NONE: 0,
|
||||
PARTS: 1,
|
||||
FULL: 2
|
||||
};
|
||||
|
||||
// We can only turn content into an HTML string if it contains no template
|
||||
// tags and no "tricky" HTML tags. If we can optimize the entire content
|
||||
// into a string, we return OPTIMIZABLE.FULL. If the we are given an
|
||||
// unoptimizable node, we return OPTIMIZABLE.NONE. If we are given a tree
|
||||
// that contains an unoptimizable node somewhere, we return OPTIMIZABLE.PARTS.
|
||||
//
|
||||
// For example, we always create SVG elements programmatically, since SVG
|
||||
// doesn't have innerHTML. If we are given an SVG element, we return NONE.
|
||||
// However, if we are given a big tree that contains SVG somewhere, we
|
||||
// return PARTS so that the optimizer can descend into the tree and optimize
|
||||
// other parts of it.
|
||||
var CanOptimizeVisitor = HTML.Visitor.extend();
|
||||
CanOptimizeVisitor.def({
|
||||
visitNull: constant(OPTIMIZABLE.FULL),
|
||||
visitPrimitive: constant(OPTIMIZABLE.FULL),
|
||||
visitComment: constant(OPTIMIZABLE.FULL),
|
||||
visitCharRef: constant(OPTIMIZABLE.FULL),
|
||||
visitRaw: constant(OPTIMIZABLE.FULL),
|
||||
visitObject: constant(OPTIMIZABLE.NONE),
|
||||
visitFunction: constant(OPTIMIZABLE.NONE),
|
||||
visitArray: function (x) {
|
||||
for (var i = 0; i < x.length; i++)
|
||||
if (this.visit(x[i]) !== OPTIMIZABLE.FULL)
|
||||
return OPTIMIZABLE.PARTS;
|
||||
return OPTIMIZABLE.FULL;
|
||||
},
|
||||
visitTag: function (tag) {
|
||||
var tagName = tag.tagName;
|
||||
if (tagName === 'textarea') {
|
||||
// optimizing into a TEXTAREA's RCDATA would require being a little
|
||||
// more clever.
|
||||
return OPTIMIZABLE.NONE;
|
||||
} else if (tagName === 'script') {
|
||||
// script tags don't work when rendered from strings
|
||||
return OPTIMIZABLE.NONE;
|
||||
} else if (! (HTML.isKnownElement(tagName) &&
|
||||
! HTML.isKnownSVGElement(tagName))) {
|
||||
// foreign elements like SVG can't be stringified for innerHTML.
|
||||
return OPTIMIZABLE.NONE;
|
||||
} else if (tagName === 'table') {
|
||||
// Avoid ever producing HTML containing `<table><tr>...`, 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;
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
Package.describe({
|
||||
summary: "Compiler for Spacebars template language",
|
||||
version: '1.0.12'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.export('SpacebarsCompiler');
|
||||
|
||||
api.use('htmljs');
|
||||
api.use('html-tools');
|
||||
api.use('blaze-tools');
|
||||
|
||||
api.use('underscore');
|
||||
// The templating plugin will pull in minifier-js, so that generated code will
|
||||
// be beautified. But it's a weak dependency so that eg boilerplate-generator
|
||||
// doesn't pull in the minifier.
|
||||
api.use('minifier-js', ['server'], { weak: true });
|
||||
api.addFiles(['templatetag.js',
|
||||
'optimizer.js',
|
||||
'react.js',
|
||||
'codegen.js',
|
||||
'compiler.js']);
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use([
|
||||
'underscore',
|
||||
'spacebars-compiler',
|
||||
'tinytest',
|
||||
'blaze-tools',
|
||||
'coffeescript',
|
||||
'spacebars',
|
||||
'blaze'
|
||||
]);
|
||||
api.addFiles('spacebars_tests.js');
|
||||
api.addFiles('compile_tests.js');
|
||||
api.addFiles('compiler_output_tests.coffee');
|
||||
});
|
||||
44
packages/spacebars-compiler/react.js
vendored
44
packages/spacebars-compiler/react.js
vendored
@@ -1,44 +0,0 @@
|
||||
// A visitor to ensure that React components included via the `{{>
|
||||
// React}}` template defined in the react-template-helper package are
|
||||
// the only child in their parent component. Otherwise `React.render`
|
||||
// would eliminate all of their sibling nodes.
|
||||
//
|
||||
// It's a little strange that this logic is in spacebars-compiler if
|
||||
// it's only relevant to a specific package but there's no way to have
|
||||
// a package hook into a build plugin.
|
||||
ReactComponentSiblingForbidder = HTML.Visitor.extend();
|
||||
ReactComponentSiblingForbidder.def({
|
||||
visitArray: function (array, parentTag) {
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
this.visit(array[i], parentTag);
|
||||
}
|
||||
},
|
||||
visitObject: function (obj, parentTag) {
|
||||
if (obj.type === "INCLUSION" && obj.path.length === 1 && obj.path[0] === "React") {
|
||||
if (!parentTag) {
|
||||
throw new Error(
|
||||
"{{> React}} must be used in a container element"
|
||||
+ (this.sourceName ? (" in " + this.sourceName) : "")
|
||||
+ ". Learn more at https://github.com/meteor/meteor/wiki/React-components-must-be-the-only-thing-in-their-wrapper-element");
|
||||
}
|
||||
|
||||
var numSiblings = 0;
|
||||
for (var i = 0; i < parentTag.children.length; i++) {
|
||||
var child = parentTag.children[i];
|
||||
if (child !== obj && !(typeof child === "string" && child.match(/^\s*$/))) {
|
||||
numSiblings++;
|
||||
}
|
||||
}
|
||||
|
||||
if (numSiblings > 0) {
|
||||
throw new Error(
|
||||
"{{> React}} must be used as the only child in a container element"
|
||||
+ (this.sourceName ? (" in " + this.sourceName) : "")
|
||||
+ ". Learn more at https://github.com/meteor/meteor/wiki/React-components-must-be-the-only-thing-in-their-wrapper-element");
|
||||
}
|
||||
}
|
||||
},
|
||||
visitTag: function (tag) {
|
||||
this.visitArray(tag.children, tag /*parentTag*/);
|
||||
}
|
||||
});
|
||||
@@ -1,317 +0,0 @@
|
||||
Tinytest.add("spacebars-compiler - stache tags", function (test) {
|
||||
|
||||
var run = function (input, expected) {
|
||||
if (typeof expected === "string") {
|
||||
// test for error starting with string `expected`
|
||||
var msg = '';
|
||||
test.throws(function () {
|
||||
try {
|
||||
SpacebarsCompiler.TemplateTag.parse(input);
|
||||
} catch (e) {
|
||||
msg = e.message;
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
test.equal(msg.slice(0, expected.length), expected);
|
||||
} else {
|
||||
var result = SpacebarsCompiler.TemplateTag.parse(input);
|
||||
test.equal(result, expected);
|
||||
}
|
||||
};
|
||||
|
||||
run('{{foo}}', {type: 'DOUBLE', path: ['foo'], args: []});
|
||||
run('{{foo3}}', {type: 'DOUBLE', path: ['foo3'], args: []});
|
||||
run('{{{foo}}}', {type: 'TRIPLE', path: ['foo'], args: []});
|
||||
run('{{{foo}}', "Expected `}}}`");
|
||||
run('{{{foo', "Expected");
|
||||
run('{{foo', "Expected");
|
||||
run('{{ {foo}}}', "Unknown stache tag");
|
||||
run('{{{{foo}}}}', "Unknown stache tag");
|
||||
run('{{{>foo}}}', "Unknown stache tag");
|
||||
run('{{>>foo}}', "Unknown stache tag");
|
||||
run('{{! asdf }}', {type: 'COMMENT', value: ' asdf '});
|
||||
run('{{ ! asdf }}', {type: 'COMMENT', value: ' asdf '});
|
||||
run('{{ ! asdf }asdf', "Unclosed");
|
||||
run('{{!-- asdf --}}', {type: 'BLOCKCOMMENT', value: ' asdf '});
|
||||
run('{{ !-- asdf -- }}', {type: 'BLOCKCOMMENT', value: ' asdf '});
|
||||
run('{{ !-- {{asdf}} -- }}', {type: 'BLOCKCOMMENT', value: ' {{asdf}} '});
|
||||
run('{{ !-- {{as--df}} --}}', {type: 'BLOCKCOMMENT', value: ' {{as--df}} '});
|
||||
run('{{ !-- asdf }asdf', "Unclosed");
|
||||
run('{{ !-- asdf --}asdf', "Unclosed");
|
||||
run('{{else}}', {type: 'ELSE'});
|
||||
run('{{ else }}', {type: 'ELSE'});
|
||||
run('{{else x}}', "Expected");
|
||||
run('{{else_x}}', {type: 'DOUBLE', path: ['else_x'], args: []});
|
||||
run('{{/if}}', {type: 'BLOCKCLOSE', path: ['if']});
|
||||
run('{{ / if }}', {type: 'BLOCKCLOSE', path: ['if']});
|
||||
run('{{/if x}}', "Expected");
|
||||
run('{{#if}}', {type: 'BLOCKOPEN', path: ['if'], args: []});
|
||||
run('{{ # if }}', {type: 'BLOCKOPEN', path: ['if'], args: []});
|
||||
run('{{#if_3}}', {type: 'BLOCKOPEN', path: ['if_3'], args: []});
|
||||
run('{{>x}}', {type: 'INCLUSION', path: ['x'], args: []});
|
||||
run('{{ > x }}', {type: 'INCLUSION', path: ['x'], args: []});
|
||||
run('{{>x_3}}', {type: 'INCLUSION', path: ['x_3'], args: []});
|
||||
|
||||
|
||||
|
||||
run('{{foo 3}}', {type: 'DOUBLE', path: ['foo'], args: [['NUMBER', 3]]});
|
||||
run('{{ foo 3 }}', {type: 'DOUBLE', path: ['foo'], args: [['NUMBER', 3]]});
|
||||
run('{{#foo 3}}', {type: 'BLOCKOPEN', path: ['foo'], args: [['NUMBER', 3]]});
|
||||
run('{{ # foo 3 }}', {type: 'BLOCKOPEN', path: ['foo'],
|
||||
args: [['NUMBER', 3]]});
|
||||
run('{{>foo 3}}', {type: 'INCLUSION', path: ['foo'], args: [['NUMBER', 3]]});
|
||||
run('{{ > foo 3 }}', {type: 'INCLUSION', path: ['foo'],
|
||||
args: [['NUMBER', 3]]});
|
||||
run('{{{foo 3}}}', {type: 'TRIPLE', path: ['foo'], args: [['NUMBER', 3]]});
|
||||
|
||||
run('{{foo bar ./foo foo/bar a.b.c baz=qux x3=.}}',
|
||||
{type: 'DOUBLE', path: ['foo'],
|
||||
args: [['PATH', ['bar']],
|
||||
['PATH', ['.', 'foo']],
|
||||
['PATH', ['foo', 'bar']],
|
||||
['PATH', ['a', 'b', 'c']],
|
||||
['PATH', ['qux'], 'baz'],
|
||||
['PATH', ['.'], 'x3']]});
|
||||
|
||||
// nested expressions
|
||||
run('{{helper (subhelper ./arg) arg.sub (args.passedHelper)}}', {
|
||||
type: 'DOUBLE', path: ['helper'],
|
||||
args: [
|
||||
[
|
||||
'EXPR', {
|
||||
type: "EXPR", path: ["subhelper"],
|
||||
args: [["PATH", [".", "arg"]]]
|
||||
}
|
||||
], [
|
||||
"PATH", ["arg", "sub"]
|
||||
], [
|
||||
"EXPR", {
|
||||
type: "EXPR",
|
||||
path: ["args", "passedHelper"],
|
||||
args: []
|
||||
}
|
||||
]
|
||||
]
|
||||
});
|
||||
run('{{helper (h arg}}', "Expected `)`");
|
||||
run('{{helper (h arg))}}', "Expected");
|
||||
run('{{helper ) h arg}}', "Expected");
|
||||
run('{{(dyn) arg}}', "Expected ID");
|
||||
|
||||
run('{{{x 0.3 [0].[3] .4 ./[4]}}}',
|
||||
{type: 'TRIPLE', path: ['x'],
|
||||
args: [['NUMBER', 0.3],
|
||||
['PATH', ['0', '3']],
|
||||
['NUMBER', .4],
|
||||
['PATH', ['.', '4']]]});
|
||||
|
||||
run('{{# foo this this.x null z=null}}',
|
||||
{type: 'BLOCKOPEN', path: ['foo'],
|
||||
args: [['PATH', ['.']],
|
||||
['PATH', ['.', 'x']],
|
||||
['NULL', null],
|
||||
['NULL', null, 'z']]});
|
||||
|
||||
run('{{./foo 3}}', {type: 'DOUBLE', path: ['.', 'foo'], args: [['NUMBER', 3]]});
|
||||
run('{{this/foo 3}}', {type: 'DOUBLE', path: ['.', 'foo'], args: [['NUMBER', 3]]});
|
||||
run('{{../foo 3}}', {type: 'DOUBLE', path: ['..', 'foo'], args: [['NUMBER', 3]]});
|
||||
run('{{../../foo 3}}', {type: 'DOUBLE', path: ['...', 'foo'], args: [['NUMBER', 3]]});
|
||||
|
||||
run('{{foo x/..}}', "Expected");
|
||||
run('{{foo x/.}}', "Expected");
|
||||
|
||||
run('{{#a.b.c}}', {type: 'BLOCKOPEN', path: ['a', 'b', 'c'],
|
||||
args: []});
|
||||
run('{{> a.b.c}}', {type: 'INCLUSION', path: ['a', 'b', 'c'],
|
||||
args: []});
|
||||
|
||||
run('{{foo.[]/[]}}', {type: 'DOUBLE', path: ['foo', '', ''],
|
||||
args: []});
|
||||
run('{{x foo.[=]}}', {type: 'DOUBLE', path: ['x'],
|
||||
args: [['PATH', ['foo', '=']]]});
|
||||
run('{{[].foo}}', "Path can't start with empty string");
|
||||
|
||||
run('{{foo null}}', {type: 'DOUBLE', path: ['foo'],
|
||||
args: [['NULL', null]]});
|
||||
run('{{foo false}}', {type: 'DOUBLE', path: ['foo'],
|
||||
args: [['BOOLEAN', false]]});
|
||||
run('{{foo true}}', {type: 'DOUBLE', path: ['foo'],
|
||||
args: [['BOOLEAN', true]]});
|
||||
run('{{foo "bar"}}', {type: 'DOUBLE', path: ['foo'],
|
||||
args: [['STRING', 'bar']]});
|
||||
run("{{foo 'bar'}}", {type: 'DOUBLE', path: ['foo'],
|
||||
args: [['STRING', 'bar']]});
|
||||
|
||||
run('{{foo -1 -2}}', {type: 'DOUBLE', path: ['foo'],
|
||||
args: [['NUMBER', -1], ['NUMBER', -2]]});
|
||||
|
||||
run('{{x "\'"}}', {type: 'DOUBLE', path: ['x'], args: [['STRING', "'"]]});
|
||||
run('{{x \'"\'}}', {type: 'DOUBLE', path: ['x'], args: [['STRING', '"']]});
|
||||
|
||||
run('{{> foo x=1 y=2}}',
|
||||
{type: 'INCLUSION', path: ['foo'],
|
||||
args: [['NUMBER', 1, 'x'],
|
||||
['NUMBER', 2, 'y']]});
|
||||
// spaces around '=' are fine
|
||||
run('{{> foo x = 1 y = 2}}',
|
||||
{type: 'INCLUSION', path: ['foo'],
|
||||
args: [['NUMBER', 1, 'x'],
|
||||
['NUMBER', 2, 'y']]});
|
||||
run('{{> foo with-dashes=1 another-one=2}}',
|
||||
{type: 'INCLUSION', path: ['foo'],
|
||||
args: [['NUMBER', 1, 'with-dashes'],
|
||||
['NUMBER', 2, 'another-one']]});
|
||||
run('{{> foo 1="keyword can start with a number"}}',
|
||||
{type: 'INCLUSION', path: ['foo'],
|
||||
args: [['STRING', 'keyword can start with a number', '1']]});
|
||||
run('{{> foo disallow-dashes-in-posarg}}',
|
||||
"Expected");
|
||||
run('{{> foo disallow-#=1}}',
|
||||
"Expected");
|
||||
run('{{> foo disallow->=1}}',
|
||||
"Expected");
|
||||
run('{{> foo disallow-{=1}}',
|
||||
"Expected");
|
||||
run('{{> foo disallow-(=1}}',
|
||||
"Expected");
|
||||
run('{{> foo disallow-}=1}}',
|
||||
"Expected");
|
||||
run('{{> foo disallow-)=1}}',
|
||||
"Expected");
|
||||
run('{{> foo x=1 y=2 z}}',
|
||||
"Can't have a non-keyword argument");
|
||||
|
||||
run('{{true.foo}}', "Can't use");
|
||||
run('{{foo.this}}', "Can only use");
|
||||
run('{{./this}}', "Can only use");
|
||||
run('{{../this}}', "Can only use");
|
||||
|
||||
run('{{foo "="}}', {type: 'DOUBLE', path: ['foo'],
|
||||
args: [['STRING', '=']]});
|
||||
|
||||
run('{{| asdf', { type: 'ESCAPE', value: '{{' });
|
||||
run('{{{| asdf', { type: 'ESCAPE', value: '{{{' });
|
||||
run('{{{{| asdf', { type: 'ESCAPE', value: '{{{{' });
|
||||
});
|
||||
|
||||
|
||||
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);
|
||||
test.equal(Spacebars.dot(
|
||||
{x:1, y: function () { return this.x+1; }}, 'y')(), 2);
|
||||
test.equal(Spacebars.dot(
|
||||
function () {
|
||||
return {x:1, y: function () { return this.x+1; }};
|
||||
}, 'y')(), 2);
|
||||
|
||||
var m = 1;
|
||||
var mget = function () {
|
||||
return {
|
||||
answer: m,
|
||||
getAnswer: function () {
|
||||
return this.answer;
|
||||
}
|
||||
};
|
||||
};
|
||||
var mgetDotAnswer = Spacebars.dot(mget, 'answer');
|
||||
test.equal(mgetDotAnswer, 1);
|
||||
|
||||
m = 3;
|
||||
var mgetDotGetAnswer = Spacebars.dot(mget, 'getAnswer');
|
||||
test.equal(mgetDotGetAnswer(), 3);
|
||||
m = 4;
|
||||
test.equal(mgetDotGetAnswer(), 3);
|
||||
|
||||
var closet = {
|
||||
mget: mget,
|
||||
mget2: function () {
|
||||
return this.mget();
|
||||
}
|
||||
};
|
||||
|
||||
m = 5;
|
||||
var f1 = Spacebars.dot(closet, 'mget', 'answer');
|
||||
m = 6;
|
||||
var f2 = Spacebars.dot(closet, 'mget2', 'answer');
|
||||
test.equal(f2, 6);
|
||||
m = 8;
|
||||
var f3 = Spacebars.dot(closet, 'mget2', 'getAnswer');
|
||||
m = 9;
|
||||
test.equal(f3(), 8);
|
||||
|
||||
test.equal(Spacebars.dot(0, 'abc', 'def'), 0);
|
||||
test.equal(Spacebars.dot(function () { return null; }, 'abc', 'def'), null);
|
||||
test.equal(Spacebars.dot(function () { return 0; }, 'abc', 'def'), 0);
|
||||
|
||||
// test that in `foo.bar`, `bar` may be a function that takes arguments.
|
||||
test.equal(Spacebars.dot(
|
||||
{ one: 1, inc: function (x) { return this.one + x; } }, 'inc')(6), 7);
|
||||
test.equal(Spacebars.dot(
|
||||
function () {
|
||||
return { one: 1, inc: function (x) { return this.one + x; } };
|
||||
}, 'inc')(8), 9);
|
||||
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
Tinytest.add("spacebars-compiler - parse", function (test) {
|
||||
test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('{{foo}}')),
|
||||
'SpacebarsCompiler.TemplateTag({type: "DOUBLE", path: ["foo"]})');
|
||||
|
||||
test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('{{!foo}}')), 'null');
|
||||
test.equal(BlazeTools.toJS(SpacebarsCompiler.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(BlazeTools.toJS(SpacebarsCompiler.parse('{{#foo}}x{{/foo}}')),
|
||||
'SpacebarsCompiler.TemplateTag({type: "BLOCKOPEN", path: ["foo"], content: "x"})');
|
||||
|
||||
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(BlazeTools.toJS(SpacebarsCompiler.parse('<div>hello</div> {{#foo}}<div>{{#bar}}world{{/bar}}</div>{{/foo}}')),
|
||||
'[HTML.DIV("hello"), " ", SpacebarsCompiler.TemplateTag({type: "BLOCKOPEN", path: ["foo"], content: HTML.DIV(SpacebarsCompiler.TemplateTag({type: "BLOCKOPEN", path: ["bar"], content: "world"}))})]');
|
||||
|
||||
|
||||
test.throws(function () {
|
||||
SpacebarsCompiler.parse('<a {{{x}}}></a>');
|
||||
});
|
||||
test.throws(function () {
|
||||
SpacebarsCompiler.parse('<a {{#if x}}{{/if}}></a>');
|
||||
});
|
||||
test.throws(function () {
|
||||
SpacebarsCompiler.parse('<a {{k}}={[v}}></a>');
|
||||
});
|
||||
test.throws(function () {
|
||||
SpacebarsCompiler.parse('<a x{{y}}></a>');
|
||||
});
|
||||
test.throws(function () {
|
||||
SpacebarsCompiler.parse('<a x{{y}}=z></a>');
|
||||
});
|
||||
test.throws(function () {
|
||||
SpacebarsCompiler.parse('<a {{> x}}></a>');
|
||||
});
|
||||
|
||||
test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('<a {{! x--}} b=c{{! x}} {{! x}}></a>')),
|
||||
'HTML.A({b: "c"})');
|
||||
|
||||
test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('<a {{!-- x--}} b=c{{ !-- x --}} {{!-- x -- }}></a>')),
|
||||
'HTML.A({b: "c"})');
|
||||
|
||||
// currently, if there are only comments, the attribute is truthy. This is
|
||||
// because comments are stripped during tokenization. If we include
|
||||
// comments in the token stream, these cases will become falsy for selected.
|
||||
test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('<input selected={{!foo}}>')),
|
||||
'HTML.INPUT({selected: ""})');
|
||||
test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('<input selected={{!foo}}{{!bar}}>')),
|
||||
'HTML.INPUT({selected: ""})');
|
||||
test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('<input selected={{!--foo--}}>')),
|
||||
'HTML.INPUT({selected: ""})');
|
||||
test.equal(BlazeTools.toJS(SpacebarsCompiler.parse('<input selected={{!--foo--}}{{!--bar--}}>')),
|
||||
'HTML.INPUT({selected: ""})');
|
||||
|
||||
});
|
||||
@@ -1,506 +0,0 @@
|
||||
SpacebarsCompiler = {};
|
||||
|
||||
// A TemplateTag is the result of parsing a single `{{...}}` tag.
|
||||
//
|
||||
// The `.type` of a TemplateTag is one of:
|
||||
//
|
||||
// - `"DOUBLE"` - `{{foo}}`
|
||||
// - `"TRIPLE"` - `{{{foo}}}`
|
||||
// - `"EXPR"` - `(foo)`
|
||||
// - `"COMMENT"` - `{{! foo}}`
|
||||
// - `"BLOCKCOMMENT" - `{{!-- foo--}}`
|
||||
// - `"INCLUSION"` - `{{> foo}}`
|
||||
// - `"BLOCKOPEN"` - `{{#foo}}`
|
||||
// - `"BLOCKCLOSE"` - `{{/foo}}`
|
||||
// - `"ELSE"` - `{{else}}`
|
||||
// - `"ESCAPE"` - `{{|`, `{{{|`, `{{{{|` and so on
|
||||
//
|
||||
// Besides `type`, the mandatory properties of a TemplateTag are:
|
||||
//
|
||||
// - `path` - An array of one or more strings. The path of `{{foo.bar}}`
|
||||
// is `["foo", "bar"]`. Applies to DOUBLE, TRIPLE, INCLUSION, BLOCKOPEN,
|
||||
// and BLOCKCLOSE.
|
||||
//
|
||||
// - `args` - An array of zero or more argument specs. An argument spec
|
||||
// is a two or three element array, consisting of a type, value, and
|
||||
// optional keyword name. For example, the `args` of `{{foo "bar" x=3}}`
|
||||
// are `[["STRING", "bar"], ["NUMBER", 3, "x"]]`. Applies to DOUBLE,
|
||||
// TRIPLE, INCLUSION, and BLOCKOPEN.
|
||||
//
|
||||
// - `value` - A string of the comment's text. Applies to COMMENT and
|
||||
// BLOCKCOMMENT.
|
||||
//
|
||||
// These additional are typically set during parsing:
|
||||
//
|
||||
// - `position` - The HTMLTools.TEMPLATE_TAG_POSITION specifying at what sort
|
||||
// of site the TemplateTag was encountered (e.g. at element level or as
|
||||
// part of an attribute value). Its absence implies
|
||||
// TEMPLATE_TAG_POSITION.ELEMENT.
|
||||
//
|
||||
// - `content` and `elseContent` - When a BLOCKOPEN tag's contents are
|
||||
// parsed, they are put here. `elseContent` will only be present if
|
||||
// an `{{else}}` was found.
|
||||
|
||||
var TEMPLATE_TAG_POSITION = HTMLTools.TEMPLATE_TAG_POSITION;
|
||||
|
||||
TemplateTag = SpacebarsCompiler.TemplateTag = function () {
|
||||
HTMLTools.TemplateTag.apply(this, arguments);
|
||||
};
|
||||
TemplateTag.prototype = new HTMLTools.TemplateTag;
|
||||
TemplateTag.prototype.constructorName = 'SpacebarsCompiler.TemplateTag';
|
||||
|
||||
var makeStacheTagStartRegex = function (r) {
|
||||
return new RegExp(r.source + /(?![{>!#/])/.source,
|
||||
r.ignoreCase ? 'i' : '');
|
||||
};
|
||||
|
||||
// "starts" regexes are used to see what type of template
|
||||
// tag the parser is looking at. They must match a non-empty
|
||||
// result, but not the interesting part of the tag.
|
||||
var starts = {
|
||||
ESCAPE: /^\{\{(?=\{*\|)/,
|
||||
ELSE: makeStacheTagStartRegex(/^\{\{\s*else(?=[\s}])/i),
|
||||
DOUBLE: makeStacheTagStartRegex(/^\{\{\s*(?!\s)/),
|
||||
TRIPLE: makeStacheTagStartRegex(/^\{\{\{\s*(?!\s)/),
|
||||
BLOCKCOMMENT: makeStacheTagStartRegex(/^\{\{\s*!--/),
|
||||
COMMENT: makeStacheTagStartRegex(/^\{\{\s*!/),
|
||||
INCLUSION: makeStacheTagStartRegex(/^\{\{\s*>\s*(?!\s)/),
|
||||
BLOCKOPEN: makeStacheTagStartRegex(/^\{\{\s*#\s*(?!\s)/),
|
||||
BLOCKCLOSE: makeStacheTagStartRegex(/^\{\{\s*\/\s*(?!\s)/)
|
||||
};
|
||||
|
||||
var ends = {
|
||||
DOUBLE: /^\s*\}\}/,
|
||||
TRIPLE: /^\s*\}\}\}/,
|
||||
EXPR: /^\s*\)/
|
||||
};
|
||||
|
||||
var endsString = {
|
||||
DOUBLE: '}}',
|
||||
TRIPLE: '}}}',
|
||||
EXPR: ')'
|
||||
};
|
||||
|
||||
// Parse a tag from the provided scanner or string. If the input
|
||||
// doesn't start with `{{`, returns null. Otherwise, either succeeds
|
||||
// and returns a SpacebarsCompiler.TemplateTag, or throws an error (using
|
||||
// `scanner.fatal` if a scanner is provided).
|
||||
TemplateTag.parse = function (scannerOrString) {
|
||||
var scanner = scannerOrString;
|
||||
if (typeof scanner === 'string')
|
||||
scanner = new HTMLTools.Scanner(scannerOrString);
|
||||
|
||||
if (! (scanner.peek() === '{' &&
|
||||
(scanner.rest()).slice(0, 2) === '{{'))
|
||||
return null;
|
||||
|
||||
var run = function (regex) {
|
||||
// regex is assumed to start with `^`
|
||||
var result = regex.exec(scanner.rest());
|
||||
if (! result)
|
||||
return null;
|
||||
var ret = result[0];
|
||||
scanner.pos += ret.length;
|
||||
return ret;
|
||||
};
|
||||
|
||||
var advance = function (amount) {
|
||||
scanner.pos += amount;
|
||||
};
|
||||
|
||||
var scanIdentifier = function (isFirstInPath) {
|
||||
var id = BlazeTools.parseExtendedIdentifierName(scanner);
|
||||
if (! id) {
|
||||
expected('IDENTIFIER');
|
||||
}
|
||||
if (isFirstInPath &&
|
||||
(id === 'null' || id === 'true' || id === 'false'))
|
||||
scanner.fatal("Can't use null, true, or false, as an identifier at start of path");
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
var scanPath = function () {
|
||||
var segments = [];
|
||||
|
||||
// handle initial `.`, `..`, `./`, `../`, `../..`, `../../`, etc
|
||||
var dots;
|
||||
if ((dots = run(/^[\.\/]+/))) {
|
||||
var ancestorStr = '.'; // eg `../../..` maps to `....`
|
||||
var endsWithSlash = /\/$/.test(dots);
|
||||
|
||||
if (endsWithSlash)
|
||||
dots = dots.slice(0, -1);
|
||||
|
||||
_.each(dots.split('/'), function(dotClause, index) {
|
||||
if (index === 0) {
|
||||
if (dotClause !== '.' && dotClause !== '..')
|
||||
expected("`.`, `..`, `./` or `../`");
|
||||
} else {
|
||||
if (dotClause !== '..')
|
||||
expected("`..` or `../`");
|
||||
}
|
||||
|
||||
if (dotClause === '..')
|
||||
ancestorStr += '.';
|
||||
});
|
||||
|
||||
segments.push(ancestorStr);
|
||||
|
||||
if (!endsWithSlash)
|
||||
return segments;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// scan a path segment
|
||||
|
||||
if (run(/^\[/)) {
|
||||
var seg = run(/^[\s\S]*?\]/);
|
||||
if (! seg)
|
||||
error("Unterminated path segment");
|
||||
seg = seg.slice(0, -1);
|
||||
if (! seg && ! segments.length)
|
||||
error("Path can't start with empty string");
|
||||
segments.push(seg);
|
||||
} else {
|
||||
var id = scanIdentifier(! segments.length);
|
||||
if (id === 'this') {
|
||||
if (! segments.length) {
|
||||
// initial `this`
|
||||
segments.push('.');
|
||||
} else {
|
||||
error("Can only use `this` at the beginning of a path.\nInstead of `foo.this` or `../this`, just write `foo` or `..`.");
|
||||
}
|
||||
} else {
|
||||
segments.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
var sep = run(/^[\.\/]/);
|
||||
if (! sep)
|
||||
break;
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
|
||||
// scan the keyword portion of a keyword argument
|
||||
// (the "foo" portion in "foo=bar").
|
||||
// Result is either the keyword matched, or null
|
||||
// if we're not at a keyword argument position.
|
||||
var scanArgKeyword = function () {
|
||||
var match = /^([^\{\}\(\)\>#=\s"'\[\]]+)\s*=\s*/.exec(scanner.rest());
|
||||
if (match) {
|
||||
scanner.pos += match[0].length;
|
||||
return match[1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// scan an argument; succeeds or errors.
|
||||
// Result is an array of two or three items:
|
||||
// type , value, and (indicating a keyword argument)
|
||||
// keyword name.
|
||||
var scanArg = function () {
|
||||
var keyword = scanArgKeyword(); // null if not parsing a kwarg
|
||||
var value = scanArgValue();
|
||||
return keyword ? value.concat(keyword) : value;
|
||||
};
|
||||
|
||||
// scan an argument value (for keyword or positional arguments);
|
||||
// succeeds or errors. Result is an array of type, value.
|
||||
var scanArgValue = function () {
|
||||
var startPos = scanner.pos;
|
||||
var result;
|
||||
if ((result = BlazeTools.parseNumber(scanner))) {
|
||||
return ['NUMBER', result.value];
|
||||
} else if ((result = BlazeTools.parseStringLiteral(scanner))) {
|
||||
return ['STRING', result.value];
|
||||
} else if (/^[\.\[]/.test(scanner.peek())) {
|
||||
return ['PATH', scanPath()];
|
||||
} else if (run(/^\(/)) {
|
||||
return ['EXPR', scanExpr('EXPR')];
|
||||
} else if ((result = BlazeTools.parseExtendedIdentifierName(scanner))) {
|
||||
var id = result;
|
||||
if (id === 'null') {
|
||||
return ['NULL', null];
|
||||
} else if (id === 'true' || id === 'false') {
|
||||
return ['BOOLEAN', id === 'true'];
|
||||
} else {
|
||||
scanner.pos = startPos; // unconsume `id`
|
||||
return ['PATH', scanPath()];
|
||||
}
|
||||
} else {
|
||||
expected('identifier, number, string, boolean, null, or a sub expression enclosed in "(", ")"');
|
||||
}
|
||||
};
|
||||
|
||||
var scanExpr = function (type) {
|
||||
var endType = type;
|
||||
if (type === 'INCLUSION' || type === 'BLOCKOPEN')
|
||||
endType = 'DOUBLE';
|
||||
|
||||
var tag = new TemplateTag;
|
||||
tag.type = type;
|
||||
tag.path = scanPath();
|
||||
tag.args = [];
|
||||
var foundKwArg = false;
|
||||
while (true) {
|
||||
run(/^\s*/);
|
||||
if (run(ends[endType]))
|
||||
break;
|
||||
else if (/^[})]/.test(scanner.peek())) {
|
||||
expected('`' + endsString[endType] + '`');
|
||||
}
|
||||
var newArg = scanArg();
|
||||
if (newArg.length === 3) {
|
||||
foundKwArg = true;
|
||||
} else {
|
||||
if (foundKwArg)
|
||||
error("Can't have a non-keyword argument after a keyword argument");
|
||||
}
|
||||
tag.args.push(newArg);
|
||||
|
||||
// expect a whitespace or a closing ')' or '}'
|
||||
if (run(/^(?=[\s})])/) !== '')
|
||||
expected('space');
|
||||
}
|
||||
|
||||
return tag;
|
||||
};
|
||||
|
||||
var type;
|
||||
|
||||
var error = function (msg) {
|
||||
scanner.fatal(msg);
|
||||
};
|
||||
|
||||
var expected = function (what) {
|
||||
error('Expected ' + what);
|
||||
};
|
||||
|
||||
// must do ESCAPE first, immediately followed by ELSE
|
||||
// order of others doesn't matter
|
||||
if (run(starts.ESCAPE)) type = 'ESCAPE';
|
||||
else if (run(starts.ELSE)) type = 'ELSE';
|
||||
else if (run(starts.DOUBLE)) type = 'DOUBLE';
|
||||
else if (run(starts.TRIPLE)) type = 'TRIPLE';
|
||||
else if (run(starts.BLOCKCOMMENT)) type = 'BLOCKCOMMENT';
|
||||
else if (run(starts.COMMENT)) type = 'COMMENT';
|
||||
else if (run(starts.INCLUSION)) type = 'INCLUSION';
|
||||
else if (run(starts.BLOCKOPEN)) type = 'BLOCKOPEN';
|
||||
else if (run(starts.BLOCKCLOSE)) type = 'BLOCKCLOSE';
|
||||
else
|
||||
error('Unknown stache tag');
|
||||
|
||||
var tag = new TemplateTag;
|
||||
tag.type = type;
|
||||
|
||||
if (type === 'BLOCKCOMMENT') {
|
||||
var result = run(/^[\s\S]*?--\s*?\}\}/);
|
||||
if (! result)
|
||||
error("Unclosed block comment");
|
||||
tag.value = result.slice(0, result.lastIndexOf('--'));
|
||||
} else if (type === 'COMMENT') {
|
||||
var result = run(/^[\s\S]*?\}\}/);
|
||||
if (! result)
|
||||
error("Unclosed comment");
|
||||
tag.value = result.slice(0, -2);
|
||||
} else if (type === 'BLOCKCLOSE') {
|
||||
tag.path = scanPath();
|
||||
if (! run(ends.DOUBLE))
|
||||
expected('`}}`');
|
||||
} else if (type === 'ELSE') {
|
||||
if (! run(ends.DOUBLE))
|
||||
expected('`}}`');
|
||||
} else if (type === 'ESCAPE') {
|
||||
var result = run(/^\{*\|/);
|
||||
tag.value = '{{' + result.slice(0, -1);
|
||||
} else {
|
||||
// DOUBLE, TRIPLE, BLOCKOPEN, INCLUSION
|
||||
tag = scanExpr(type);
|
||||
}
|
||||
|
||||
return tag;
|
||||
};
|
||||
|
||||
// Returns a SpacebarsCompiler.TemplateTag parsed from `scanner`, leaving scanner
|
||||
// at its original position.
|
||||
//
|
||||
// An error will still be thrown if there is not a valid template tag at
|
||||
// the current position.
|
||||
TemplateTag.peek = function (scanner) {
|
||||
var startPos = scanner.pos;
|
||||
var result = TemplateTag.parse(scanner);
|
||||
scanner.pos = startPos;
|
||||
return result;
|
||||
};
|
||||
|
||||
// Like `TemplateTag.parse`, but in the case of blocks, parse the complete
|
||||
// `{{#foo}}...{{/foo}}` with `content` and possible `elseContent`, rather
|
||||
// than just the BLOCKOPEN tag.
|
||||
//
|
||||
// In addition:
|
||||
//
|
||||
// - Throws an error if `{{else}}` or `{{/foo}}` tag is encountered.
|
||||
//
|
||||
// - Returns `null` for a COMMENT. (This case is distinguishable from
|
||||
// parsing no tag by the fact that the scanner is advanced.)
|
||||
//
|
||||
// - Takes an HTMLTools.TEMPLATE_TAG_POSITION `position` and sets it as the
|
||||
// TemplateTag's `.position` property.
|
||||
//
|
||||
// - Validates the tag's well-formedness and legality at in its position.
|
||||
TemplateTag.parseCompleteTag = function (scannerOrString, position) {
|
||||
var scanner = scannerOrString;
|
||||
if (typeof scanner === 'string')
|
||||
scanner = new HTMLTools.Scanner(scannerOrString);
|
||||
|
||||
var startPos = scanner.pos; // for error messages
|
||||
var result = TemplateTag.parse(scannerOrString);
|
||||
if (! result)
|
||||
return result;
|
||||
|
||||
if (result.type === 'BLOCKCOMMENT')
|
||||
return null;
|
||||
|
||||
if (result.type === 'COMMENT')
|
||||
return null;
|
||||
|
||||
if (result.type === 'ELSE')
|
||||
scanner.fatal("Unexpected {{else}}");
|
||||
|
||||
if (result.type === 'BLOCKCLOSE')
|
||||
scanner.fatal("Unexpected closing template tag");
|
||||
|
||||
position = (position || TEMPLATE_TAG_POSITION.ELEMENT);
|
||||
if (position !== TEMPLATE_TAG_POSITION.ELEMENT)
|
||||
result.position = position;
|
||||
|
||||
if (result.type === 'BLOCKOPEN') {
|
||||
// parse block contents
|
||||
|
||||
// Construct a string version of `.path` for comparing start and
|
||||
// end tags. For example, `foo/[0]` was parsed into `["foo", "0"]`
|
||||
// and now becomes `foo,0`. This form may also show up in error
|
||||
// messages.
|
||||
var blockName = result.path.join(',');
|
||||
|
||||
var textMode = null;
|
||||
if (blockName === 'markdown' ||
|
||||
position === TEMPLATE_TAG_POSITION.IN_RAWTEXT) {
|
||||
textMode = HTML.TEXTMODE.STRING;
|
||||
} else if (position === TEMPLATE_TAG_POSITION.IN_RCDATA ||
|
||||
position === TEMPLATE_TAG_POSITION.IN_ATTRIBUTE) {
|
||||
textMode = HTML.TEXTMODE.RCDATA;
|
||||
}
|
||||
var parserOptions = {
|
||||
getTemplateTag: TemplateTag.parseCompleteTag,
|
||||
shouldStop: isAtBlockCloseOrElse,
|
||||
textMode: textMode
|
||||
};
|
||||
result.content = HTMLTools.parseFragment(scanner, parserOptions);
|
||||
|
||||
if (scanner.rest().slice(0, 2) !== '{{')
|
||||
scanner.fatal("Expected {{else}} or block close for " + blockName);
|
||||
|
||||
var lastPos = scanner.pos; // save for error messages
|
||||
var tmplTag = TemplateTag.parse(scanner); // {{else}} or {{/foo}}
|
||||
|
||||
if (tmplTag.type === 'ELSE') {
|
||||
// parse {{else}} and content up to close tag
|
||||
result.elseContent = HTMLTools.parseFragment(scanner, parserOptions);
|
||||
|
||||
if (scanner.rest().slice(0, 2) !== '{{')
|
||||
scanner.fatal("Expected block close for " + blockName);
|
||||
|
||||
lastPos = scanner.pos;
|
||||
tmplTag = TemplateTag.parse(scanner);
|
||||
}
|
||||
|
||||
if (tmplTag.type === 'BLOCKCLOSE') {
|
||||
var blockName2 = tmplTag.path.join(',');
|
||||
if (blockName !== blockName2) {
|
||||
scanner.pos = lastPos;
|
||||
scanner.fatal('Expected tag to close ' + blockName + ', found ' +
|
||||
blockName2);
|
||||
}
|
||||
} else {
|
||||
scanner.pos = lastPos;
|
||||
scanner.fatal('Expected tag to close ' + blockName + ', found ' +
|
||||
tmplTag.type);
|
||||
}
|
||||
}
|
||||
|
||||
var finalPos = scanner.pos;
|
||||
scanner.pos = startPos;
|
||||
validateTag(result, scanner);
|
||||
scanner.pos = finalPos;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
var isAtBlockCloseOrElse = function (scanner) {
|
||||
// Detect `{{else}}` or `{{/foo}}`.
|
||||
//
|
||||
// We do as much work ourselves before deferring to `TemplateTag.peek`,
|
||||
// for efficiency (we're called for every input token) and to be
|
||||
// less obtrusive, because `TemplateTag.peek` will throw an error if it
|
||||
// sees `{{` followed by a malformed tag.
|
||||
var rest, type;
|
||||
return (scanner.peek() === '{' &&
|
||||
(rest = scanner.rest()).slice(0, 2) === '{{' &&
|
||||
/^\{\{\s*(\/|else\b)/.test(rest) &&
|
||||
(type = TemplateTag.peek(scanner).type) &&
|
||||
(type === 'BLOCKCLOSE' || type === 'ELSE'));
|
||||
};
|
||||
|
||||
// Validate that `templateTag` is correctly formed and legal for its
|
||||
// HTML position. Use `scanner` to report errors. On success, does
|
||||
// nothing.
|
||||
var validateTag = function (ttag, scanner) {
|
||||
|
||||
if (ttag.type === 'INCLUSION' || ttag.type === 'BLOCKOPEN') {
|
||||
var args = ttag.args;
|
||||
if (ttag.path[0] === 'each' && args[1] && args[1][0] === 'PATH' &&
|
||||
args[1][1][0] === 'in') {
|
||||
// For slightly better error messages, we detect the each-in case
|
||||
// here in order not to complain if the user writes `{{#each 3 in x}}`
|
||||
// that "3 is not a function"
|
||||
} else {
|
||||
if (args.length > 1 && args[0].length === 2 && args[0][0] !== 'PATH') {
|
||||
// we have a positional argument that is not a PATH followed by
|
||||
// other arguments
|
||||
scanner.fatal("First argument must be a function, to be called on " +
|
||||
"the rest of the arguments; found " + args[0][0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var position = ttag.position || TEMPLATE_TAG_POSITION.ELEMENT;
|
||||
if (position === TEMPLATE_TAG_POSITION.IN_ATTRIBUTE) {
|
||||
if (ttag.type === 'DOUBLE' || ttag.type === 'ESCAPE') {
|
||||
return;
|
||||
} else if (ttag.type === 'BLOCKOPEN') {
|
||||
var path = ttag.path;
|
||||
var path0 = path[0];
|
||||
if (! (path.length === 1 && (path0 === 'if' ||
|
||||
path0 === 'unless' ||
|
||||
path0 === 'with' ||
|
||||
path0 === 'each'))) {
|
||||
scanner.fatal("Custom block helpers are not allowed in an HTML attribute, only built-in ones like #each and #if");
|
||||
}
|
||||
} else {
|
||||
scanner.fatal(ttag.type + " template tag is not allowed in an HTML attribute");
|
||||
}
|
||||
} else if (position === TEMPLATE_TAG_POSITION.IN_START_TAG) {
|
||||
if (! (ttag.type === 'DOUBLE')) {
|
||||
scanner.fatal("Reactive HTML attributes must either have a constant name or consist of a single {{helper}} providing a dictionary of names and values. A template tag of type " + ttag.type + " is not allowed here.");
|
||||
}
|
||||
if (scanner.peek() === '=') {
|
||||
scanner.fatal("Template tags are not allowed in attribute names, only in attribute values or in the form of a single {{helper}} that evaluates to a dictionary of name=value pairs.");
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
1
packages/spacebars-tests/.gitignore
vendored
1
packages/spacebars-tests/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.build*
|
||||
@@ -1 +0,0 @@
|
||||
This is an internal Meteor package.
|
||||
@@ -1,49 +0,0 @@
|
||||
<p><i>hi</i>
|
||||
/each}}</p>
|
||||
|
||||
<p><b><i>hi</i></b>
|
||||
<b>/each}}</b></p>
|
||||
|
||||
<ul>
|
||||
<li><i>hi</i></li>
|
||||
<li><p>/each}}</p></li>
|
||||
<li><p><b><i>hi</i></b></p></li>
|
||||
<li><b>/each}}</b></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code><i>hi</i>
|
||||
/each}}
|
||||
|
||||
<b><i>hi</i></b>
|
||||
<b>/each}}</b>
|
||||
</code></pre>
|
||||
|
||||
<p>&gt</p>
|
||||
|
||||
<ul>
|
||||
<li>&gt</li>
|
||||
</ul>
|
||||
|
||||
<p><code>&gt</code></p>
|
||||
|
||||
<pre><code>&gt
|
||||
</code></pre>
|
||||
|
||||
<p>></p>
|
||||
|
||||
<ul>
|
||||
<li>></li>
|
||||
</ul>
|
||||
|
||||
<p><code>&gt;</code></p>
|
||||
|
||||
<pre><code>&gt;
|
||||
</code></pre>
|
||||
|
||||
<p><code><i>hi</i></code>
|
||||
<code>/each}}</code></p>
|
||||
|
||||
<p><code><b><i>hi</i></b></code>
|
||||
<code><b>/each}}</code></p>
|
||||
@@ -1,15 +0,0 @@
|
||||
<p><b></b></p>
|
||||
|
||||
<ul>
|
||||
<li></li>
|
||||
<li><b></b></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code><b></b>
|
||||
</code></pre>
|
||||
|
||||
<p>``</p>
|
||||
|
||||
<p><code><b></b></code></p>
|
||||
@@ -1,19 +0,0 @@
|
||||
<p>item</p>
|
||||
|
||||
<p><b>item</b></p>
|
||||
|
||||
<ul>
|
||||
<li><p>item</p></li>
|
||||
<li><p><b>item</b></p></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code>item
|
||||
|
||||
<b>item</b>
|
||||
</code></pre>
|
||||
|
||||
<p><code>item</code></p>
|
||||
|
||||
<p><code><b>item</b></code></p>
|
||||
@@ -1,19 +0,0 @@
|
||||
<p>false</p>
|
||||
|
||||
<p><b>false</b></p>
|
||||
|
||||
<ul>
|
||||
<li><p>false</p></li>
|
||||
<li><p><b>false</b></p></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code>false
|
||||
|
||||
<b>false</b>
|
||||
</code></pre>
|
||||
|
||||
<p><code>false</code></p>
|
||||
|
||||
<p><code><b>false</b></code></p>
|
||||
@@ -1,19 +0,0 @@
|
||||
<p>true</p>
|
||||
|
||||
<p><b>true</b></p>
|
||||
|
||||
<ul>
|
||||
<li><p>true</p></li>
|
||||
<li><p><b>true</b></p></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code>true
|
||||
|
||||
<b>true</b>
|
||||
</code></pre>
|
||||
|
||||
<p><code>true</code></p>
|
||||
|
||||
<p><code><b>true</b></code></p>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
Package.describe({
|
||||
summary: "Additional tests for Spacebars",
|
||||
version: '1.0.8'
|
||||
});
|
||||
|
||||
// These tests are in a separate package to avoid a circular dependency
|
||||
// between the `spacebars` and `templating` packages.
|
||||
Package.onTest(function (api) {
|
||||
api.use([
|
||||
'es5-shim',
|
||||
'underscore',
|
||||
'spacebars',
|
||||
'tinytest',
|
||||
'jquery',
|
||||
'test-helpers',
|
||||
'reactive-var',
|
||||
'showdown',
|
||||
'minimongo',
|
||||
'tracker',
|
||||
'mongo',
|
||||
'random',
|
||||
'blaze',
|
||||
'session'
|
||||
]);
|
||||
|
||||
api.use('templating', 'client');
|
||||
api.addFiles([
|
||||
'template_tests.html',
|
||||
'template_tests.js',
|
||||
'templating_tests.html',
|
||||
'templating_tests.js',
|
||||
|
||||
'old_templates.js', // backcompat for packages built with old Blaze APIs.
|
||||
'old_templates_tests.js'
|
||||
], 'client');
|
||||
|
||||
api.addFiles('template_tests_server.js', 'server');
|
||||
|
||||
api.addAssets([
|
||||
'assets/markdown_basic.html',
|
||||
'assets/markdown_if1.html',
|
||||
'assets/markdown_if2.html',
|
||||
'assets/markdown_each1.html',
|
||||
'assets/markdown_each2.html'
|
||||
], 'server');
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
||||
var path = Npm.require("path");
|
||||
var Future = Npm.require('fibers/future');
|
||||
|
||||
Meteor.methods({
|
||||
getAsset: function (filename) {
|
||||
return Assets.getText(path.join("assets", filename));
|
||||
}
|
||||
});
|
||||
|
||||
var templateSubFutures = {};
|
||||
Meteor.publish("templateSub", function (futureId) {
|
||||
var self = this;
|
||||
Meteor.defer(function () { // because subs are blocking
|
||||
if (futureId) {
|
||||
var f = new Future();
|
||||
templateSubFutures[futureId] = f;
|
||||
f.wait();
|
||||
delete templateSubFutures[futureId];
|
||||
}
|
||||
|
||||
self.ready();
|
||||
});
|
||||
});
|
||||
Meteor.methods({
|
||||
makeTemplateSubReady: function (futureId) {
|
||||
templateSubFutures[futureId].return();
|
||||
}
|
||||
});
|
||||
@@ -1,224 +0,0 @@
|
||||
<template name="test_assembly_a0">
|
||||
{{> test_assembly_a1}}
|
||||
</template>
|
||||
|
||||
<template name="test_assembly_a1">
|
||||
{{> test_assembly_a2}}
|
||||
</template>
|
||||
|
||||
<template name="test_assembly_a2">
|
||||
{{> test_assembly_a3}}
|
||||
</template>
|
||||
|
||||
<template name="test_assembly_a3">
|
||||
Hi
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<template name="test_assembly_b0">
|
||||
{{> test_assembly_b1}}
|
||||
</template>
|
||||
|
||||
<template name="test_assembly_b1">
|
||||
x{{#if stuff}}y{{/if}}{{> test_assembly_b2}}
|
||||
</template>
|
||||
|
||||
<template name="test_assembly_b2">
|
||||
hi
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<template name="test_table_b0">
|
||||
<table>
|
||||
<tbody>
|
||||
{{> test_table_b1}}
|
||||
{{> test_table_b1}}
|
||||
{{> test_table_b1}}
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template name="test_table_b1">
|
||||
<tr>
|
||||
{{> test_table_b2}}
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template name="test_table_b2">
|
||||
<td>
|
||||
{{> test_table_b3}}
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<template name="test_table_b3">
|
||||
Foo.
|
||||
</template>
|
||||
|
||||
|
||||
<template name="test_table_each">
|
||||
<table>
|
||||
<tbody>
|
||||
{{#each foo}}
|
||||
<tr><td>{{bar}}</td></tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template name="test_event_data_with">
|
||||
<div>
|
||||
xxx
|
||||
{{#with TWO}}
|
||||
<div>
|
||||
xxx
|
||||
{{#with THREE}}
|
||||
<div>
|
||||
xxx
|
||||
</div>
|
||||
{{/with}}
|
||||
</div>
|
||||
{{/with}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="test_capture_events">
|
||||
<video class='video1'>
|
||||
<source id='mp4'
|
||||
src=""
|
||||
type='video/mp4'>
|
||||
</video>
|
||||
<video class='video2'>
|
||||
<source id='mp4'
|
||||
src=""
|
||||
type='video/mp4'>
|
||||
</video>
|
||||
<video class='video2'>
|
||||
<source id='mp4'
|
||||
src=""
|
||||
type='video/mp4'>
|
||||
</video>
|
||||
</template>
|
||||
|
||||
<template name="test_safestring_a">
|
||||
{{foo}} {{{foo}}} {{bar}} {{{bar}}}
|
||||
{{fooprop}} {{{fooprop}}} {{barprop}} {{{barprop}}}
|
||||
</template>
|
||||
|
||||
<template name="test_helpers_a">
|
||||
platypus={{platypus}}
|
||||
watermelon={{watermelon}}
|
||||
daisy={{daisy}}
|
||||
tree={{tree}}
|
||||
warthog={{warthog}}
|
||||
</template>
|
||||
|
||||
<template name="test_helpers_b">
|
||||
unknown={{unknown}}
|
||||
zero={{zero}}
|
||||
</template>
|
||||
|
||||
<template name="test_helpers_c">
|
||||
platypus.X={{platypus.X}}
|
||||
watermelon.X={{watermelon.X}}
|
||||
daisy.X={{daisy.X}}
|
||||
tree.X={{tree.X}}
|
||||
warthog.X={{warthog.X}}
|
||||
getNull.X={{getNull.X}}
|
||||
getUndefined.X={{getUndefined.X}}
|
||||
getUndefined.X.Y={{getUndefined.X.Y}}
|
||||
</template>
|
||||
|
||||
<template name="test_helpers_d">
|
||||
daisygetter={{daisygetter}}
|
||||
thisTest={{thisTest}}
|
||||
{{#with fancy}}
|
||||
../thisTest={{../thisTest}}
|
||||
{{/with}}
|
||||
{{#with "foo"}}
|
||||
../fancy.currentFruit={{../fancy.currentFruit}}
|
||||
{{/with}}
|
||||
</template>
|
||||
|
||||
<template name="test_helpers_e">
|
||||
fancy.foo={{fancy.foo}}
|
||||
fancy.apple.banana={{fancy.apple.banana}}
|
||||
fancy.currentFruit={{fancy.currentFruit}}
|
||||
fancy.currentCountry.name={{fancy.currentCountry.name}}
|
||||
fancy.currentCountry.population={{fancy.currentCountry.population}}
|
||||
fancy.currentCountry.unicorns={{fancy.currentCountry.unicorns}}
|
||||
</template>
|
||||
|
||||
<template name="test_helpers_f">
|
||||
fancyhelper.foo={{fancyhelper.foo}}
|
||||
fancyhelper.apple.banana={{fancyhelper.apple.banana}}
|
||||
fancyhelper.currentFruit={{fancyhelper.currentFruit}}
|
||||
fancyhelper.currentCountry.name={{fancyhelper.currentCountry.name}}
|
||||
fancyhelper.currentCountry.population={{fancyhelper.currentCountry.population}}
|
||||
fancyhelper.currentCountry.unicorns={{fancyhelper.currentCountry.unicorns}}
|
||||
</template>
|
||||
|
||||
<template name="test_helpers_g">
|
||||
platypus={{platypus}}
|
||||
this.platypus={{this.platypus}}
|
||||
</template>
|
||||
|
||||
<template name="test_helpers_h">
|
||||
(methodListFour 6 7 8 9={{methodListFour 6 7 8 9}})
|
||||
(methodListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns={{methodListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns}})
|
||||
(methodListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns a=platypus b=thisTest c=fancyhelper.currentFruit d=fancyhelper.currentCountry.unicorns={{methodListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns a=platypus b=thisTest c=fancyhelper.currentFruit d=fancyhelper.currentCountry.unicorns}})
|
||||
(helperListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns={{helperListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns}})
|
||||
(helperListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns a=platypus b=thisTest c=fancyhelper.currentFruit d=fancyhelper.currentCountry.unicorns={{helperListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns a=platypus b=thisTest c=fancyhelper.currentFruit d=fancyhelper.currentCountry.unicorns}})
|
||||
</template>
|
||||
|
||||
<template name="test_render_a">
|
||||
{{foo}}<br><hr>
|
||||
</template>
|
||||
|
||||
<template name="test_render_b">
|
||||
{{#with 200}}{{foo}}<br><hr>{{/with}}
|
||||
</template>
|
||||
|
||||
<template name="test_render_c">
|
||||
<br><hr>
|
||||
</template>
|
||||
|
||||
<template name="test_template_arg_a">
|
||||
<b>Foo</b> <i>Bar</i> <u>Baz</u>
|
||||
</template>
|
||||
|
||||
<template name="test_template_helpers_a">
|
||||
{{foo}}{{bar}}{{baz}}
|
||||
</template>
|
||||
|
||||
<template name="test_template_helpers_b">
|
||||
{{name}}{{arity}}{{toString}}{{length}}{{var}}
|
||||
</template>
|
||||
|
||||
<template name="test_template_helpers_c">
|
||||
{{name}}{{arity}}{{length}}{{var}}x
|
||||
</template>
|
||||
|
||||
<template name="test_template_events_a">
|
||||
<b>foo</b><u>bar</u><i>baz</i>
|
||||
</template>
|
||||
|
||||
<template name="test_template_events_b">
|
||||
<b>foo</b><u>bar</u><i>baz</i>
|
||||
</template>
|
||||
|
||||
<template name="test_template_events_c">
|
||||
<b>foo</b><u>bar</u><i>baz</i>
|
||||
</template>
|
||||
|
||||
<!-- Test for Issue 617 - Type casting for helpers -->
|
||||
<template name="test_type_casting">
|
||||
{{testTypeCasting 'true' 'false' true false 0 1 -1 10 -10}}
|
||||
</template>
|
||||
|
||||
<!-- Test for issue #801 - Each handling of falsy values eg. [1, 2, null, undefined] -->
|
||||
<template name="test_template_issue801">
|
||||
{{#each values}}{{this}}{{/each}}
|
||||
</template>
|
||||
|
||||
@@ -1,619 +0,0 @@
|
||||
|
||||
// for events to bubble an element needs to be in the DOM.
|
||||
// @return {Function} call this for cleanup
|
||||
var addToBody = function (el) {
|
||||
el.style.display = "none";
|
||||
document.body.appendChild(el);
|
||||
return function () {
|
||||
document.body.removeChild(el);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Tinytest.add("spacebars-tests - templating_tests - assembly", function (test) {
|
||||
|
||||
// Test for a bug that made it to production -- after a replacement,
|
||||
// we need to also check the newly replaced node for replacements
|
||||
var div = renderToDiv(Template.test_assembly_a0);
|
||||
test.equal(canonicalizeHtml(div.innerHTML),
|
||||
"Hi");
|
||||
|
||||
// Another production bug -- we must use LiveRange to replace the
|
||||
// placeholder, or risk breaking other LiveRanges
|
||||
Session.set("stuff", true); // XXX bad form to use Session in a test?
|
||||
Template.test_assembly_b1.helpers({
|
||||
stuff: function () {
|
||||
return Session.get("stuff");
|
||||
}
|
||||
});
|
||||
var onscreen = renderToDiv(Template.test_assembly_b0);
|
||||
test.equal(canonicalizeHtml(onscreen.innerHTML), "xyhi");
|
||||
Session.set("stuff", false);
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(onscreen.innerHTML), "xhi");
|
||||
Tracker.flush();
|
||||
});
|
||||
|
||||
// Test that if a template throws an error, then pending_partials is
|
||||
// cleaned up properly (that template rendering doesn't break..)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Tinytest.add("spacebars-tests - templating_tests - table assembly", function(test) {
|
||||
var childWithTag = function(node, tag) {
|
||||
return _.find(node.childNodes, function(n) {
|
||||
return n.nodeName === tag;
|
||||
});
|
||||
};
|
||||
|
||||
// The table.rows test would fail when TR/TD tags are stripped due
|
||||
// to improper html-to-fragment
|
||||
var table = childWithTag(renderToDiv(Template.test_table_b0), "TABLE");
|
||||
test.equal(table.rows.length, 3);
|
||||
|
||||
var c = new LocalCollection();
|
||||
c.insert({bar:'a'});
|
||||
c.insert({bar:'b'});
|
||||
c.insert({bar:'c'});
|
||||
var onscreen = renderToDiv(Template.test_table_each, {foo: c.find()});
|
||||
table = childWithTag(onscreen, "TABLE");
|
||||
|
||||
test.equal(table.rows.length, 3, table.parentNode.innerHTML);
|
||||
var tds = onscreen.getElementsByTagName("TD");
|
||||
test.equal(tds.length, 3);
|
||||
test.equal(canonicalizeHtml(tds[0].innerHTML), "a");
|
||||
test.equal(canonicalizeHtml(tds[1].innerHTML), "b");
|
||||
test.equal(canonicalizeHtml(tds[2].innerHTML), "c");
|
||||
|
||||
Tracker.flush();
|
||||
});
|
||||
|
||||
Tinytest.add("spacebars-tests - templating_tests - event handler this", function(test) {
|
||||
|
||||
Template.test_event_data_with.helpers({
|
||||
ONE: {str: "one"},
|
||||
TWO: {str: "two"},
|
||||
THREE: {str: "three"}
|
||||
});
|
||||
|
||||
Template.test_event_data_with.events({
|
||||
'click': function(event, template) {
|
||||
test.isTrue(this.str);
|
||||
test.equal(template.data.str, "one");
|
||||
event_buf.push(this.str);
|
||||
}
|
||||
});
|
||||
|
||||
var event_buf = [];
|
||||
var containerDiv = renderToDiv(Template.test_event_data_with, {str: "one"});
|
||||
var cleanupDiv = addToBody(containerDiv);
|
||||
|
||||
var divs = containerDiv.getElementsByTagName("div");
|
||||
test.equal(3, divs.length);
|
||||
|
||||
clickElement(divs[0]);
|
||||
test.equal(event_buf, ['one']);
|
||||
event_buf.length = 0;
|
||||
|
||||
clickElement(divs[1]);
|
||||
test.equal(event_buf, ['two']);
|
||||
event_buf.length = 0;
|
||||
|
||||
clickElement(divs[2]);
|
||||
test.equal(event_buf, ['three']);
|
||||
event_buf.length = 0;
|
||||
|
||||
cleanupDiv();
|
||||
Tracker.flush();
|
||||
});
|
||||
|
||||
|
||||
if (document.addEventListener) {
|
||||
// Only run this test on browsers with support for event
|
||||
// capturing. A more detailed analysis can be found at
|
||||
// https://www.meteor.com/blog/2013/09/06/browser-events-bubbling-capturing-and-delegation
|
||||
|
||||
// This is related to issue at https://gist.github.com/mquandalle/8157017
|
||||
// Tests two situations related to events that can only be captured, not bubbled:
|
||||
// 1. Event should only fire the handler that matches the selector given
|
||||
// 2. Event should work on every element in the selector and not just the first element
|
||||
// This test isn't written against mouseenter because it is synthesized by jQuery,
|
||||
// the bug also happened with the play event
|
||||
Tinytest.add("spacebars-tests - templating_tests - capturing events", function (test) {
|
||||
var video1Played = 0,
|
||||
video2Played = 0;
|
||||
|
||||
Template.test_capture_events.events({
|
||||
'play .video1': function () {
|
||||
video1Played++;
|
||||
},
|
||||
'play .video2': function () {
|
||||
video2Played++;
|
||||
}
|
||||
});
|
||||
|
||||
// add to body or else events don't actually fire
|
||||
var containerDiv = renderToDiv(Template.test_capture_events);
|
||||
var cleanupDiv = addToBody(containerDiv);
|
||||
|
||||
var checkAndResetEvents = function(video1, video2) {
|
||||
test.equal(video1Played, video1);
|
||||
test.equal(video2Played, video2);
|
||||
|
||||
video1Played = 0;
|
||||
video2Played = 0;
|
||||
};
|
||||
|
||||
simulateEvent($(containerDiv).find(".video1").get(0),
|
||||
"play", {}, {bubbles: false});
|
||||
checkAndResetEvents(1, 0);
|
||||
|
||||
simulateEvent($(containerDiv).find(".video2").get(0),
|
||||
"play", {}, {bubbles: false});
|
||||
checkAndResetEvents(0, 1);
|
||||
|
||||
simulateEvent($(containerDiv).find(".video2").get(1),
|
||||
"play", {}, {bubbles: false});
|
||||
checkAndResetEvents(0, 1);
|
||||
|
||||
// clean up DOM
|
||||
cleanupDiv();
|
||||
Tracker.flush();
|
||||
});
|
||||
}
|
||||
|
||||
Tinytest.add("spacebars-tests - templating_tests - safestring", function(test) {
|
||||
|
||||
Template.test_safestring_a.helpers({
|
||||
foo: function() {
|
||||
return "<br>";
|
||||
},
|
||||
bar: function() {
|
||||
return new Spacebars.SafeString("<hr>");
|
||||
}
|
||||
});
|
||||
|
||||
var obj = {fooprop: "<br>",
|
||||
barprop: new Spacebars.SafeString("<hr>")};
|
||||
var html = canonicalizeHtml(
|
||||
renderToDiv(Template.test_safestring_a, obj).innerHTML);
|
||||
|
||||
test.equal(html,
|
||||
"<br><br><hr><hr>"+
|
||||
"<br><br><hr><hr>");
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("spacebars-tests - templating_tests - helpers and dots", function(test) {
|
||||
Template.registerHelper("platypus", function() {
|
||||
return "eggs";
|
||||
});
|
||||
Template.registerHelper("watermelon", function() {
|
||||
return "seeds";
|
||||
});
|
||||
|
||||
Template.registerHelper("daisygetter", function() {
|
||||
return this.daisy;
|
||||
});
|
||||
|
||||
// XXX for debugging
|
||||
Template.registerHelper("debugger", function() {
|
||||
debugger;
|
||||
});
|
||||
|
||||
var getFancyObject = function() {
|
||||
return {
|
||||
foo: 'bar',
|
||||
apple: {banana: 'smoothie'},
|
||||
currentFruit: function() {
|
||||
return 'guava';
|
||||
},
|
||||
currentCountry: function() {
|
||||
return {name: 'Iceland',
|
||||
_pop: 321007,
|
||||
population: function() {
|
||||
return this._pop;
|
||||
},
|
||||
unicorns: 0, // falsy value
|
||||
daisyGetter: function() {
|
||||
return this.daisy;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Template.registerHelper("fancyhelper", getFancyObject);
|
||||
|
||||
Template.test_helpers_a.helpers({
|
||||
platypus: 'bill',
|
||||
warthog: function() {
|
||||
return 'snout';
|
||||
}
|
||||
});
|
||||
|
||||
var listFour = function(a, b, c, d, options) {
|
||||
test.isTrue(options instanceof Spacebars.kw);
|
||||
var keywordArgs = _.map(_.keys(options.hash), function(k) {
|
||||
var val = options.hash[k];
|
||||
return k+':'+val;
|
||||
});
|
||||
return [a, b, c, d].concat(keywordArgs).join(' ');
|
||||
};
|
||||
|
||||
var dataObj = {
|
||||
zero: 0,
|
||||
platypus: 'weird',
|
||||
watermelon: 'rind',
|
||||
daisy: 'petal',
|
||||
tree: function() { return 'leaf'; },
|
||||
thisTest: function() { return this.tree(); },
|
||||
getNull: function() { return null; },
|
||||
getUndefined: function () { return; },
|
||||
fancy: getFancyObject(),
|
||||
methodListFour: listFour
|
||||
};
|
||||
|
||||
var html;
|
||||
html = canonicalizeHtml(
|
||||
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
|
||||
'daisy=petal', // unshadowed object property
|
||||
'tree=leaf', // function object property
|
||||
'warthog=snout' // function Template property
|
||||
]);
|
||||
|
||||
html = canonicalizeHtml(
|
||||
renderToDiv(Template.test_helpers_b, dataObj).innerHTML);
|
||||
test.equal(html.match(/\S+/g), [
|
||||
// unknown properties silently fail
|
||||
'unknown=',
|
||||
// falsy property comes through
|
||||
'zero=0'
|
||||
]);
|
||||
|
||||
html = canonicalizeHtml(
|
||||
renderToDiv(Template.test_helpers_c, dataObj).innerHTML);
|
||||
test.equal(html.match(/\S+/g), [
|
||||
// property gets are supposed to silently fail
|
||||
'platypus.X=',
|
||||
'watermelon.X=',
|
||||
'daisy.X=',
|
||||
'tree.X=',
|
||||
'warthog.X=',
|
||||
'getNull.X=',
|
||||
'getUndefined.X=',
|
||||
'getUndefined.X.Y='
|
||||
]);
|
||||
|
||||
html = canonicalizeHtml(
|
||||
renderToDiv(Template.test_helpers_d, dataObj).innerHTML);
|
||||
test.equal(html.match(/\S+/g), [
|
||||
// helpers should get current data context in `this`
|
||||
'daisygetter=petal',
|
||||
// object methods should get object in `this`
|
||||
'thisTest=leaf',
|
||||
// nesting inside {{#with fancy}} shouldn't affect
|
||||
// method
|
||||
'../thisTest=leaf',
|
||||
// combine .. and .
|
||||
'../fancy.currentFruit=guava'
|
||||
]);
|
||||
|
||||
html = canonicalizeHtml(
|
||||
renderToDiv(Template.test_helpers_e, dataObj).innerHTML);
|
||||
test.equal(html.match(/\S+/g), [
|
||||
'fancy.foo=bar',
|
||||
'fancy.apple.banana=smoothie',
|
||||
'fancy.currentFruit=guava',
|
||||
'fancy.currentCountry.name=Iceland',
|
||||
'fancy.currentCountry.population=321007',
|
||||
'fancy.currentCountry.unicorns=0'
|
||||
]);
|
||||
|
||||
html = canonicalizeHtml(
|
||||
renderToDiv(Template.test_helpers_f, dataObj).innerHTML);
|
||||
test.equal(html.match(/\S+/g), [
|
||||
'fancyhelper.foo=bar',
|
||||
'fancyhelper.apple.banana=smoothie',
|
||||
'fancyhelper.currentFruit=guava',
|
||||
'fancyhelper.currentCountry.name=Iceland',
|
||||
'fancyhelper.currentCountry.population=321007',
|
||||
'fancyhelper.currentCountry.unicorns=0'
|
||||
]);
|
||||
|
||||
// test significance of 'this', which prevents helper from
|
||||
// shadowing property
|
||||
html = canonicalizeHtml(
|
||||
renderToDiv(Template.test_helpers_g, dataObj).innerHTML);
|
||||
test.equal(html.match(/\S+/g), [
|
||||
'platypus=eggs',
|
||||
'this.platypus=weird'
|
||||
]);
|
||||
|
||||
// test interpretation of arguments
|
||||
|
||||
Template.test_helpers_h.helpers({helperListFour: listFour});
|
||||
|
||||
html = canonicalizeHtml(
|
||||
renderToDiv(Template.test_helpers_h, dataObj).innerHTML);
|
||||
var trials =
|
||||
html.match(/\(.*?\)/g);
|
||||
test.equal(trials[0],
|
||||
'(methodListFour 6 7 8 9=6 7 8 9)');
|
||||
test.equal(trials[1],
|
||||
'(methodListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns=eggs leaf guava 0)');
|
||||
test.equal(trials[2],
|
||||
'(methodListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns a=platypus b=thisTest c=fancyhelper.currentFruit d=fancyhelper.currentCountry.unicorns=eggs leaf guava 0 a:eggs b:leaf c:guava d:0)');
|
||||
test.equal(trials[3],
|
||||
'(helperListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns=eggs leaf guava 0)');
|
||||
test.equal(trials[4],
|
||||
'(helperListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns a=platypus b=thisTest c=fancyhelper.currentFruit d=fancyhelper.currentCountry.unicorns=eggs leaf guava 0 a:eggs b:leaf c:guava d:0)');
|
||||
test.equal(trials.length, 5);
|
||||
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add("spacebars-tests - templating_tests - rendered template", function(test) {
|
||||
var R = ReactiveVar('foo');
|
||||
Template.test_render_a.helpers({
|
||||
foo: function() {
|
||||
R.get();
|
||||
return this.x + 1;
|
||||
}
|
||||
});
|
||||
|
||||
var div = renderToDiv(Template.test_render_a, {x: 123});
|
||||
test.equal($(div).text().match(/\S+/)[0], "124");
|
||||
|
||||
var br1 = div.getElementsByTagName('br')[0];
|
||||
var hr1 = div.getElementsByTagName('hr')[0];
|
||||
test.isTrue(br1);
|
||||
test.isTrue(hr1);
|
||||
|
||||
R.set('bar');
|
||||
Tracker.flush();
|
||||
var br2 = div.getElementsByTagName('br')[0];
|
||||
var hr2 = div.getElementsByTagName('hr')[0];
|
||||
test.isTrue(br2);
|
||||
test.isTrue(br1 === br2);
|
||||
test.isTrue(hr2);
|
||||
test.isTrue(hr1 === hr2);
|
||||
|
||||
Tracker.flush();
|
||||
|
||||
/////
|
||||
|
||||
R = ReactiveVar('foo');
|
||||
|
||||
Template.test_render_b.helpers({foo: function() {
|
||||
R.get();
|
||||
return (+this) + 1;
|
||||
}});
|
||||
|
||||
div = renderToDiv(Template.test_render_b, {x: 123});
|
||||
test.equal($(div).text().match(/\S+/)[0], "201");
|
||||
|
||||
var br1 = div.getElementsByTagName('br')[0];
|
||||
var hr1 = div.getElementsByTagName('hr')[0];
|
||||
test.isTrue(br1);
|
||||
test.isTrue(hr1);
|
||||
|
||||
R.set('bar');
|
||||
Tracker.flush();
|
||||
var br2 = div.getElementsByTagName('br')[0];
|
||||
var hr2 = div.getElementsByTagName('hr')[0];
|
||||
test.isTrue(br2);
|
||||
test.isTrue(br1 === br2);
|
||||
test.isTrue(hr2);
|
||||
test.isTrue(hr1 === hr2);
|
||||
|
||||
Tracker.flush();
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("spacebars-tests - templating_tests - template arg", function (test) {
|
||||
Template.test_template_arg_a.events({
|
||||
click: function (event, template) {
|
||||
template.firstNode.innerHTML = 'Hello';
|
||||
template.lastNode.innerHTML = 'World';
|
||||
template.find('i').innerHTML =
|
||||
(template.findAll('*').length)+"-element";
|
||||
template.lastNode.innerHTML += ' (the secret is '+
|
||||
template.secret+')';
|
||||
}
|
||||
});
|
||||
|
||||
Template.test_template_arg_a.created = function() {
|
||||
var self = this;
|
||||
test.isFalse(self.firstNode);
|
||||
test.isFalse(self.lastNode);
|
||||
test.throws(function () { return self.find("*"); });
|
||||
test.throws(function () { return self.findAll("*"); });
|
||||
};
|
||||
|
||||
Template.test_template_arg_a.rendered = function () {
|
||||
var template = this;
|
||||
template.firstNode.innerHTML = 'Greetings';
|
||||
template.lastNode.innerHTML = 'Line';
|
||||
template.find('i').innerHTML =
|
||||
(template.findAll('b').length)+"-bold";
|
||||
template.secret = "strawberry "+template.data.food;
|
||||
};
|
||||
|
||||
Template.test_template_arg_a.destroyed = function() {
|
||||
var self = this;
|
||||
test.isFalse(self.firstNode);
|
||||
test.isFalse(self.lastNode);
|
||||
test.throws(function () { return self.find("*"); });
|
||||
test.throws(function () { return self.findAll("*"); });
|
||||
};
|
||||
|
||||
var div = renderToDiv(Template.test_template_arg_a, {food: "pie"});
|
||||
var cleanupDiv = addToBody(div);
|
||||
Tracker.flush(); // cause `rendered` to be called
|
||||
test.equal($(div).text(), "Greetings 1-bold Line");
|
||||
clickElement(div.querySelector('i'));
|
||||
test.equal($(div).text(), "Hello 3-element World (the secret is strawberry pie)");
|
||||
|
||||
cleanupDiv();
|
||||
Tracker.flush();
|
||||
});
|
||||
|
||||
Tinytest.add("spacebars-tests - templating_tests - helpers", function (test) {
|
||||
var tmpl = Template.test_template_helpers_a;
|
||||
|
||||
tmpl._NOWARN_OLDSTYLE_HELPERS = true;
|
||||
tmpl.foo = 'z';
|
||||
tmpl.helpers({bar: 'b'});
|
||||
// helpers(...) takes precendence of assigned helper
|
||||
tmpl.helpers({foo: 'a', baz: function() { return 'c'; }});
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal($(div).text().match(/\S+/)[0], 'abc');
|
||||
Tracker.flush();
|
||||
|
||||
tmpl = Template.test_template_helpers_b;
|
||||
|
||||
tmpl.helpers({
|
||||
'name': 'A',
|
||||
'arity': 'B',
|
||||
'toString': 'C',
|
||||
'length': 4,
|
||||
'var': 'D'
|
||||
});
|
||||
|
||||
div = renderToDiv(tmpl);
|
||||
var txt = $(div).text();
|
||||
txt = txt.replace('[object Object]', 'X'); // IE 8
|
||||
txt = txt.match(/\S+/)[0];
|
||||
test.isTrue(txt.match(/^AB[CX]4D$/));
|
||||
// We don't make helpers with names like toString work in IE 8.
|
||||
test.expect_fail();
|
||||
test.equal(txt, 'ABC4D');
|
||||
Tracker.flush();
|
||||
|
||||
// test that helpers don't "leak"
|
||||
tmpl = Template.test_template_helpers_c;
|
||||
div = renderToDiv(tmpl);
|
||||
test.equal($(div).text(), 'x');
|
||||
Tracker.flush();
|
||||
});
|
||||
|
||||
Tinytest.add("spacebars-tests - templating_tests - events", function (test) {
|
||||
var tmpl = Template.test_template_events_a;
|
||||
|
||||
var buf = [];
|
||||
|
||||
// old style
|
||||
tmpl.events = {
|
||||
'click b': function () { buf.push('b'); }
|
||||
};
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
var cleanupDiv = addToBody(div);
|
||||
clickElement($(div).find('b')[0]);
|
||||
test.equal(buf, ['b']);
|
||||
cleanupDiv();
|
||||
Tracker.flush();
|
||||
|
||||
///
|
||||
|
||||
tmpl = Template.test_template_events_b;
|
||||
buf = [];
|
||||
// new style
|
||||
tmpl.events({
|
||||
'click u': function () { buf.push('u'); }
|
||||
});
|
||||
tmpl.events({
|
||||
'click i': function () { buf.push('i'); }
|
||||
});
|
||||
|
||||
div = renderToDiv(tmpl);
|
||||
cleanupDiv = addToBody(div);
|
||||
clickElement($(div).find('u')[0]);
|
||||
clickElement($(div).find('i')[0]);
|
||||
test.equal(buf, ['u', 'i']);
|
||||
cleanupDiv();
|
||||
Tracker.flush();
|
||||
|
||||
//Test for identical callbacks for issue #650
|
||||
tmpl = Template.test_template_events_c;
|
||||
buf = [];
|
||||
tmpl.events({
|
||||
'click u': function () { buf.push('a'); }
|
||||
});
|
||||
tmpl.events({
|
||||
'click u': function () { buf.push('b'); }
|
||||
});
|
||||
|
||||
div = renderToDiv(tmpl);
|
||||
cleanupDiv = addToBody(div);
|
||||
clickElement($(div).find('u')[0]);
|
||||
test.equal(buf.length, 2);
|
||||
test.isTrue(_.contains(buf, 'a'));
|
||||
test.isTrue(_.contains(buf, 'b'));
|
||||
cleanupDiv();
|
||||
Tracker.flush();
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add('spacebars-tests - templating_tests - helper typecast Issue #617', function (test) {
|
||||
|
||||
Template.registerHelper('testTypeCasting', function (/*arguments*/) {
|
||||
// Return a string representing the arguments passed to this
|
||||
// function, including types. eg:
|
||||
// (1, true) -> "[number,1][boolean,true]"
|
||||
return _.reduce(_.toArray(arguments), function (memo, arg) {
|
||||
if (typeof arg === 'object')
|
||||
return memo + "[object]";
|
||||
return memo + "[" + typeof arg + "," + arg + "]";
|
||||
}, "");
|
||||
return x;
|
||||
});
|
||||
|
||||
var div = renderToDiv(Template.test_type_casting);
|
||||
var result = canonicalizeHtml(div.innerHTML);
|
||||
test.equal(
|
||||
result,
|
||||
// This corresponds to entries in templating_tests.html.
|
||||
// true/faslse
|
||||
"[string,true][string,false][boolean,true][boolean,false]" +
|
||||
// numbers
|
||||
"[number,0][number,1][number,-1][number,10][number,-10]" +
|
||||
// handlebars 'options' argument. appended to args of all helpers.
|
||||
"[object]");
|
||||
});
|
||||
|
||||
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.helpers({
|
||||
values: function() { return [0,1,2,null,undefined,false]; }});
|
||||
var div = renderToDiv(Template.test_template_issue801);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "012");
|
||||
});
|
||||
|
||||
Tinytest.add('spacebars-tests - templating_tests - duplicate template error', function (test) {
|
||||
Template.__checkName("test_duplicate_template");
|
||||
Template.test_duplicate_template = new Template(
|
||||
"dup", function () { return null; });
|
||||
|
||||
test.throws(function () {
|
||||
Template.__checkName("test_duplicate_template");
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add('spacebars-tests - templating_tests - reserved template name error', function (test) {
|
||||
|
||||
_.each('length __proto__ prototype name body currentData instance'.split(' '),
|
||||
function (name) {
|
||||
test.throws(function () {
|
||||
Template.__checkName(name);
|
||||
}, /This template name is reserved: /);
|
||||
});
|
||||
});
|
||||
1
packages/spacebars/.gitignore
vendored
1
packages/spacebars/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.build*
|
||||
@@ -1,9 +0,0 @@
|
||||
# Spacebars
|
||||
|
||||
Spacebars is a Meteor template language inspired by
|
||||
[Handlebars](http://handlebarsjs.com/). It shares some of the spirit and syntax
|
||||
of Handlebars, but it has been tailored to produce reactive Meteor templates
|
||||
when compiled.
|
||||
|
||||
|
||||
Read more at http://docs.meteor.com/packages/spacebars.html
|
||||
@@ -1,23 +0,0 @@
|
||||
Package.describe({
|
||||
summary: "Handlebars-like template language for Meteor",
|
||||
version: '1.0.12'
|
||||
});
|
||||
|
||||
// For more, see package `spacebars-compiler`, which is used by
|
||||
// the build plugin and not shipped to the client unless you
|
||||
// ask for it by name.
|
||||
//
|
||||
// The Spacebars build plugin is in package `templating`.
|
||||
//
|
||||
// Additional tests are in `spacebars-tests`.
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.export('Spacebars');
|
||||
|
||||
api.use('htmljs');
|
||||
api.use('tracker');
|
||||
api.use('blaze');
|
||||
api.use('observe-sequence');
|
||||
api.use('underscore');
|
||||
api.addFiles(['spacebars-runtime.js']);
|
||||
});
|
||||
@@ -1,283 +0,0 @@
|
||||
Spacebars = {};
|
||||
|
||||
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);
|
||||
var view = templateOrFunction.constructView(contentFunc, elseFunc);
|
||||
view.__startsNewLexicalScope = true;
|
||||
return view;
|
||||
}
|
||||
|
||||
var templateVar = Blaze.ReactiveVar(null, tripleEquals);
|
||||
var view = Blaze.View('Spacebars.include', function () {
|
||||
var template = templateVar.get();
|
||||
if (template === null)
|
||||
return null;
|
||||
|
||||
if (! Blaze.isTemplate(template))
|
||||
throw new Error("Expected template or null, found: " + template);
|
||||
|
||||
return template.constructView(contentFunc, elseFunc);
|
||||
});
|
||||
view.__templateVar = templateVar;
|
||||
view.onViewCreated(function () {
|
||||
this.autorun(function () {
|
||||
templateVar.set(templateOrFunction());
|
||||
});
|
||||
});
|
||||
view.__startsNewLexicalScope = true;
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
// Executes `{{foo bar baz}}` when called on `(foo, bar, baz)`.
|
||||
// If `bar` and `baz` are functions, they are called before
|
||||
// `foo` is called on them.
|
||||
//
|
||||
// This is the shared part of Spacebars.mustache and
|
||||
// Spacebars.attrMustache, which differ in how they post-process the
|
||||
// result.
|
||||
Spacebars.mustacheImpl = function (value/*, args*/) {
|
||||
var args = arguments;
|
||||
// if we have any arguments (pos or kw), add an options argument
|
||||
// if there isn't one.
|
||||
if (args.length > 1) {
|
||||
var kw = args[args.length - 1];
|
||||
if (! (kw instanceof Spacebars.kw)) {
|
||||
kw = Spacebars.kw();
|
||||
// clone arguments into an actual array, then push
|
||||
// the empty kw object.
|
||||
args = Array.prototype.slice.call(arguments);
|
||||
args.push(kw);
|
||||
} else {
|
||||
// For each keyword arg, call it if it's a function
|
||||
var newHash = {};
|
||||
for (var k in kw.hash) {
|
||||
var v = kw.hash[k];
|
||||
newHash[k] = (typeof v === 'function' ? v() : v);
|
||||
}
|
||||
args[args.length - 1] = Spacebars.kw(newHash);
|
||||
}
|
||||
}
|
||||
|
||||
return Spacebars.call.apply(null, args);
|
||||
};
|
||||
|
||||
Spacebars.mustache = function (value/*, args*/) {
|
||||
var result = Spacebars.mustacheImpl.apply(null, arguments);
|
||||
|
||||
if (result instanceof Spacebars.SafeString)
|
||||
return HTML.Raw(result.toString());
|
||||
else
|
||||
// map `null`, `undefined`, and `false` to null, which is important
|
||||
// so that attributes with nully values are considered absent.
|
||||
// stringify anything else (e.g. strings, booleans, numbers including 0).
|
||||
return (result == null || result === false) ? null : String(result);
|
||||
};
|
||||
|
||||
Spacebars.attrMustache = function (value/*, args*/) {
|
||||
var result = Spacebars.mustacheImpl.apply(null, arguments);
|
||||
|
||||
if (result == null || result === '') {
|
||||
return null;
|
||||
} else if (typeof result === 'object') {
|
||||
return result;
|
||||
} else if (typeof result === 'string' && HTML.isValidAttributeName(result)) {
|
||||
var obj = {};
|
||||
obj[result] = '';
|
||||
return obj;
|
||||
} else {
|
||||
throw new Error("Expected valid attribute name, '', null, or object");
|
||||
}
|
||||
};
|
||||
|
||||
Spacebars.dataMustache = function (value/*, args*/) {
|
||||
var result = Spacebars.mustacheImpl.apply(null, arguments);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Idempotently wrap in `HTML.Raw`.
|
||||
//
|
||||
// Called on the return value from `Spacebars.mustache` in case the
|
||||
// template uses triple-stache (`{{{foo bar baz}}}`).
|
||||
Spacebars.makeRaw = function (value) {
|
||||
if (value == null) // null or undefined
|
||||
return null;
|
||||
else if (value instanceof HTML.Raw)
|
||||
return value;
|
||||
else
|
||||
return HTML.Raw(value);
|
||||
};
|
||||
|
||||
// If `value` is a function, evaluate its `args` (by calling them, if they
|
||||
// are functions), and then call it on them. Otherwise, return `value`.
|
||||
//
|
||||
// If `value` is not a function and is not null, then this method will assert
|
||||
// that there are no args. We check for null before asserting because a user
|
||||
// may write a template like {{user.fullNameWithPrefix 'Mr.'}}, where the
|
||||
// function will be null until data is ready.
|
||||
Spacebars.call = function (value/*, args*/) {
|
||||
if (typeof value === 'function') {
|
||||
// Evaluate arguments by calling them if they are functions.
|
||||
var newArgs = [];
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var arg = arguments[i];
|
||||
newArgs[i-1] = (typeof arg === 'function' ? arg() : arg);
|
||||
}
|
||||
|
||||
return value.apply(null, newArgs);
|
||||
} else {
|
||||
if (value != null && arguments.length > 1) {
|
||||
throw new Error("Can't call non-function: " + value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
// Call this as `Spacebars.kw({ ... })`. The return value
|
||||
// is `instanceof Spacebars.kw`.
|
||||
Spacebars.kw = function (hash) {
|
||||
if (! (this instanceof Spacebars.kw))
|
||||
// called without new; call with new
|
||||
return new Spacebars.kw(hash);
|
||||
|
||||
this.hash = hash || {};
|
||||
};
|
||||
|
||||
// Call this as `Spacebars.SafeString("some HTML")`. The return value
|
||||
// is `instanceof Spacebars.SafeString` (and `instanceof Handlebars.SafeString).
|
||||
Spacebars.SafeString = function (html) {
|
||||
if (! (this instanceof Spacebars.SafeString))
|
||||
// called without new; call with new
|
||||
return new Spacebars.SafeString(html);
|
||||
|
||||
return new Handlebars.SafeString(html);
|
||||
};
|
||||
Spacebars.SafeString.prototype = Handlebars.SafeString.prototype;
|
||||
|
||||
// `Spacebars.dot(foo, "bar", "baz")` performs a special kind
|
||||
// of `foo.bar.baz` that allows safe indexing of `null` and
|
||||
// indexing of functions (which calls the function). If the
|
||||
// result is a function, it is always a bound function (e.g.
|
||||
// a wrapped version of `baz` that always uses `foo.bar` as
|
||||
// `this`).
|
||||
//
|
||||
// In `Spacebars.dot(foo, "bar")`, `foo` is assumed to be either
|
||||
// a non-function value or a "fully-bound" function wrapping a value,
|
||||
// where fully-bound means it takes no arguments and ignores `this`.
|
||||
//
|
||||
// `Spacebars.dot(foo, "bar")` performs the following steps:
|
||||
//
|
||||
// * If `foo` is falsy, return `foo`.
|
||||
//
|
||||
// * If `foo` is a function, call it (set `foo` to `foo()`).
|
||||
//
|
||||
// * If `foo` is falsy now, return `foo`.
|
||||
//
|
||||
// * Return `foo.bar`, binding it to `foo` if it's a function.
|
||||
Spacebars.dot = function (value, id1/*, id2, ...*/) {
|
||||
if (arguments.length > 2) {
|
||||
// Note: doing this recursively is probably less efficient than
|
||||
// doing it in an iterative loop.
|
||||
var argsForRecurse = [];
|
||||
argsForRecurse.push(Spacebars.dot(value, id1));
|
||||
argsForRecurse.push.apply(argsForRecurse,
|
||||
Array.prototype.slice.call(arguments, 2));
|
||||
return Spacebars.dot.apply(null, argsForRecurse);
|
||||
}
|
||||
|
||||
if (typeof value === 'function')
|
||||
value = value();
|
||||
|
||||
if (! value)
|
||||
return value; // falsy, don't index, pass through
|
||||
|
||||
var result = value[id1];
|
||||
if (typeof result !== 'function')
|
||||
return result;
|
||||
// `value[id1]` (or `value()[id1]`) is a function.
|
||||
// Bind it so that when called, `value` will be placed in `this`.
|
||||
return function (/*arguments*/) {
|
||||
return result.apply(value, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
// 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.onViewCreated(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.
|
||||
|
||||
Tracker.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 Tracker.onInvalidate built into materializeView.
|
||||
// (When a View's render method is invalidated, it immediately
|
||||
// tears down all the subviews, via a Tracker.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;
|
||||
};
|
||||
|
||||
// XXX COMPAT WITH 0.9.0
|
||||
Spacebars.TemplateWith = Blaze._TemplateWith;
|
||||
@@ -1,10 +0,0 @@
|
||||
# static-html
|
||||
|
||||
Essentially, an alternative to the `templating` package that doesn't compile Blaze templates. Mostly useful if you want to use Angular or React as your view layer and just want to get some static HTML content on your page as a render target for your view framework.
|
||||
|
||||
This build plugin parses all of the `.html` files in your app and looks for top-level tags:
|
||||
|
||||
- `<head>` - appended to the `head` section of your HTML
|
||||
- `<body>` - appended to the `body` section of your HTML
|
||||
|
||||
Attributes are supported on the `<body>` tag, but not on `<head>`.
|
||||
@@ -1,29 +0,0 @@
|
||||
Package.describe({
|
||||
version: '1.1.12',
|
||||
// Brief, one-line summary of the package.
|
||||
summary: 'Define static page content in .html files',
|
||||
git: 'https://github.com/meteor/meteor',
|
||||
// By default, Meteor will default to using README.md for documentation.
|
||||
// To avoid submitting documentation, set this field to null.
|
||||
documentation: 'README.md'
|
||||
});
|
||||
|
||||
Package.registerBuildPlugin({
|
||||
name: "compileStaticHtmlBatch",
|
||||
use: [
|
||||
'caching-html-compiler',
|
||||
'ecmascript',
|
||||
'templating-tools',
|
||||
'underscore'
|
||||
],
|
||||
sources: [
|
||||
'static-html.js'
|
||||
]
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.use('isobuild:compiler-plugin@1.0.0');
|
||||
|
||||
// Body attributes are compiled to code that uses Meteor.startup
|
||||
api.imply('meteor', 'client');
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
Plugin.registerCompiler({
|
||||
extensions: ['html'],
|
||||
archMatching: 'web',
|
||||
isTemplate: true
|
||||
}, () => new CachingHtmlCompiler("static-html", TemplatingTools.scanHtmlForTags, compileTagsToStaticHtml));
|
||||
|
||||
// Same API as TutorialTools.compileTagsWithSpacebars, but instead of compiling
|
||||
// with Spacebars, it just returns static HTML
|
||||
function compileTagsToStaticHtml(tags) {
|
||||
var handler = new StaticHtmlTagHandler();
|
||||
|
||||
tags.forEach((tag) => {
|
||||
handler.addTagToResults(tag);
|
||||
});
|
||||
|
||||
return handler.getResults();
|
||||
};
|
||||
|
||||
class StaticHtmlTagHandler {
|
||||
constructor() {
|
||||
this.results = {
|
||||
head: '',
|
||||
body: '',
|
||||
js: '',
|
||||
bodyAttrs: {}
|
||||
};
|
||||
}
|
||||
|
||||
getResults() {
|
||||
return this.results;
|
||||
}
|
||||
|
||||
addTagToResults(tag) {
|
||||
this.tag = tag;
|
||||
|
||||
// do we have 1 or more attributes?
|
||||
const hasAttribs = ! _.isEmpty(this.tag.attribs);
|
||||
|
||||
if (this.tag.tagName === "head") {
|
||||
if (hasAttribs) {
|
||||
this.throwCompileError("Attributes on <head> not supported");
|
||||
}
|
||||
|
||||
this.results.head += this.tag.contents;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// <body> or <template>
|
||||
|
||||
try {
|
||||
if (this.tag.tagName === "body") {
|
||||
this.addBodyAttrs(this.tag.attribs);
|
||||
|
||||
// We may be one of many `<body>` tags.
|
||||
this.results.body += this.tag.contents;
|
||||
} else {
|
||||
this.throwCompileError("Expected <head> or <body> tag", this.tag.tagStartIndex);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.scanner) {
|
||||
// The error came from Spacebars
|
||||
this.throwCompileError(e.message, this.tag.contentsStartIndex + e.offset);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addBodyAttrs(attrs) {
|
||||
Object.keys(attrs).forEach((attr) => {
|
||||
const val = attrs[attr];
|
||||
|
||||
// This check is for conflicting body attributes in the same file;
|
||||
// we check across multiple files in caching-html-compiler using the
|
||||
// attributes on results.bodyAttrs
|
||||
if (this.results.bodyAttrs.hasOwnProperty(attr) && this.results.bodyAttrs[attr] !== val) {
|
||||
this.throwCompileError(
|
||||
`<body> declarations have conflicting values for the '${attr}' attribute.`);
|
||||
}
|
||||
|
||||
this.results.bodyAttrs[attr] = val;
|
||||
});
|
||||
}
|
||||
|
||||
throwCompileError(message, overrideIndex) {
|
||||
TemplatingTools.throwCompileError(this.tag, message, overrideIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
# templating-tools
|
||||
|
||||
Has some conveniently abstracted functions that are used together with the `caching-html-compiler` package to implement different template compilers:
|
||||
|
||||
1. `templating`
|
||||
2. `static-html`
|
||||
|
||||
These functions contain some code shared between the above build plugins, and if you are building your own build plugin they can be useful too. But they aren't guaranteed to be helpful for every use case, so you should carefully decide if they are appropriate for your package.
|
||||
|
||||
---------
|
||||
|
||||
### TemplatingTools.scanHtmlForTags(options)
|
||||
|
||||
Scan an HTML file for top-level tags as specified by `options.tagNames`, and return an array of `Tag` objects. See more about `Tag` objects below.
|
||||
|
||||
#### Options
|
||||
|
||||
1. `sourceName` the name of the input file, used when throwing errors.
|
||||
2. `contents` the contents of the input file, these are parsed to find the top-level tags
|
||||
3. `tagNames` the top-level tags to look for in the HTML.
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const tags = scanHtmlForTags({
|
||||
sourceName: inputPath,
|
||||
contents: contents,
|
||||
tagNames: ["body", "head", "template"]
|
||||
});
|
||||
```
|
||||
|
||||
### TemplatingTools.compileTagsWithSpacebars(tags)
|
||||
|
||||
Transform an array of tags into a result object of the following form:
|
||||
|
||||
```js
|
||||
{
|
||||
js: String,
|
||||
body: "",
|
||||
head: String,
|
||||
bodyAttrs: {
|
||||
[attrName]: String
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. The contents of every `<template>` and `<body>` tag will be compiled into JavaScript with `spacebars-compiler`, and the code appended to the `js` field of the result.
|
||||
2. The contents of every `<head>` tag will be concatenated into the `head` field of the result.
|
||||
3. Any attributes found on `<body>` tags will be added to the `bodyAtts` field of the result.
|
||||
4. Every `<template>` tag is required to have a `name` attribute, and no other attributes.
|
||||
5. The `<head>` tag is not allowed to have any attributes.
|
||||
|
||||
### TemplatingTools.CompileError
|
||||
|
||||
This error is thrown when a compilation error happens. If you catch it, look for the following fields, which are set by `TemplatingTools.throwCompileError`:
|
||||
|
||||
1. `message` The error message to show to the user.
|
||||
2. `file` The filename where the error occured.
|
||||
3. `line` The line number where the error occured.
|
||||
|
||||
### TemplatingTools.throwCompileError(tag, message, [overrideIndex])
|
||||
|
||||
Throw a `TemplatingTools.CompileError` with the right properties. Handles generating the line number of the error for you.
|
||||
|
||||
#### Arguments
|
||||
|
||||
1. `tag` the Tag object in which this compile error occured. The fields on this object are used to populate fields on the resulting error.
|
||||
2. `message` the error message, will be displayed to the user.
|
||||
3. `overrideIndex` optional - if provided will be used to determine the line number of the error; otherwise the index of the start of the tag will be used.
|
||||
|
||||
### Tag object
|
||||
|
||||
The `scanHtml` and `compileTagsWithSpacebars` functions communicate via an array of Tag objects, which have the following form:
|
||||
|
||||
```js
|
||||
{
|
||||
// Name of the tag - "body", "head", "template", etc
|
||||
tagName: String,
|
||||
|
||||
// Attributes on the tag
|
||||
attribs: { [attrName]: String },
|
||||
|
||||
// Contents of the tag
|
||||
contents: String,
|
||||
|
||||
// Starting index of the opening tag in the source file
|
||||
// (used to throw informative errors)
|
||||
tagStartIndex: Number,
|
||||
|
||||
// Starting index of the contents of the tag in the source file
|
||||
// (used to throw informative errors)
|
||||
contentsStartIndex: Number,
|
||||
|
||||
// The contents of the entire source file, should be used only to
|
||||
// throw informative errors (for example, this can be used to
|
||||
// determine the line number for an error)
|
||||
fileContents: String,
|
||||
|
||||
// The file name of the initial source file, used to throw errors
|
||||
sourceName: String
|
||||
};
|
||||
```
|
||||
@@ -1,18 +0,0 @@
|
||||
TemplatingTools.generateTemplateJS =
|
||||
function generateTemplateJS(name, renderFuncCode) {
|
||||
const nameLiteral = JSON.stringify(name);
|
||||
const templateDotNameLiteral = JSON.stringify(`Template.${name}`);
|
||||
|
||||
return `
|
||||
Template.__checkName(${nameLiteral});
|
||||
Template[${nameLiteral}] = new Template(${templateDotNameLiteral}, ${renderFuncCode});
|
||||
`;
|
||||
}
|
||||
|
||||
TemplatingTools.generateBodyJS =
|
||||
function generateBodyJS(renderFuncCode) {
|
||||
return `
|
||||
Template.body.addContent(${renderFuncCode});
|
||||
Meteor.startup(Template.body.renderToDocument);
|
||||
`;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
TemplatingTools.compileTagsWithSpacebars = function compileTagsWithSpacebars(tags) {
|
||||
var handler = new SpacebarsTagCompiler();
|
||||
|
||||
tags.forEach((tag) => {
|
||||
handler.addTagToResults(tag);
|
||||
});
|
||||
|
||||
return handler.getResults();
|
||||
};
|
||||
|
||||
class SpacebarsTagCompiler {
|
||||
constructor() {
|
||||
this.results = {
|
||||
head: '',
|
||||
body: '',
|
||||
js: '',
|
||||
bodyAttrs: {}
|
||||
};
|
||||
}
|
||||
|
||||
getResults() {
|
||||
return this.results;
|
||||
}
|
||||
|
||||
addTagToResults(tag) {
|
||||
this.tag = tag;
|
||||
|
||||
// do we have 1 or more attributes?
|
||||
const hasAttribs = ! _.isEmpty(this.tag.attribs);
|
||||
|
||||
if (this.tag.tagName === "head") {
|
||||
if (hasAttribs) {
|
||||
this.throwCompileError("Attributes on <head> not supported");
|
||||
}
|
||||
|
||||
this.results.head += this.tag.contents;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// <body> or <template>
|
||||
|
||||
try {
|
||||
if (this.tag.tagName === "template") {
|
||||
const name = this.tag.attribs.name;
|
||||
|
||||
if (! name) {
|
||||
this.throwCompileError("Template has no 'name' attribute");
|
||||
}
|
||||
|
||||
if (SpacebarsCompiler.isReservedName(name)) {
|
||||
this.throwCompileError(`Template can't be named "${name}"`);
|
||||
}
|
||||
|
||||
const renderFuncCode = SpacebarsCompiler.compile(this.tag.contents, {
|
||||
isTemplate: true,
|
||||
sourceName: `Template "${name}"`
|
||||
});
|
||||
|
||||
this.results.js += TemplatingTools.generateTemplateJS(
|
||||
name, renderFuncCode);
|
||||
} else if (this.tag.tagName === "body") {
|
||||
this.addBodyAttrs(this.tag.attribs);
|
||||
|
||||
const renderFuncCode = SpacebarsCompiler.compile(this.tag.contents, {
|
||||
isBody: true,
|
||||
sourceName: "<body>"
|
||||
});
|
||||
|
||||
// We may be one of many `<body>` tags.
|
||||
this.results.js += TemplatingTools.generateBodyJS(renderFuncCode);
|
||||
} else {
|
||||
this.throwCompileError("Expected <template>, <head>, or <body> tag in template file", tagStartIndex);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.scanner) {
|
||||
// The error came from Spacebars
|
||||
this.throwCompileError(e.message, this.tag.contentsStartIndex + e.offset);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addBodyAttrs(attrs) {
|
||||
Object.keys(attrs).forEach((attr) => {
|
||||
const val = attrs[attr];
|
||||
|
||||
// This check is for conflicting body attributes in the same file;
|
||||
// we check across multiple files in caching-html-compiler using the
|
||||
// attributes on results.bodyAttrs
|
||||
if (this.results.bodyAttrs.hasOwnProperty(attr) && this.results.bodyAttrs[attr] !== val) {
|
||||
this.throwCompileError(
|
||||
`<body> declarations have conflicting values for the '${attr}' attribute.`);
|
||||
}
|
||||
|
||||
this.results.bodyAttrs[attr] = val;
|
||||
});
|
||||
}
|
||||
|
||||
throwCompileError(message, overrideIndex) {
|
||||
TemplatingTools.throwCompileError(this.tag, message, overrideIndex);
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
Tinytest.add("templating-tools - html scanner", function (test) {
|
||||
var testInString = function(actualStr, wantedContents) {
|
||||
if (actualStr.indexOf(wantedContents) >= 0)
|
||||
test.ok();
|
||||
else
|
||||
test.fail("Expected "+JSON.stringify(wantedContents)+
|
||||
" in "+JSON.stringify(actualStr));
|
||||
};
|
||||
|
||||
var checkError = function(f, msgText, lineNum) {
|
||||
try {
|
||||
f();
|
||||
} catch (e) {
|
||||
if (! e instanceof TemplatingTools.CompileError) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (e.line === lineNum)
|
||||
test.ok();
|
||||
else
|
||||
test.fail("Error should have been on line " + lineNum + ", not " +
|
||||
e.line);
|
||||
testInString(e.message, msgText);
|
||||
return;
|
||||
}
|
||||
test.fail("Parse error didn't throw exception");
|
||||
};
|
||||
|
||||
// returns the appropriate code to put content in the body,
|
||||
// where content is something simple like the string "Hello"
|
||||
// (passed in as a source string including the quotes).
|
||||
var simpleBody = function (content) {
|
||||
return "\nTemplate.body.addContent((function() {\n var view = this;\n return " + content + ";\n}));\nMeteor.startup(Template.body.renderToDocument);\n";
|
||||
};
|
||||
|
||||
// arguments are quoted strings like '"hello"'
|
||||
var simpleTemplate = function (templateName, content) {
|
||||
// '"hello"' into '"Template.hello"'
|
||||
var viewName = templateName.slice(0, 1) + 'Template.' + templateName.slice(1);
|
||||
|
||||
return '\nTemplate.__checkName(' + templateName + ');\nTemplate[' + templateName +
|
||||
'] = new Template(' + viewName +
|
||||
', (function() {\n var view = this;\n return ' + content + ';\n}));\n';
|
||||
};
|
||||
|
||||
var checkResults = function(results, expectJs, expectHead, expectBodyAttrs) {
|
||||
test.equal(results.body, '');
|
||||
test.equal(results.js, expectJs || '');
|
||||
test.equal(results.head, expectHead || '');
|
||||
test.equal(results.bodyAttrs, expectBodyAttrs || {});
|
||||
};
|
||||
|
||||
function scanForTest(contents) {
|
||||
const tags = TemplatingTools.scanHtmlForTags({
|
||||
sourceName: "",
|
||||
contents: contents,
|
||||
tagNames: ["body", "head", "template"]
|
||||
});
|
||||
|
||||
return TemplatingTools.compileTagsWithSpacebars(tags);
|
||||
}
|
||||
|
||||
checkError(function() {
|
||||
return scanForTest("asdf");
|
||||
}, "Expected one of: <body>, <head>, <template>", 1);
|
||||
|
||||
// body all on one line
|
||||
checkResults(
|
||||
scanForTest("<body>Hello</body>"),
|
||||
simpleBody('"Hello"'));
|
||||
|
||||
// multi-line body, contents trimmed
|
||||
checkResults(
|
||||
scanForTest("\n\n\n<body>\n\nHello\n\n</body>\n\n\n"),
|
||||
simpleBody('"Hello"'));
|
||||
|
||||
// same as previous, but with various HTML comments
|
||||
checkResults(
|
||||
scanForTest("\n<!--\n\nfoo\n-->\n<!-- -->\n"+
|
||||
"<body>\n\nHello\n\n</body>\n\n<!----\n>\n\n"),
|
||||
simpleBody('"Hello"'));
|
||||
|
||||
// head and body
|
||||
checkResults(
|
||||
scanForTest("<head>\n<title>Hello</title>\n</head>\n\n<body>World</body>\n\n"),
|
||||
simpleBody('"World"'),
|
||||
"<title>Hello</title>");
|
||||
|
||||
// head and body with tag whitespace
|
||||
checkResults(
|
||||
scanForTest("<head\n>\n<title>Hello</title>\n</head >\n\n<body>World</body\n\n>\n\n"),
|
||||
simpleBody('"World"'),
|
||||
"<title>Hello</title>");
|
||||
|
||||
// head, body, and template
|
||||
checkResults(
|
||||
scanForTest("<head>\n<title>Hello</title>\n</head>\n\n<body>World</body>\n\n"+
|
||||
'<template name="favoritefood">\n pizza\n</template>\n'),
|
||||
simpleBody('"World"') + simpleTemplate('"favoritefood"', '"pizza"'),
|
||||
"<title>Hello</title>");
|
||||
|
||||
// one-line template
|
||||
checkResults(
|
||||
scanForTest('<template name="favoritefood">pizza</template>'),
|
||||
simpleTemplate('"favoritefood"', '"pizza"'));
|
||||
|
||||
// template with other attributes
|
||||
checkResults(
|
||||
scanForTest('<template foo="bar" name="favoritefood" baz="qux">'+
|
||||
'pizza</template>'),
|
||||
simpleTemplate('"favoritefood"', '"pizza"'));
|
||||
|
||||
// whitespace around '=' in attributes and at end of tag
|
||||
checkResults(
|
||||
scanForTest('<template foo = "bar" name ="favoritefood" baz= "qux" >'+
|
||||
'pizza</template\n\n>'),
|
||||
simpleTemplate('"favoritefood"', '"pizza"'));
|
||||
|
||||
// whitespace around template name
|
||||
checkResults(
|
||||
scanForTest('<template name=" favoritefood ">pizza</template>'),
|
||||
simpleTemplate('"favoritefood"', '"pizza"'));
|
||||
|
||||
// single quotes around template name
|
||||
checkResults(
|
||||
scanForTest('<template name=\'the "cool" template\'>'+
|
||||
'pizza</template>'),
|
||||
simpleTemplate('"the \\"cool\\" template"', '"pizza"'));
|
||||
|
||||
checkResults(scanForTest('<body foo="bar">\n Hello\n</body>'), simpleBody('"Hello"'), "", {foo: "bar"});
|
||||
|
||||
// error cases; exact line numbers are not critical, these just reflect
|
||||
// the current implementation
|
||||
|
||||
// unclosed body (error mentions body)
|
||||
checkError(function() {
|
||||
return scanForTest("\n\n<body>\n Hello\n</body");
|
||||
}, "body", 3);
|
||||
|
||||
// bad open tag
|
||||
checkError(function() {
|
||||
return scanForTest("\n\n\n<bodyd>\n Hello\n</body>");
|
||||
}, "Expected one of: <body>, <head>, <template>", 4);
|
||||
checkError(function() {
|
||||
return scanForTest("\n\n\n\n<body foo=>\n Hello\n</body>");
|
||||
}, "error in tag", 5);
|
||||
|
||||
// unclosed tag
|
||||
checkError(function() {
|
||||
return scanForTest("\n<body>Hello");
|
||||
}, "nclosed", 2);
|
||||
|
||||
// unnamed template
|
||||
checkError(function() {
|
||||
return scanForTest(
|
||||
"\n\n<template>Hi</template>\n\n<template>Hi</template>");
|
||||
}, "name", 3);
|
||||
|
||||
// helpful doctype message
|
||||
checkError(function() {
|
||||
return scanForTest(
|
||||
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '+
|
||||
'"http://www.w3.org/TR/html4/strict.dtd">'+
|
||||
'\n\n<head>\n</head>');
|
||||
}, "DOCTYPE", 1);
|
||||
|
||||
// lowercase basic doctype
|
||||
checkError(function() {
|
||||
return scanForTest(
|
||||
'<!doctype html>');
|
||||
}, "DOCTYPE", 1);
|
||||
|
||||
// attributes on head not supported
|
||||
checkError(function() {
|
||||
return scanForTest('<head foo="bar">\n Hello\n</head>');
|
||||
}, "<head>", 1);
|
||||
|
||||
// can't mismatch quotes
|
||||
checkError(function() {
|
||||
return scanForTest('<template name="foo\'>'+
|
||||
'pizza</template>');
|
||||
}, "error in tag", 1);
|
||||
|
||||
// unexpected <html> at top level
|
||||
checkError(function() {
|
||||
return scanForTest('\n<html>\n</html>');
|
||||
}, "Expected one of: <body>, <head>, <template>", 2);
|
||||
|
||||
});
|
||||
@@ -1,172 +0,0 @@
|
||||
TemplatingTools.scanHtmlForTags = function scanHtmlForTags(options) {
|
||||
const scan = new HtmlScan(options);
|
||||
return scan.getTags();
|
||||
};
|
||||
|
||||
/**
|
||||
* Scan an HTML file for top-level tags and extract their contents. Pass them to
|
||||
* a tag handler (an object with a handleTag method)
|
||||
*
|
||||
* This is a primitive, regex-based scanner. It scans
|
||||
* top-level tags, which are allowed to have attributes,
|
||||
* and ignores top-level HTML comments.
|
||||
*/
|
||||
class HtmlScan {
|
||||
/**
|
||||
* Initialize and run a scan of a single file
|
||||
* @param {String} sourceName The filename, used in errors only
|
||||
* @param {String} contents The contents of the file
|
||||
* @param {String[]} tagNames An array of tag names that are accepted at the
|
||||
* top level. If any other tag is encountered, an error is thrown.
|
||||
*/
|
||||
constructor({
|
||||
sourceName,
|
||||
contents,
|
||||
tagNames
|
||||
}) {
|
||||
this.sourceName = sourceName;
|
||||
this.contents = contents;
|
||||
this.tagNames = tagNames;
|
||||
|
||||
this.rest = contents;
|
||||
this.index = 0;
|
||||
|
||||
this.tags = [];
|
||||
|
||||
tagNameRegex = this.tagNames.join("|");
|
||||
const openTagRegex = new RegExp(`^((<(${tagNameRegex})\\b)|(<!--)|(<!DOCTYPE|{{!)|$)`, "i");
|
||||
|
||||
while (this.rest) {
|
||||
// skip whitespace first (for better line numbers)
|
||||
this.advance(this.rest.match(/^\s*/)[0].length);
|
||||
|
||||
const match = openTagRegex.exec(this.rest);
|
||||
|
||||
if (! match) {
|
||||
this.throwCompileError(`Expected one of: <${this.tagNames.join('>, <')}>`);
|
||||
}
|
||||
|
||||
const matchToken = match[1];
|
||||
const matchTokenTagName = match[3];
|
||||
const matchTokenComment = match[4];
|
||||
const matchTokenUnsupported = match[5];
|
||||
|
||||
const tagStartIndex = this.index;
|
||||
this.advance(match.index + match[0].length);
|
||||
|
||||
if (! matchToken) {
|
||||
break; // matched $ (end of file)
|
||||
}
|
||||
|
||||
if (matchTokenComment === '<!--') {
|
||||
// top-level HTML comment
|
||||
const commentEnd = /--\s*>/.exec(this.rest);
|
||||
if (! commentEnd)
|
||||
this.throwCompileError("unclosed HTML comment in template file");
|
||||
this.advance(commentEnd.index + commentEnd[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchTokenUnsupported) {
|
||||
switch (matchTokenUnsupported.toLowerCase()) {
|
||||
case '<!doctype':
|
||||
this.throwCompileError(
|
||||
"Can't set DOCTYPE here. (Meteor sets <!DOCTYPE html> for you)");
|
||||
case '{{!':
|
||||
this.throwCompileError(
|
||||
"Can't use '{{! }}' outside a template. Use '<!-- -->'.");
|
||||
}
|
||||
|
||||
this.throwCompileError();
|
||||
}
|
||||
|
||||
// otherwise, a <tag>
|
||||
const tagName = matchTokenTagName.toLowerCase();
|
||||
const tagAttribs = {}; // bare name -> value dict
|
||||
const tagPartRegex = /^\s*((([a-zA-Z0-9:_-]+)\s*=\s*(["'])(.*?)\4)|(>))/;
|
||||
|
||||
// read attributes
|
||||
let attr;
|
||||
while ((attr = tagPartRegex.exec(this.rest))) {
|
||||
const attrToken = attr[1];
|
||||
const attrKey = attr[3];
|
||||
let attrValue = attr[5];
|
||||
this.advance(attr.index + attr[0].length);
|
||||
|
||||
if (attrToken === '>') {
|
||||
break;
|
||||
}
|
||||
|
||||
// XXX we don't HTML unescape the attribute value
|
||||
// (e.g. to allow "abcd"efg") or protect against
|
||||
// collisions with methods of tagAttribs (e.g. for
|
||||
// a property named toString)
|
||||
attrValue = attrValue.match(/^\s*([\s\S]*?)\s*$/)[1]; // trim
|
||||
tagAttribs[attrKey] = attrValue;
|
||||
}
|
||||
|
||||
if (! attr) { // didn't end on '>'
|
||||
this.throwCompileError("Parse error in tag");
|
||||
}
|
||||
|
||||
// find </tag>
|
||||
const end = (new RegExp('</'+tagName+'\\s*>', 'i')).exec(this.rest);
|
||||
if (! end) {
|
||||
this.throwCompileError("unclosed <"+tagName+">");
|
||||
}
|
||||
|
||||
const tagContents = this.rest.slice(0, end.index);
|
||||
const contentsStartIndex = this.index;
|
||||
|
||||
// trim the tag contents.
|
||||
// this is a courtesy and is also relied on by some unit tests.
|
||||
var m = tagContents.match(/^([ \t\r\n]*)([\s\S]*?)[ \t\r\n]*$/);
|
||||
const trimmedContentsStartIndex = contentsStartIndex + m[1].length;
|
||||
const trimmedTagContents = m[2];
|
||||
|
||||
const tag = {
|
||||
tagName: tagName,
|
||||
attribs: tagAttribs,
|
||||
contents: trimmedTagContents,
|
||||
contentsStartIndex: trimmedContentsStartIndex,
|
||||
tagStartIndex: tagStartIndex,
|
||||
fileContents: this.contents,
|
||||
sourceName: this.sourceName
|
||||
};
|
||||
|
||||
// save the tag
|
||||
this.tags.push(tag);
|
||||
|
||||
// advance afterwards, so that line numbers in errors are correct
|
||||
this.advance(end.index + end[0].length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the parser
|
||||
* @param {Number} amount The amount of characters to advance
|
||||
*/
|
||||
advance(amount) {
|
||||
this.rest = this.rest.substring(amount);
|
||||
this.index += amount;
|
||||
}
|
||||
|
||||
throwCompileError(msg, overrideIndex) {
|
||||
const finalIndex = (typeof overrideIndex === 'number' ? overrideIndex : this.index);
|
||||
|
||||
const err = new TemplatingTools.CompileError();
|
||||
err.message = msg || "bad formatting in template file";
|
||||
err.file = this.sourceName;
|
||||
err.line = this.contents.substring(0, finalIndex).split('\n').length;
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
throwBodyAttrsError(msg) {
|
||||
this.parseError(msg);
|
||||
}
|
||||
|
||||
getTags() {
|
||||
return this.tags;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
Package.describe({
|
||||
name: 'templating-tools',
|
||||
version: '1.0.4',
|
||||
// Brief, one-line summary of the package.
|
||||
summary: 'Tools to scan HTML and compile tags when building a templating package',
|
||||
// URL to the Git repository containing the source code for this package.
|
||||
git: 'https://github.com/meteor/meteor',
|
||||
// By default, Meteor will default to using README.md for documentation.
|
||||
// To avoid submitting documentation, set this field to null.
|
||||
documentation: 'README.md'
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.use([
|
||||
'underscore',
|
||||
'ecmascript',
|
||||
'spacebars-compiler',
|
||||
|
||||
// minifier-js is a weak dependency of spacebars-compiler; adding it here
|
||||
// ensures that the output is minified. (Having it as a weak dependency means
|
||||
// that we don't ship uglify etc with built apps just because
|
||||
// boilerplate-generator uses spacebars-compiler.)
|
||||
// XXX maybe uglify should be applied by this plugin instead of via magic
|
||||
// weak dependency.
|
||||
'minifier-js'
|
||||
]);
|
||||
|
||||
api.addFiles([
|
||||
'templating-tools.js',
|
||||
'html-scanner.js',
|
||||
'compile-tags-with-spacebars.js',
|
||||
'throw-compile-error.js',
|
||||
'code-generation.js'
|
||||
]);
|
||||
|
||||
api.export('TemplatingTools');
|
||||
});
|
||||
|
||||
Package.onTest(function(api) {
|
||||
api.use([
|
||||
'tinytest',
|
||||
'templating-tools',
|
||||
'ecmascript'
|
||||
]);
|
||||
|
||||
api.addFiles('html-scanner-tests.js', 'server');
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
TemplatingTools = {
|
||||
// This type of error should be thrown during compilation
|
||||
CompileError: class CompileError {}
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
TemplatingTools.throwCompileError =
|
||||
function throwCompileError(tag, message, overrideIndex) {
|
||||
const finalIndex = (typeof overrideIndex === 'number' ?
|
||||
overrideIndex : tag.tagStartIndex);
|
||||
|
||||
const err = new TemplatingTools.CompileError();
|
||||
err.message = message || "bad formatting in template file";
|
||||
err.file = tag.sourceName;
|
||||
err.line = tag.fileContents.substring(0, finalIndex).split('\n').length;
|
||||
throw err;
|
||||
}
|
||||
1
packages/templating/.gitignore
vendored
1
packages/templating/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.build*
|
||||
@@ -1,13 +0,0 @@
|
||||
# templating
|
||||
|
||||
Compiles Blaze templates defined in `.html` files. Also automatically includes Blaze on the client.
|
||||
|
||||
This build plugin parses all of the HTML files in your app and looks for three top-level tags:
|
||||
|
||||
- `<head>` - appended to the `head` section of your HTML
|
||||
- `<body>` - appended to the `body` section of your HTML
|
||||
- `<template name="templateName">` - compiled into a Blaze template, which can be included with `{{> templateName}} or referenced in JS code with `Template.templateName`.
|
||||
|
||||
For more details, see the [Meteor docs about
|
||||
templating](http://docs.meteor.com/#/full/livehtmltemplates) and the Blaze
|
||||
[project page](https://www.meteor.com/blaze).
|
||||
@@ -1,23 +0,0 @@
|
||||
<!-- Expects the data context to have a `template` property (the name of
|
||||
the template to render) and an optional `data` property. If the `data`
|
||||
property is not specified, then the parent data context will be used
|
||||
instead. Uses the __dynamicWithDataContext template below to actually
|
||||
render the template. -->
|
||||
<template name="__dynamic">
|
||||
{{checkContext}}
|
||||
{{#if dataContextPresent}}
|
||||
{{# __dynamicWithDataContext}}{{> Template.contentBlock}}{{/__dynamicWithDataContext}}
|
||||
{{else}}
|
||||
{{! if there was no explicit 'data' argument, use the parent context}}
|
||||
{{# __dynamicWithDataContext template=template data=..}}{{> Template.contentBlock}}{{/__dynamicWithDataContext}}
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
<!-- Expects the data context to have a `template` property (the name of
|
||||
the template to render) and a `data` property, which can be falsey. -->
|
||||
<template name="__dynamicWithDataContext">
|
||||
{{#with chooseTemplate template}}
|
||||
{{!-- The .. is evaluated inside {{#with ../data}} --}}
|
||||
{{# .. ../data}}{{> Template.contentBlock}}{{/ ..}}
|
||||
{{/with}}
|
||||
</template>
|
||||
@@ -1,39 +0,0 @@
|
||||
var Template = Blaze.Template;
|
||||
|
||||
/**
|
||||
* @isTemplate true
|
||||
* @memberOf Template
|
||||
* @function dynamic
|
||||
* @summary Choose a template to include dynamically, by name.
|
||||
* @locus Templates
|
||||
* @param {String} template The name of the template to include.
|
||||
* @param {Object} [data] Optional. The data context in which to include the
|
||||
* template.
|
||||
*/
|
||||
|
||||
Template.__dynamicWithDataContext.helpers({
|
||||
chooseTemplate: function (name) {
|
||||
return Blaze._getTemplate(name, function () {
|
||||
return Template.instance();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.__dynamic.helpers({
|
||||
dataContextPresent: function () {
|
||||
return _.has(this, "data");
|
||||
},
|
||||
checkContext: function () {
|
||||
if (! _.has(this, "template")) {
|
||||
throw new Error("Must specify name in the 'template' argument " +
|
||||
"to {{> Template.dynamic}}.");
|
||||
}
|
||||
|
||||
_.each(this, function (v, k) {
|
||||
if (k !== "template" && k !== "data") {
|
||||
throw new Error("Invalid argument to {{> Template.dynamic}}: " +
|
||||
k);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
<template name="ui_dynamic_test">
|
||||
{{> Template.dynamic template=templateName data=templateData}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_no_data">
|
||||
{{> Template.dynamic template=templateName}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_inherited_data">
|
||||
{{#with context}}
|
||||
{{> Template.dynamic template=templateName}}
|
||||
{{else}}
|
||||
{{> Template.dynamic template=templateName}}
|
||||
{{/with}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_sub">
|
||||
test{{foo}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_contentblock">
|
||||
{{#Template.dynamic template=templateName data=templateData}}contentBlock{{/Template.dynamic}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_contentblock_no_data">
|
||||
{{# Template.dynamic template=templateName}}contentBlock{{/Template.dynamic}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_sub_contentblock">
|
||||
test{{foo}}{{>Template.contentBlock}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_falsey_inner_context">
|
||||
{{#with foo="bar"}}
|
||||
{{> Template.dynamic template=templateName data=context}}
|
||||
{{/with}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_bad_args0">
|
||||
{{> Template.dynamic}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_bad_args1">
|
||||
{{> Template.dynamic foo="bar"}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_bad_args2">
|
||||
{{> Template.dynamic template="ui_dynamic_test_sub" foo="bar"}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_falsey_context">
|
||||
{{> Template.dynamic template="ui_dynamic_test_falsey_context_sub"}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_falsey_context_sub">
|
||||
{{foo}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_backcompat">
|
||||
{{> UI.dynamic template=templateName data=templateData}}
|
||||
</template>
|
||||
@@ -1,239 +0,0 @@
|
||||
Tinytest.add(
|
||||
"spacebars - ui-dynamic-template - render template dynamically", function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_test;
|
||||
|
||||
var nameVar = new ReactiveVar;
|
||||
var dataVar = new ReactiveVar;
|
||||
tmpl.helpers({
|
||||
templateName: function () {
|
||||
return nameVar.get();
|
||||
},
|
||||
templateData: function () {
|
||||
return dataVar.get();
|
||||
}
|
||||
});
|
||||
|
||||
// No template chosen
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
|
||||
// Choose the "ui-dynamic-test-sub" template, with no data context
|
||||
// passed in.
|
||||
nameVar.set("ui_dynamic_test_sub");
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "test");
|
||||
|
||||
// Set a data context.
|
||||
dataVar.set({ foo: "bar" });
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "testbar");
|
||||
});
|
||||
|
||||
// Same test as above, but the {{> Template.dynamic}} inclusion has no
|
||||
// `dataContext` argument.
|
||||
Tinytest.add(
|
||||
"spacebars - ui-dynamic-template - render template dynamically, no data context",
|
||||
function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_test_no_data;
|
||||
|
||||
var nameVar = new ReactiveVar;
|
||||
tmpl.helpers({
|
||||
templateName: function () {
|
||||
return nameVar.get();
|
||||
}
|
||||
});
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
|
||||
nameVar.set("ui_dynamic_test_sub");
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "test");
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - ui-dynamic-template - render template " +
|
||||
"dynamically, data context gets inherited",
|
||||
function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_test_inherited_data;
|
||||
|
||||
var nameVar = new ReactiveVar();
|
||||
var dataVar = new ReactiveVar();
|
||||
tmpl.helpers({
|
||||
templateName: function () {
|
||||
return nameVar.get();
|
||||
},
|
||||
context: function () {
|
||||
return dataVar.get();
|
||||
}
|
||||
});
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
|
||||
nameVar.set("ui_dynamic_test_sub");
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "test");
|
||||
|
||||
// Set the top-level template's data context; this should be
|
||||
// inherited by the dynamically-chosen template, since the {{>
|
||||
// Template.dynamic}} inclusion didn't include a data argument.
|
||||
dataVar.set({ foo: "bar" });
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "testbar");
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - ui-dynamic-template - render template dynamically with contentBlock", function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_test_contentblock;
|
||||
|
||||
var nameVar = new ReactiveVar;
|
||||
var dataVar = new ReactiveVar;
|
||||
tmpl.helpers({
|
||||
templateName: function () {
|
||||
return nameVar.get();
|
||||
},
|
||||
templateData: function () {
|
||||
return dataVar.get();
|
||||
}
|
||||
});
|
||||
|
||||
// No template chosen
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
|
||||
// Choose the "ui-dynamic-test-sub" template, with no data context
|
||||
// passed in.
|
||||
nameVar.set("ui_dynamic_test_sub_contentblock");
|
||||
Tracker.flush({_throwFirstError: true});
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "testcontentBlock");
|
||||
|
||||
// Set a data context.
|
||||
dataVar.set({ foo: "bar" });
|
||||
Tracker.flush({_throwFirstError: true});
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "testbarcontentBlock");
|
||||
});
|
||||
|
||||
// Same test as above, but the {{> Template.dynamic}} inclusion has no
|
||||
// `dataContext` argument.
|
||||
Tinytest.add(
|
||||
"spacebars - ui-dynamic-template - render template dynamically with contentBlock, no data context",
|
||||
function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_test_contentblock_no_data;
|
||||
|
||||
var nameVar = new ReactiveVar;
|
||||
tmpl.helpers({
|
||||
templateName: function () {
|
||||
return nameVar.get();
|
||||
}
|
||||
});
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
|
||||
nameVar.set("ui_dynamic_test_sub_contentblock");
|
||||
Tracker.flush({_throwFirstError: true});
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "testcontentBlock");
|
||||
});
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - ui-dynamic-template - render template " +
|
||||
"dynamically, data context does not get inherited if " +
|
||||
"falsey context is passed in",
|
||||
function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_test_falsey_inner_context;
|
||||
|
||||
var nameVar = new ReactiveVar();
|
||||
var dataVar = new ReactiveVar();
|
||||
tmpl.helpers({
|
||||
templateName: function () {
|
||||
return nameVar.get();
|
||||
},
|
||||
context: function () {
|
||||
return dataVar.get();
|
||||
}
|
||||
});
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
|
||||
nameVar.set("ui_dynamic_test_sub");
|
||||
Tracker.flush();
|
||||
// Even though the data context is falsey, we DON'T expect the
|
||||
// subtemplate to inherit the data context from the parent template.
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "test");
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - ui-dynamic-template - render template " +
|
||||
"dynamically, bad arguments",
|
||||
function (test, expect) {
|
||||
var tmplPrefix = "ui_dynamic_test_bad_args";
|
||||
var errors = [
|
||||
"Must specify 'template' as an argument",
|
||||
"Must specify 'template' as an argument",
|
||||
"Invalid argument to {{> Template.dynamic}}"
|
||||
];
|
||||
|
||||
for (var i = 0; i < 3; i++) {
|
||||
var tmpl = Template[tmplPrefix + i];
|
||||
test.throws(function () {
|
||||
Blaze._throwNextException = true;
|
||||
var div = renderToDiv(tmpl);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - ui-dynamic-template - render template " +
|
||||
"dynamically, falsey context",
|
||||
function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_test_falsey_context;
|
||||
var subtmpl = Template.ui_dynamic_test_falsey_context_sub;
|
||||
|
||||
var subtmplContext;
|
||||
subtmpl.helpers({foo: function () {
|
||||
subtmplContext = this;
|
||||
}});
|
||||
var div = renderToDiv(tmpl);
|
||||
|
||||
// Because `this` can only be an object, Blaze normalizes falsey
|
||||
// data contexts to {}.
|
||||
test.equal(subtmplContext, {});
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - ui-dynamic-template - back-compat", function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_backcompat;
|
||||
|
||||
var nameVar = new ReactiveVar;
|
||||
var dataVar = new ReactiveVar;
|
||||
tmpl.helpers({
|
||||
templateName: function () {
|
||||
return nameVar.get();
|
||||
},
|
||||
templateData: function () {
|
||||
return dataVar.get();
|
||||
}
|
||||
});
|
||||
|
||||
// No template chosen
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
|
||||
// Choose the "ui-dynamic-test-sub" template, with no data context
|
||||
// passed in.
|
||||
nameVar.set("ui_dynamic_test_sub");
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "test");
|
||||
|
||||
// Set a data context.
|
||||
dataVar.set({ foo: "bar" });
|
||||
Tracker.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "testbar");
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
Package.describe({
|
||||
summary: "Allows templates to be defined in .html files",
|
||||
version: '1.2.14'
|
||||
});
|
||||
|
||||
// Today, this package is closely intertwined with Handlebars, meaning
|
||||
// that other templating systems will need to duplicate this logic. In
|
||||
// the future, perhaps we should have the concept of a template system
|
||||
// registry and a default templating system, ideally per-package.
|
||||
|
||||
Package.registerBuildPlugin({
|
||||
name: "compileTemplatesBatch",
|
||||
// minifier-js is a weak dependency of spacebars-compiler; adding it here
|
||||
// ensures that the output is minified. (Having it as a weak dependency means
|
||||
// that we don't ship uglify etc with built apps just because
|
||||
// boilerplate-generator uses spacebars-compiler.)
|
||||
// XXX maybe uglify should be applied by this plugin instead of via magic
|
||||
// weak dependency.
|
||||
use: [
|
||||
'caching-html-compiler',
|
||||
'ecmascript',
|
||||
'templating-tools'
|
||||
],
|
||||
sources: [
|
||||
'plugin/compile-templates.js'
|
||||
]
|
||||
});
|
||||
|
||||
// This onUse describes the *runtime* implications of using this package.
|
||||
Package.onUse(function (api) {
|
||||
// XXX would like to do the following only when the first html file
|
||||
// is encountered
|
||||
|
||||
api.addFiles('templating.js', 'client');
|
||||
api.export('Template', 'client');
|
||||
|
||||
api.use('underscore'); // only the subset in packages/blaze/microscore.js
|
||||
|
||||
api.use('isobuild:compiler-plugin@1.0.0');
|
||||
|
||||
// html_scanner.js emits client code that calls Meteor.startup and
|
||||
// Blaze, so anybody using templating (eg apps) need to implicitly use
|
||||
// 'meteor' and 'blaze'.
|
||||
api.use(['blaze', 'spacebars']);
|
||||
api.imply(['meteor', 'blaze', 'spacebars'], 'client');
|
||||
|
||||
api.addFiles(['dynamic.html', 'dynamic.js'], 'client');
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
Plugin.registerCompiler({
|
||||
extensions: ['html'],
|
||||
archMatching: 'web',
|
||||
isTemplate: true
|
||||
}, () => new CachingHtmlCompiler(
|
||||
"templating",
|
||||
TemplatingTools.scanHtmlForTags,
|
||||
TemplatingTools.compileTagsWithSpacebars
|
||||
));
|
||||
@@ -1,78 +0,0 @@
|
||||
|
||||
// Packages and apps add templates on to this object.
|
||||
|
||||
/**
|
||||
* @summary The class for defining templates
|
||||
* @class
|
||||
* @instanceName Template.myTemplate
|
||||
*/
|
||||
Template = Blaze.Template;
|
||||
|
||||
var RESERVED_TEMPLATE_NAMES = "__proto__ name".split(" ");
|
||||
|
||||
// Check for duplicate template names and illegal names that won't work.
|
||||
Template.__checkName = function (name) {
|
||||
// Some names can't be used for Templates. These include:
|
||||
// - Properties Blaze sets on the Template object.
|
||||
// - Properties that some browsers don't let the code to set.
|
||||
// These are specified in RESERVED_TEMPLATE_NAMES.
|
||||
if (name in Template || _.contains(RESERVED_TEMPLATE_NAMES, name)) {
|
||||
if ((Template[name] instanceof Template) && name !== "body")
|
||||
throw new Error("There are multiple templates named '" + name + "'. Each template needs a unique name.");
|
||||
throw new Error("This template name is reserved: " + name);
|
||||
}
|
||||
};
|
||||
|
||||
// XXX COMPAT WITH 0.8.3
|
||||
Template.__define__ = function (name, renderFunc) {
|
||||
Template.__checkName(name);
|
||||
Template[name] = new Template("Template." + name, renderFunc);
|
||||
// Exempt packages built pre-0.9.0 from warnings about using old
|
||||
// helper syntax, because we can. It's not very useful to get a
|
||||
// warning about someone else's code (like a package on Atmosphere),
|
||||
// and this should at least put a bit of a dent in number of warnings
|
||||
// that come from packages that haven't been updated lately.
|
||||
Template[name]._NOWARN_OLDSTYLE_HELPERS = true;
|
||||
};
|
||||
|
||||
// Define a template `Template.body` that renders its
|
||||
// `contentRenderFuncs`. `<body>` tags (of which there may be
|
||||
// multiple) will have their contents added to it.
|
||||
|
||||
/**
|
||||
* @summary The [template object](#templates_api) representing your `<body>`
|
||||
* tag.
|
||||
* @locus Client
|
||||
*/
|
||||
Template.body = new Template('body', function () {
|
||||
var view = this;
|
||||
return _.map(Template.body.contentRenderFuncs, function (func) {
|
||||
return func.apply(view);
|
||||
});
|
||||
});
|
||||
Template.body.contentRenderFuncs = []; // array of Blaze.Views
|
||||
Template.body.view = null;
|
||||
|
||||
Template.body.addContent = function (renderFunc) {
|
||||
Template.body.contentRenderFuncs.push(renderFunc);
|
||||
};
|
||||
|
||||
// This function does not use `this` and so it may be called
|
||||
// as `Meteor.startup(Template.body.renderIntoDocument)`.
|
||||
Template.body.renderToDocument = function () {
|
||||
// Only do it once.
|
||||
if (Template.body.view)
|
||||
return;
|
||||
|
||||
var view = Blaze.render(Template.body, document.body);
|
||||
Template.body.view = view;
|
||||
};
|
||||
|
||||
// XXX COMPAT WITH 0.9.0
|
||||
UI.body = Template.body;
|
||||
|
||||
// XXX COMPAT WITH 0.9.0
|
||||
// (<body> tags in packages built with 0.9.0)
|
||||
Template.__body__ = Template.body;
|
||||
Template.__body__.__contentParts = Template.body.contentViews;
|
||||
Template.__body__.__instantiate = Template.body.renderToDocument;
|
||||
Reference in New Issue
Block a user