diff --git a/History.md b/History.md
index 5ec2d0a1fa..d9a1d171c1 100644
--- a/History.md
+++ b/History.md
@@ -1,6 +1,64 @@
## vNEXT
+## v0.4.1
+
+* New `email` smart package, with [`Email.send`](http://docs.meteor.com/#email)
+ API.
+
+* Upgrade Node from 0.6.17 to 0.8.8, as well as many Node modules in the dev
+ bundle; those that are user-exposed are:
+ * coffee-script: 1.3.3 (from 1.3.1)
+ * stylus: 0.29.0 (from 0.28.1)
+ * nib: 0.8.2 (from 0.7.0)
+
+* All publicly documented APIs now use `camelCase` rather than
+ `under_scores`. The old spellings continue to work for now. New names are:
+ - `Meteor.isClient`/`isServer`
+ - `this.isSimulation` inside a method invocation
+ - `Meteor.deps.Context.onInvalidate`
+ - `Meteor.status().retryCount`/`retryTime`
+
+* Spark improvements
+ * Optimize selector matching for event maps.
+ * Fix `Spark._currentRenderer` behavior in timer callbacks.
+ * Fix bug caused by interaction between `Template.foo.preserve` and
+ `{{#constant}}`. #323
+ * Allow `{{#each}}` over a collection of objects without `_id`. #281
+ * Spark now supports Firefox 3.6.
+ * Added a script to build a standalone spark.js that does not depend on
+ Meteor (it depends on jQuery or Sizzle if you need IE7 support,
+ and otherwise is fully standalone).
+
+* Database writes from within `Meteor.setTimeout`/`setInterval`/`defer` will be
+ batched with other writes from the current method invocation if they start
+ before the method completes.
+
+* Make `Meteor.Cursor.forEach` fully synchronous even if the user's callback
+ yields. #321.
+
+* Recover from exceptions thrown in `Meteor.publish` handlers.
+
+* Upgrade bootstrap to version 2.1.1. #336, #337, #288, #293
+
+* Change the implementation of the `meteor deploy` password prompt to not crash
+ Emacs M-x shell.
+
+* Optimize `LocalCollection.remove(id)` to be O(1) rather than O(n).
+
+* Optimize client-side database performance when receiving updated data from the
+ server outside of method calls.
+
+* Better error reporting when a package in `.meteor/packages` does not exist.
+
+* Better error reporting for coffeescript. #331
+
+* Better error handling in `Handlebars.Exception`.
+
+
+Patches contributed by GitHub users fivethirty, tmeasday, and xenolf.
+
+
## v0.4.0
* Merge Spark, a new live page update engine
diff --git a/admin/debian/changelog b/admin/debian/changelog
index b4c3533497..8517d7b6be 100644
--- a/admin/debian/changelog
+++ b/admin/debian/changelog
@@ -1,4 +1,4 @@
-meteor (0.4.0-1) unstable; urgency=low
+meteor (0.4.1-1) unstable; urgency=low
* Automated debian build.
diff --git a/admin/debian/rules b/admin/debian/rules
index 7b223eeb5e..6a83b44185 100755
--- a/admin/debian/rules
+++ b/admin/debian/rules
@@ -17,11 +17,5 @@ override_dh_prep:
tar -C debian/tmp/usr/lib -xzf $(TARBALL)
echo -n 'deb' > debian/tmp/usr/lib/meteor/.package_stamp
-# node fibers distributes copies of the library pre-compiled for many
-# different architectures. This confuses shlibdeps. Just ignore the
-# fibers library.
-override_dh_shlibdeps:
- dh_shlibdeps -Xfibers.node
-
%:
dh $@
diff --git a/admin/generate-dev-bundle.sh b/admin/generate-dev-bundle.sh
index efc27770e6..6f495e7c2d 100755
--- a/admin/generate-dev-bundle.sh
+++ b/admin/generate-dev-bundle.sh
@@ -1,8 +1,9 @@
#!/bin/bash
set -e
+set -u
-BUNDLE_VERSION=0.2.2
+BUNDLE_VERSION=0.2.3
UNAME=$(uname)
ARCH=$(uname -m)
@@ -182,7 +183,6 @@ npm install mongodb@1.1.5
npm install uglify-js@1.3.3
npm install clean-css@0.6.0
npm install progress@0.0.5
-npm install fibers@0.6.9
npm install useragent@1.1.0
npm install request@2.11.0
npm install http-proxy@0.8.2
@@ -201,6 +201,19 @@ git clone http://github.com/akdubya/rbytes.git
npm install sockjs@0.3.1
rm -rf rbytes
+npm install fibers@0.6.9
+# Fibers ships with compiled versions of its C code for a dozen platforms. This
+# bloats our dev bundle, and confuses dpkg-buildpackage and rpmbuild into
+# thinking that the packages need to depend on both 32- and 64-bit versions of
+# libstd++. Remove all the ones other than our architecture. (Expression based
+# on build.js in fibers source.)
+FIBERS_ARCH=$(node -p -e 'process.platform + "-" + process.arch + "-v8-" + /[0-9]+\.[0-9]+/.exec(process.versions.v8)[0]')
+cd fibers/bin
+mv $FIBERS_ARCH ..
+rm -rf *
+mv ../$FIBERS_ARCH .
+cd ../..
+
cd "$DIR"
curl "$MONGO_URL" | tar -xz
diff --git a/admin/install-s3.sh b/admin/install-s3.sh
index d302a6ec5b..75b8502e9b 100755
--- a/admin/install-s3.sh
+++ b/admin/install-s3.sh
@@ -5,7 +5,7 @@
## example.
URLBASE="https://d3sqy0vbqsdhku.cloudfront.net"
-VERSION="0.4.0"
+VERSION="0.4.1"
PKGVERSION="${VERSION}-1"
UNAME=`uname`
diff --git a/admin/manifest.json b/admin/manifest.json
index ec2de4b0dc..d60784e251 100644
--- a/admin/manifest.json
+++ b/admin/manifest.json
@@ -1,6 +1,6 @@
{
- "version": "0.4.0",
- "deb_version": "0.4.0-1",
- "rpm_version": "0.4.0-1",
+ "version": "0.4.1",
+ "deb_version": "0.4.1-1",
+ "rpm_version": "0.4.1-1",
"urlbase": "https://d3sqy0vbqsdhku.cloudfront.net"
}
diff --git a/admin/meteor.spec b/admin/meteor.spec
index 31233ed895..43dde1499e 100644
--- a/admin/meteor.spec
+++ b/admin/meteor.spec
@@ -5,7 +5,7 @@
Summary: Meteor platform and JavaScript application server
Vendor: Meteor
Name: meteor
-Version: 0.4.0
+Version: 0.4.1
Release: 1
License: MIT
Group: Networking/WWW
diff --git a/admin/node.sh b/admin/node.sh
index 3326cd3bc8..ba712deb85 100755
--- a/admin/node.sh
+++ b/admin/node.sh
@@ -12,4 +12,4 @@ fi
cd "$ORIGDIR"
export NODE_PATH="$TOPDIR/dev_bundle/lib/node_modules"
-exec "$TOPDIR/dev_bundle/bin/node" $*
+exec "$TOPDIR/dev_bundle/bin/node" "$@"
diff --git a/app/lib/updater.js b/app/lib/updater.js
index f15d1c04c6..2ff86cb452 100644
--- a/app/lib/updater.js
+++ b/app/lib/updater.js
@@ -1,4 +1,4 @@
-exports.CURRENT_VERSION = "0.4.0";
+exports.CURRENT_VERSION = "0.4.1";
var fs = require("fs");
var http = require("http");
diff --git a/app/meteor/post-upgrade.js b/app/meteor/post-upgrade.js
index 5000bf0fda..a33008e577 100644
--- a/app/meteor/post-upgrade.js
+++ b/app/meteor/post-upgrade.js
@@ -2,7 +2,7 @@ try {
// XXX can't get this from updater.js because in 0.3.7 and before the
// updater didn't have the right NODE_PATH set. At some point we can
// remove this and just use updater.CURRENT_VERSION.
- var VERSION = "0.4.0";
+ var VERSION = "0.4.1";
var fs = require('fs');
var path = require('path');
diff --git a/docs/client/api.html b/docs/client/api.html
index 675f8e53a2..5984bd19e5 100644
--- a/docs/client/api.html
+++ b/docs/client/api.html
@@ -28,6 +28,7 @@ put on the screen.
});
}
+{{> api_box absoluteUrl}}
Publish and subscribe
diff --git a/docs/client/api.js b/docs/client/api.js
index dd78bb410c..fc0306ac52 100644
--- a/docs/client/api.js
+++ b/docs/client/api.js
@@ -24,6 +24,36 @@ Template.api.startup = {
]
};
+Template.api.absoluteUrl = {
+ id: "meteor_absoluteurl",
+ name: "Meteor.absoluteUrl([path], [options])",
+ locus: "Anywhere",
+ descr: ["Generate an absolute URL pointing to the application. The server "
+ + "reads from the `ROOT_URL` environment variable to determine "
+ + "where it is running. This is taken care of automatically for "
+ + "apps deployed with `meteor deploy`, but must be provided when "
+ + "using `meteor bundle`."],
+ args: [
+ {name: "path",
+ type: "String",
+ descr: 'A path to append to the root URL. Do not include a leading "`/`".'
+ }
+ ],
+ options: [
+ {name: "secure",
+ type: "Boolean",
+ descr: "Create an HTTPS URL."
+ },
+ {name: "replaceLocalhost",
+ type: "Boolean",
+ descr: "Replace localhost with 127.0.0.1. Useful for services that don't recognize localhost as a domain name."},
+ {name: "rootUrl",
+ type: "String",
+ descr: "Override the default ROOT_URL from the server environment. For example: \"`http://foo.example.com`\""
+ }
+ ]
+};
+
Template.api.publish = {
id: "meteor_publish",
name: "Meteor.publish(name, func)",
diff --git a/docs/client/docs.html b/docs/client/docs.html
index 9306792c71..331460e720 100644
--- a/docs/client/docs.html
+++ b/docs/client/docs.html
@@ -11,7 +11,7 @@
-
Meteor 0.4.0
+
Meteor 0.4.1
{{> introduction }}
{{> concepts }}
{{> api }}
diff --git a/docs/client/docs.js b/docs/client/docs.js
index 165e679de5..871b46b975 100644
--- a/docs/client/docs.js
+++ b/docs/client/docs.js
@@ -1,4 +1,4 @@
-METEOR_VERSION = "0.4.0";
+METEOR_VERSION = "0.4.1";
Meteor.startup(function () {
// XXX this is broken by the new multi-page layout. Also, it was
@@ -82,7 +82,8 @@ var toc = [
"Core", [
"Meteor.isClient",
"Meteor.isServer",
- "Meteor.startup"
+ "Meteor.startup",
+ "Meteor.absoluteUrl"
],
"Publish and subscribe", [
@@ -205,7 +206,6 @@ var toc = [
],
"Packages", [ [
- "absolute-url",
"amplify",
"backbone",
"bootstrap",
diff --git a/docs/client/packages.html b/docs/client/packages.html
index 51eddc0373..1c12157077 100644
--- a/docs/client/packages.html
+++ b/docs/client/packages.html
@@ -16,7 +16,6 @@ and removed with:
$ meteor remove
-{{> pkg_absolute_url}}
{{> pkg_amplify}}
{{> pkg_backbone}}
{{> pkg_bootstrap}}
diff --git a/docs/client/packages/absolute-url.html b/docs/client/packages/absolute-url.html
deleted file mode 100644
index 19a0401e63..0000000000
--- a/docs/client/packages/absolute-url.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-{{#better_markdown}}
-## `absolute-url`
-
-This package allows constructing absolute URLs pointing back to the
-application. The server reads from the `ROOT_URL` environment variable
-to determine where it is running. This is taken care of automatically
-for apps deployed with `meteor deploy`, but must be provided when using
-`meteor bundle`.
-
-{{/better_markdown}}
-
-{{> api_box absoluteUrl}}
-
-
diff --git a/docs/client/packages/absolute-url.js b/docs/client/packages/absolute-url.js
deleted file mode 100644
index 7a4ba1d036..0000000000
--- a/docs/client/packages/absolute-url.js
+++ /dev/null
@@ -1,24 +0,0 @@
-Template.pkg_absolute_url.absoluteUrl = {
- id: "meteor_absoluteUrl",
- name: "Meteor.absoluteUrl([path], [options])",
- locus: "Anywhere",
- descr: ["Generate an absolute URL pointing to the application."],
- args: [
- {name: "path",
- type: "String",
- descr: 'A path to append to the root URL. Do not include a leading "`/`".'
- }
- ],
- options: [
- {name: "secure",
- type: "Boolean",
- descr: "Create an HTTPS URL."
- },
- {name: "rootUrl",
- type: "String",
- descr: "Override the default ROOT_URL from the server environment. For example: \"`http://foo.example.com`\""
- }
- ]
-
-};
-
diff --git a/examples/unfinished/jsparse-demo/.meteor/.gitignore b/examples/unfinished/jsparse-demo/.meteor/.gitignore
new file mode 100644
index 0000000000..4083037423
--- /dev/null
+++ b/examples/unfinished/jsparse-demo/.meteor/.gitignore
@@ -0,0 +1 @@
+local
diff --git a/examples/unfinished/jsparse-demo/.meteor/packages b/examples/unfinished/jsparse-demo/.meteor/packages
new file mode 100644
index 0000000000..0c508c3289
--- /dev/null
+++ b/examples/unfinished/jsparse-demo/.meteor/packages
@@ -0,0 +1,7 @@
+# Meteor packages used by this project, one per line.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+
+autopublish
+jsparse
diff --git a/examples/unfinished/jsparse-demo/jsparse-demo.css b/examples/unfinished/jsparse-demo/jsparse-demo.css
new file mode 100644
index 0000000000..bea973c397
--- /dev/null
+++ b/examples/unfinished/jsparse-demo/jsparse-demo.css
@@ -0,0 +1,147 @@
+
+* { padding: 0; margin: 0; }
+html, body { height: 100%; }
+
+#topbar {
+ position: absolute;
+ width: 100%;
+ top: 0;
+ height: 39px;
+ border-bottom: 1px solid #555;
+ overflow: auto;
+ background: #cfc;
+ font-size: 12px;
+}
+
+#topbarinner {
+ padding: 7px;
+ font-family: sans-serif;
+}
+
+#main {
+ position: absolute;
+ width: 100%;
+ top: 40px;
+ bottom: 0;
+}
+
+#inputarea textarea {
+ border: 0;
+ border-right: 1px solid #555;
+ position: absolute;
+ height: 100%;
+ left: 0;
+ right: 50%;
+ font-family: monospace;
+ font-size: 100%;
+}
+
+#output {
+ position: absolute;
+ height: 100%;
+ left: 50%;
+ right: 0;
+ overflow: auto;
+
+ font-family: monospace;
+}
+
+#inputarea textarea, #output {
+ line-height: 130%;
+}
+
+.lex { border: 1px solid #333; }
+
+.lex_keyword { background: #0f0; }
+.lex_identifier { background: #ff0; }
+.lex_punctuation { background: #0ff; }
+.lex_error { background: #f00; }
+.lex_whitespace { background: #fcc; }
+.lex_comment { background: #ccc; }
+
+.lex_regex { background: #f0f; }
+.lex_null { background: #dac; }
+.lex_boolean { background: #faf; }
+.lex_number { background: #c3f; }
+.lex_string { background: #fc3; }
+
+.parseerror {
+ background: #f99;
+ border: 1px solid blue;
+ cursor: pointer;
+}
+.parseerrormessage { color: #c00; }
+
+.box {
+ display: inline-block;
+ margin: 5px;
+ margin-top: 0;
+ background: #fff;
+}
+
+.box.statement {
+ display: block;
+}
+
+#output > .box {
+ margin-top: 5px;
+}
+
+.box.named {
+ border: 1px solid #888;
+ border-radius: 5px;
+ cursor: pointer;
+ overflow: hidden;
+ /* position:relative breaks overflow:hidden effect of rounded corners? */
+ position: static;
+}
+
+.box.head {
+ font-family: sans-serif;
+ font-size: 70%;
+ font-weight: bold;
+ display: block;
+ margin-left: 0;
+ margin-right: 0;
+ background: #ccc;
+ color: #000;
+ padding-left: 5px;
+ padding-right: 5px;
+ border-bottom: 1px solid #888;
+}
+
+.box.head:last-child {
+ margin: 0;
+ border-bottom: 0;
+}
+
+.box.token {
+ background: #ddd;
+ /*border: 1px solid #999;*/
+ border: 1px solid #00f;
+ cursor: pointer;
+ font-family: monospace;
+ font-weight: bold;
+ font-size: 120%;
+ padding: 1px;
+}
+
+.box.named[mousehover] {
+ background: #cdf;
+ border: 1px solid #448;
+}
+
+.box.token[mousehover] {
+ background: #ace;
+ border: 1px solid #448;
+}
+
+.box.named[mousehover] > .box.head {
+ background: #58b;
+ border-bottom: 1px solid #448;
+}
+
+.box.named[mousehover] > .box.head:last-child {
+ border-bottom: 0;
+}
+
diff --git a/examples/unfinished/jsparse-demo/jsparse-demo.html b/examples/unfinished/jsparse-demo/jsparse-demo.html
new file mode 100644
index 0000000000..d8a676b2b6
--- /dev/null
+++ b/examples/unfinished/jsparse-demo/jsparse-demo.html
@@ -0,0 +1,29 @@
+
+ jsparser
+
+
+
+ {{> page}}
+
+
+
+
+
+ This is a demo of the new jsparse package.
+ Edit code on the left.
+ Click on an outlined box on the right
+ to select it in the code. The full spec is supported.
+
+
+
+
{{!
+whitespace is significant here; browser swallows initial
+ newline in textarea
+}}
+
+
+ {{output}}
+
+
+
diff --git a/examples/unfinished/jsparse-demo/jsparse-demo.js b/examples/unfinished/jsparse-demo/jsparse-demo.js
new file mode 100644
index 0000000000..9ea3d008de
--- /dev/null
+++ b/examples/unfinished/jsparse-demo/jsparse-demo.js
@@ -0,0 +1,151 @@
+
+
+if (Meteor.is_client) {
+ Meteor.startup(function () {
+ if (! Session.get("input"))
+ Session.set("input", "var x = 3");
+ });
+
+ Template.page.input = function () {
+ return Session.get("input") || '';
+ };
+
+ Template.page.output = function () {
+ var input = Session.get("input") || "";
+
+ // LEXER
+ /*
+ if (! input)
+ return "";
+
+ var L = new Lexer(input);
+ var html = "";
+ while (L.next() !== 'EOF') {
+ if (L.type === "NEWLINE") {
+ html += '
';
+ } else {
+ var text = Handlebars._escape(L.text || ' ');
+ text = text.replace(/(?!.)\s/g, '
'); // for multiline comments
+ text = text.replace(/\s/g, ' ');
+ html += '' +
+ text + '';
+ if (L.type === "ERROR")
+ break;
+ }
+ }*/
+
+ // PARSER
+ var html;
+ var tree = null;
+ var parser = new JSParser(input);
+ try {
+ tree = parser.getSyntaxTree();
+ } catch (parseError) {
+ var errorLexeme = parser.lexer.lastLexeme;
+
+ html = Handlebars._escape(
+ input.substring(0, errorLexeme.startPos()));
+ html += Spark.setDataContext(
+ errorLexeme,
+ '' +
+ Handlebars._escape(errorLexeme.text() || '') +
+ '');
+ html = html.replace(/(?!.)\s/g, '
');
+ html += '' +
+ Handlebars._escape(parseError.toString()) + '
';
+ }
+ if (tree) {
+ var curPos = 0;
+ var unclosedInfos = [];
+ var toHtml = function (obj) {
+ if (obj instanceof ParseNode) {
+ var head = obj.name || '';
+ var children = obj.children;
+ var info = { startPos: curPos };
+ var isStatement = (head.indexOf('Stmnt') >= 0);
+ var html = Spark.setDataContext(
+ info,
+ '' + Handlebars._escape(head) + '
' +
+ _.map(children, toHtml).join('') + '
');
+ unclosedInfos.push(info);
+ return html;
+ } else if (obj.text) {
+ // token
+ _.each(unclosedInfos, function (info) {
+ info.endPos = curPos;
+ });
+ curPos = obj.endPos();
+ unclosedInfos.length = 0;
+ var text = obj.text();
+ // insert zero-width spaces to allow wrapping
+ text = text.replace(/.{20}/g, "$&\n");
+ text = Handlebars._escape(text);
+ text = text.replace(/\n/g, '');
+ return Spark.setDataContext(
+ obj,
+ '' + text + '
');
+ } else {
+ // other?
+ return '' +
+ Handlebars._escape(JSON.stringify(obj)) + '
';
+ }
+ };
+ html = toHtml(tree);
+ curPos = parser.lexer.pos;
+ _.each(unclosedInfos, function (info) {
+ info.endPos = curPos;
+ });
+ }
+
+ return new Handlebars.SafeString(html);
+ };
+
+ Template.page.events({
+ 'keyup #inputarea textarea': function (event) {
+ var input = event.currentTarget.value;
+ Session.set("input", input);
+ },
+ 'mouseover .box.named, mouseover .box.token': function (event) {
+ event.currentTarget.setAttribute('mousehover', 'mousehover');
+ event.stopImmediatePropagation();
+ },
+ 'mouseout .box.named, mouseout .box.token': function (event) {
+ event.currentTarget.removeAttribute('mousehover');
+ event.stopImmediatePropagation();
+ },
+ 'click .box.token': function (event) {
+ selectInputText(this.startPos(), this.endPos());
+ return false;
+ },
+ 'click .box.named': function (event) {
+ selectInputText(this.startPos, this.endPos);
+ return false;
+ },
+ 'click .parseerror': function (event) {
+ selectInputText(this.startPos(), this.endPos());
+ return false;
+ }
+ });
+
+ Template.page.preserve(['#inputarea textarea']);
+
+ var selectTextInArea = function (e, start, end){
+ e.focus();
+ if (e.setSelectionRange) {
+ e.setSelectionRange(start, end);
+ } else if (e.createTextRange) {
+ var r = e.createTextRange();
+ r.collapse(true);
+ r.moveEnd('character', end);
+ r.moveStart('character', start);
+ r.select();
+ }
+ };
+
+ var selectInputText = function (start, end) {
+ var textarea = DomUtils.find(document, '#inputarea textarea');
+ selectTextInArea(textarea, start, end);
+ };
+
+}
diff --git a/meteor b/meteor
index 34c354c120..70e583c498 100755
--- a/meteor
+++ b/meteor
@@ -1,6 +1,6 @@
#!/bin/bash
-BUNDLE_VERSION=0.2.2
+BUNDLE_VERSION=0.2.3
# OS Check. Put here because here is where we download the precompiled
# bundles that are arch specific.
diff --git a/packages/absolute-url/package.js b/packages/absolute-url/package.js
index 796cba69b8..b84536203d 100644
--- a/packages/absolute-url/package.js
+++ b/packages/absolute-url/package.js
@@ -1,17 +1,10 @@
Package.describe({
- summary: "Generate absolute URLs pointing to the application"
+ summary: "DEPRECATED: Generate absolute URLs pointing to the application",
+ internal: true
});
Package.on_use(function (api) {
- // note server before common. usually it is the other way around, but
- // in this case server must load first.
- api.add_files('url_server.js', 'server');
- api.add_files('url_common.js', ['client', 'server']);
-});
-
-Package.on_test(function (api) {
- api.use('absolute-url', ['client', 'server']);
- api.use('tinytest');
-
- api.add_files('url_tests.js', ['client', 'server']);
+ console.log('DEPRECATED. The `absolute-url` package has been folded into '
+ + 'the `meteor` package and should not be used directly. Run '
+ + '`meteor remove absolute-url` to resolve this.');
});
diff --git a/packages/domutils/domutils.js b/packages/domutils/domutils.js
index fdc7e6efb7..53f266d601 100644
--- a/packages/domutils/domutils.js
+++ b/packages/domutils/domutils.js
@@ -5,11 +5,10 @@ DomUtils = {};
(function () {
var qsaFindAllBySelector = function (selector, contextNode) {
+ // If IE7 users report the following error message, you
+ // can fix it with "meteor add jquery".
if (! document.querySelectorAll)
- // IE 7
- throw new Error(
- "This browser doesn't support querySelectorAll. " +
- "You need Sizzle or jQuery (`meteor add jquery`).");
+ throw new Error("This browser doesn't support querySelectorAll.");
// the search is constrained to descendants of `ancestor`,
// but it doesn't affect the scope of the query.
diff --git a/packages/email/email_tests.js b/packages/email/email_tests.js
index f635104ba1..68c78eef47 100644
--- a/packages/email/email_tests.js
+++ b/packages/email/email_tests.js
@@ -1,6 +1,9 @@
streamBuffers = __meteor_bootstrap__.require('stream-buffers');
Tinytest.add("email - dev mode smoke test", function (test) {
+ // This only tests dev mode, so don't run the test if this is deployed.
+ if (process.env.MAIL_URL) return;
+
var old_stream = Email._output_stream;
try {
Email._output_stream = new streamBuffers.WritableStreamBuffer;
diff --git a/packages/force-ssl/package.js b/packages/force-ssl/package.js
index 90b5037578..bc413ad820 100644
--- a/packages/force-ssl/package.js
+++ b/packages/force-ssl/package.js
@@ -8,11 +8,6 @@ Package.on_use(function (api) {
// server has been instantiated.
api.use('livedata', 'server');
- // we don't really depend on absolute-url, but we do modify its
- // behavior. If there were a way to say "if the other package is
- // loaded, make sure we come after it", we should do that here.
- api.use('absolute-url', ['client', 'server']);
-
api.add_files('force_ssl_common.js', ['client', 'server']);
api.add_files('force_ssl_server.js', 'server');
diff --git a/packages/http/httpcall_tests.js b/packages/http/httpcall_tests.js
index 2baecbaec4..edd0a29fbd 100644
--- a/packages/http/httpcall_tests.js
+++ b/packages/http/httpcall_tests.js
@@ -69,7 +69,7 @@ testAsyncMulti("httpcall - basic", [
"/foo?fruit=apple&dog=Spot+the+dog");
}]);
-testAsyncMulti("httpcall - failure", [
+testAsyncMulti("httpcall - errors", [
function(test, expect) {
// Accessing unknown server (should fail to make any connection)
diff --git a/packages/jsparse/lexer.js b/packages/jsparse/lexer.js
new file mode 100644
index 0000000000..c62e2c6bd4
--- /dev/null
+++ b/packages/jsparse/lexer.js
@@ -0,0 +1,397 @@
+
+(function () {
+
+var regexEscape = function (str) {
+ return str.replace(/[\][^$\\.*+?(){}|]/g, '\\$&');
+};
+
+// 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
+
+// Section 7.2
+// Match one or more characters of whitespace, excluding line terminators.
+// Do this by matching reluctantly, stopping at a non-dot (line terminator
+// or end of string) or a non-whitespace.
+// We are taking advantage of the fact that we are parsing JS from JS in
+// regexes like this by "passing through" the spec's definition of whitespace,
+// which is the same in regexes and the lexical grammar.
+var rWhiteSpace = /[^\S\u000A\u000D\u2028\u2029]+/g;
+// Section 7.3
+// Match one line terminator. Same as (?!.)[\s\S] but more explicit.
+var rLineTerminator = /[\u000A\u000D\u2028\u2029]/g;
+// Section 7.4
+// Match one multi-line comment.
+// [\s\S] is shorthand for any character, including newlines.
+// The *? reluctant qualifier makes this easy.
+var rMultiLineComment = /\/\*[\s\S]*?\*\//g;
+// Match one single-line comment, not including the line terminator.
+var rSingleLineComment = /\/\/.*/g;
+// 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('|') +
+ ")+", 'g');
+// 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('|') + ")+", 'g');
+// Section 7.7
+// Match one punctuator (except for division punctuators).
+var rPunctuator = new RegExp(
+ regexEscape("{ } ( ) [ ] . ; , < > <= >= == != === !== + - * % ++ -- << >> "+
+ ">>> & | ^ ! ~ && || ? : = += -= *= %= <<= >>= >>>= &= |= ^=")
+ // sort from longest to shortest so that we don't match '==' for '===' and
+ // '*' for '*=', etc.
+ .split(' ').sort(function (a,b) { return b.length - a.length; })
+ .join('|'), 'g');
+var rDivPunctuator = /\/=?/g;
+// Section 7.8.3
+var rHexLiteral = /0x[0-9a-fA-F]+$/g;
+var rOctLiteral = /0[0-7]+/g; // deprecated
+var rDecLiteral =
+ /(((0|[1-9][0-9]*)(\.[0-9]*)?)|\.[0-9]+)([Ee][+-]?[0-9]+)?/g;
+// Section 7.8.4
+var rStringQuote = /["']/g;
+// Match one or more characters besides quotes, backslashes, or line ends
+var rStringMiddle = /(?=.)[^"'\\]+?((?!.)|(?=["'\\]))/g;
+// 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])/g;
+// Section 7.8.5
+// Match one regex literal, including slashes, not including flags.
+// XXX Add support for unescaped '/' in character class, allowed by 5th ed.
+var rRegexLiteral = /\/(?![*\/])(\\.|(?=.)[^\\])+?\//g;
+var rRegexFlags = /[a-zA-Z]*/g;
+
+var rDecider =
+ /((?=.)\s)|(\/[\/\*]?)|([\][{}().;,<>=!+*%&|^~?:-])|(\d)|(["'])|(.)|([\S\s])/g;
+
+var keywordLookup = {
+ ' break': 'KEYWORD',
+ ' case': 'KEYWORD',
+ ' catch': 'KEYWORD',
+ ' continue': 'KEYWORD',
+ ' debugger': 'KEYWORD',
+ ' default': 'KEYWORD',
+ ' delete': 'KEYWORD',
+ ' do': 'KEYWORD',
+ ' else': 'KEYWORD',
+ ' finally': 'KEYWORD',
+ ' for': 'KEYWORD',
+ ' function': 'KEYWORD',
+ ' if': 'KEYWORD',
+ ' in': 'KEYWORD',
+ ' instanceof': 'KEYWORD',
+ ' new': 'KEYWORD',
+ ' return': 'KEYWORD',
+ ' switch': 'KEYWORD',
+ ' this': 'KEYWORD',
+ ' throw': 'KEYWORD',
+ ' try': 'KEYWORD',
+ ' typeof': 'KEYWORD',
+ ' var': 'KEYWORD',
+ ' void': 'KEYWORD',
+ ' while': 'KEYWORD',
+ ' with': 'KEYWORD',
+
+ ' false': 'BOOLEAN',
+ ' true': 'BOOLEAN',
+
+ ' null': 'NULL'
+};
+
+var makeSet = function (array) {
+ var s = {};
+ for (var i = 0, N = array.length; i < N; i++)
+ s[array[i]] = true;
+ return s;
+};
+
+var nonTokenTypes = makeSet('WHITESPACE COMMENT NEWLINE EOF ERROR'.split(' '));
+
+var punctuationBeforeDivision = makeSet('] ) } ++ --'.split(' '));
+var keywordsBeforeDivision = makeSet('this'.split(' '));
+
+var guessIsDivisionPermittedAfterToken = function (tok) {
+ // Figure out if a '/' character should be interpreted as division
+ // rather than the start of a regular expression when it follows the
+ // token, which must be a token lexeme per isToken().
+ // The beginning of section 7 of the spec briefly
+ // explains what's going on; basically the lexical grammar can't
+ // distinguish, for example, `e/f/g` (division) from `e=/f/g`
+ // (assignment of a regular expression), among many other variations.
+ //
+ // THIS IS ONLY A HEURISTIC, though it will rarely fail.
+ // Here are the two cases I know of where help from the parser is needed:
+ // - if (foo)
+ // /ba/.test("banana") && console.log("matches");
+ // (Close paren of a control structure before a statement starting with
+ // a regex literal. Starting a statement with a regex literal is
+ // unusual, of course, because it's hard to have a side effect.)
+ // - ++ /foo/.abc
+ // (Prefix `++` or `--` before an expression starting with a regex
+ // literal. This will run but I can't see any use for it.)
+ switch (tok.type()) {
+ case "PUNCTUATION":
+ // few punctuators can end an expression, but e.g. `)`
+ return !! punctuationBeforeDivision[tok.text()];
+ case "KEYWORD":
+ // few keywords can end an expression, but e.g. `this`
+ return !! keywordsBeforeDivision[tok.text()];
+ case "IDENTIFIER":
+ return true;
+ default: // literal
+ return true;
+ }
+};
+
+////////// PUBLIC API
+
+var Lexeme = function (pos, type, text) {
+ this._pos = pos;
+ this._type = type;
+ this._text = text;
+};
+
+Lexeme.prototype.startPos = function () {
+ return this._pos;
+};
+
+Lexeme.prototype.endPos = function () {
+ return this._pos + this._text.length;
+};
+
+Lexeme.prototype.type = function () {
+ return this._type;
+};
+
+Lexeme.prototype.text = function () {
+ return this._text;
+};
+
+Lexeme.prototype.isToken = function () {
+ return ! nonTokenTypes[this._type];
+};
+
+Lexeme.prototype.isError = function () {
+ return this._type === "ERROR";
+};
+
+Lexeme.prototype.isEOF = function () {
+ return this._type === "EOF";
+};
+
+Lexeme.prototype.prev = function () {
+ return this._prev;
+};
+
+Lexeme.prototype.next = function () {
+ return this._next;
+};
+
+Lexeme.prototype.toString = function () {
+ return this.isError() ? "ERROR" :
+ this.isEOF() ? "EOF" : "`" + this.text() + "`";
+};
+
+// Create a Lexer for the given string of JavaScript code.
+//
+// A lexer keeps a pointer `pos` into the string that is
+// advanced when you ask for the next lexeme with `next()`.
+//
+// XXXXX UPDATE DOCS
+// Properties:
+// code: Original JavaScript code string.
+// pos: Current index into the string. You can assign to it
+// to continue lexing from a different position. After
+// calling next(), it is the ending index of the most
+// recent lexeme.
+// lastPos: The starting index of the most recent lexeme.
+// Equal to `pos - text.length`.
+// text: Text of the last lexeme as a string.
+// type: Type of the last lexeme, as returned by `next()`.
+// divisionPermitted: Whether a '/' character should be interpreted
+// as division rather than the start of a regular expression.
+// This flag is set automatically during lexing based on the
+// previous token (i.e. the most recent token lexeme), but
+// it is technically only a heuristic.
+// Thie flag can be read and set manually to affect the
+// parsing of the next token.
+
+JSLexer = function (code) {
+ this.code = code;
+ this.pos = 0;
+ this.divisionPermitted = false;
+ this.lastLexeme = null;
+};
+
+JSLexer.Lexeme = Lexeme;
+
+// XXXX UPDATE DOCS
+// Return the type of the next of lexeme starting at `pos`, and advance
+// `pos` to the end of the lexeme. The text of the lexeme is available
+// in `text`. The text is always the substring of `code` between the
+// old and new values of `pos`. An "EOF" lexeme terminates
+// the stream. "ERROR" lexemes indicate a bad input string. Out of all
+// lexemes, only "EOF" has empty text, and it always has empty text.
+// All others contain at least one character from the source code.
+//
+// Lexeme types:
+// Literals: BOOLEAN, NULL, REGEX, NUMBER, STRING
+// Whitespace-like: WHITESPACE, COMMENT, NEWLINE, EOF
+// Other Tokens: IDENTIFIER, KEYWORD, PUNCTUATION
+// ... and ERROR
+
+JSLexer.prototype.next = function () {
+ var self = this;
+ var code = self.code;
+ var origPos = self.pos;
+ var divisionPermitted = self.divisionPermitted;
+
+ if (origPos > code.length)
+ throw new Error("out of range");
+
+ // Running regexes inside this function will move this local
+ // `pos` forward.
+ // When we commit to emitting a lexeme, we'll set self.pos
+ // based on it.
+ var pos = origPos;
+
+ // Emit a lexeme. Always called as `return lexeme(type)`.
+ var lexeme = function (type) {
+ // If `pos` hasn't moved, we consider this an error.
+ // This means that grammar cases that only run one regex
+ // or an alternation ('||') of regexes don't need to
+ // check for failure.
+ // This also guarantees that only EOF lexemes are empty.
+ if (pos === origPos && type !== 'EOF') {
+ type = 'ERROR';
+ pos = origPos + 1;
+ }
+ self.pos = pos;
+ var lex = new JSLexer.Lexeme(origPos, type, code.substring(origPos, pos));
+ if (self.lastLexeme) {
+ self.lastLexeme._next = lex;
+ lex._prev = self.lastLexeme;
+ }
+ self.lastLexeme = lex;
+ if (lex.isToken())
+ self.divisionPermitted = guessIsDivisionPermittedAfterToken(lex);
+ return lex;
+ };
+
+ if (pos === code.length)
+ return lexeme('EOF');
+
+ // Result of the regex match in the most recent call to `run`.
+ var match = null;
+
+ // Run a regex starting from `pos`, recording the end of the matched
+ // string in `pos` and the match data in `match`. The regex must have
+ // the 'g' (global) flag. If it doesn't match at `pos`, set `match`
+ // to null. The caller should expect the regex to match at `pos`, as
+ // failure is too expensive to run in a tight loop.
+ var run = function (regex) {
+ // Cause regex matching to start at `pos`.
+ regex.lastIndex = pos;
+ match = regex.exec(code);
+ // Simulate "sticky" matching by throwing out the match if it
+ // didn't match exactly at `pos`. If it didn't, we may have
+ // just searched the entire string.
+ if (match && (match.index !== pos))
+ match = null;
+ // Record the end position of the match back into `pos`.
+ if (match)
+ pos = regex.lastIndex;
+ return match;
+ };
+
+ // Decide which case of the grammar we are in based on one or two
+ // characters, then roll back `pos`.
+ run(rDecider);
+ pos = origPos;
+
+ // Grammar cases
+ if (match[1]) { // \s
+ run(rWhiteSpace);
+ return lexeme('WHITESPACE');
+ }
+ if (match[2]) { // one of //, /*, /
+ if (match[2] === '//') {
+ run(rSingleLineComment);
+ return lexeme('COMMENT');
+ }
+ if (match[2] === '/*') {
+ run(rMultiLineComment);
+ return lexeme(match ? 'COMMENT' : 'ERROR');
+ }
+ if (match[2] === '/') {
+ if (divisionPermitted) {
+ run(rDivPunctuator);
+ return lexeme('PUNCTUATION');
+ } else {
+ run(rRegexLiteral);
+ if (! match)
+ return lexeme('ERROR');
+ run(rRegexFlags);
+ return lexeme('REGEX');
+ }
+ }
+ }
+ if (match[3]) { // any other punctuation char
+ run(rPunctuator);
+ return lexeme(match ? 'PUNCTUATION' : 'ERROR');
+ }
+ if (match[4]) { // 0-9
+ run(rDecLiteral) || run(rHexLiteral) || run(rOctLiteral);
+ return lexeme(match ? 'NUMBER' : 'ERROR');
+ }
+ if (match[5]) { // " or '
+ run(rStringQuote);
+ var quote = match[0];
+ do {
+ run(rStringMiddle) || run(rEscapeSequence) || run(rStringQuote);
+ } while (match && match[0] !== quote);
+ if (! (match && match[0] === quote))
+ return lexeme('ERROR');
+ return lexeme('STRING');
+ }
+ if (match[7]) { // non-dot (line terminator)
+ run(rLineTerminator);
+ return lexeme('NEWLINE');
+ }
+ // dot (any non-line-terminator)
+ run(rIdentifierPrefix);
+ // Use non-short-circuiting OR, '|', to allow matching
+ // both regexes in sequence, returning false only if neither
+ // matched.
+ while (run(rIdentifierMiddle) | run(rIdentifierPrefix)) {/*continue*/}
+ var word = code.substring(origPos, pos);
+ return lexeme(keywordLookup[' '+word] || 'IDENTIFIER');
+};
+
+})();
diff --git a/packages/jsparse/package.js b/packages/jsparse/package.js
new file mode 100644
index 0000000000..3cb8c685a7
--- /dev/null
+++ b/packages/jsparse/package.js
@@ -0,0 +1,20 @@
+Package.describe({
+ summary: "Full-featured JavaScript parser",
+ internal: true
+});
+
+Package.on_use(function (api) {
+ api.add_files(['lexer.js', 'parserlib.js', 'stringify.js', 'parser.js'],
+ ['client', 'server']);
+});
+
+Package.on_test(function (api) {
+ api.use('tinytest');
+ api.use('jsparse', 'client');
+
+ api.add_files('parser_tests.js',
+ // Test just on client for faster running; should run
+ // identically on server.
+ 'client');
+ //['client', 'server']);
+});
diff --git a/packages/jsparse/parser.js b/packages/jsparse/parser.js
new file mode 100644
index 0000000000..a2021f3298
--- /dev/null
+++ b/packages/jsparse/parser.js
@@ -0,0 +1,761 @@
+///// JAVASCRIPT PARSER
+
+// What we don't support from ECMA-262 5.1:
+// - object literal trailing comma
+// - object literal get/set
+
+(function () {
+
+var expecting = Parser.expecting;
+
+var assertion = Parsers.assertion;
+var node = Parsers.node;
+var or = Parsers.or;
+var and = Parsers.and;
+var not = Parsers.not;
+var list = Parsers.list;
+var seq = Parsers.seq;
+var opt = Parsers.opt;
+var constant = Parsers.constant;
+var mapResult = Parsers.mapResult;
+
+
+var makeSet = function (array) {
+ var s = {};
+ for (var i = 0, N = array.length; i < N; i++)
+ s[array[i]] = true;
+ return s;
+};
+
+
+JSParser = function (code, options) {
+ this.lexer = new JSLexer(code);
+ this.oldToken = null;
+ this.newToken = null;
+ this.pos = 0;
+ this.isLineTerminatorHere = false;
+
+ options = options || {};
+ // pass {tokens:'strings'} to get strings for
+ // tokens instead of token objects
+ if (options.tokens === 'strings') {
+ this.tokenFunc = function (tok) {
+ return tok.text();
+ };
+ } else {
+ this.tokenFunc = function (tok) {
+ return tok;
+ };
+ }
+
+ this.consumeNewToken();
+};
+
+JSParser.prototype.consumeNewToken = function () {
+ var self = this;
+ var lexer = self.lexer;
+ self.oldToken = self.newToken;
+ self.isLineTerminatorHere = false;
+ var lex;
+ do {
+ lex = lexer.next();
+ if (lex.isError())
+ throw new Error("Bad token at position " + lex.startPos() +
+ ", text `" + lex.text() + "`");
+ else if (lex.type() === "NEWLINE")
+ self.isLineTerminatorHere = true;
+ else if (lex.type() === "COMMENT" && ! /^.*$/.test(lex.text()))
+ // multiline comments containing line terminators count
+ // as line terminators.
+ self.isLineTerminatorHere = true;
+ } while (! lex.isEOF() && ! lex.isToken());
+ self.newToken = lex;
+ self.pos = lex.startPos();
+};
+
+JSParser.prototype.getParseError = function (expecting, found) {
+ var msg = (expecting ? "Expected " + expecting : "Unexpected token");
+ if (this.oldToken)
+ msg += " after " + this.oldToken;
+ var pos = this.pos;
+ msg += " at position " + pos;
+ msg += ", found " + (found || this.newToken);
+ return new Error(msg);
+};
+
+JSParser.prototype.getSyntaxTree = function () {
+ var self = this;
+
+ var NIL = new ParseNode('nil', []);
+
+ var booleanFlaggedParser = function (parserConstructFunc) {
+ return {
+ false: parserConstructFunc(false),
+ true: parserConstructFunc(true)
+ };
+ };
+
+ // Takes a space-separated list of either punctuation or keyword tokens
+ var lookAheadToken = function (tokens) {
+ var type = (/\w/.test(tokens) ? 'KEYWORD' : 'PUNCTUATION');
+ var textSet = makeSet(tokens.split(' '));
+ return expecting(
+ tokens.split(' ').join(', '),
+ assertion(function (t) {
+ return (t.newToken.type() === type && textSet[t.newToken.text()]);
+ }));
+ };
+
+ var lookAheadTokenType = function (type) {
+ return expecting(type, assertion(function (t) {
+ return t.newToken.type() === type;
+ }));
+ };
+
+ // Takes a space-separated list of either punctuation or keyword tokens
+ var token = function (tokens) {
+ var type = (/\w/.test(tokens) ? 'KEYWORD' : 'PUNCTUATION');
+ var textSet = makeSet(tokens.split(' '));
+ return new Parser(
+ tokens.split(' ').join(', '),
+ function (t) {
+ if (t.newToken.type() === type && textSet[t.newToken.text()]) {
+ t.consumeNewToken();
+ return self.tokenFunc(t.oldToken);
+ }
+ return null;
+ });
+ };
+
+ var tokenType = function (type) {
+ return new Parser(type, function (t) {
+ if (t.newToken.type() === type) {
+ t.consumeNewToken();
+ return self.tokenFunc(t.oldToken);
+ }
+ return null;
+ });
+ };
+
+ var noLineTerminatorHere = expecting(
+ 'noLineTerminator', assertion(function (t) {
+ return ! t.isLineTerminatorHere;
+ }));
+
+ var nonLHSExpressionNames = makeSet(
+ 'unary binary postfix ternary assignment comma'.split(' '));
+ var isExpressionLHS = function (exprNode) {
+ return ! nonLHSExpressionNames[exprNode.name];
+ };
+
+ // Like token, but marks tokens that need to defy the lexer's
+ // heuristic about whether the next '/' is a division or
+ // starts a regex.
+ var preSlashToken = function (text, divisionNotRegex) {
+ var inner = token(text);
+ return new Parser(
+ inner.expecting,
+ function (t) {
+ // temporarily set divisionPermitted,
+ // restoring it if we don't match.
+ var oldValue = t.lexer.divisionPermitted;
+ var result;
+ try {
+ t.lexer.divisionPermitted = divisionNotRegex;
+ result = inner.parse(t);
+ return result;
+ } finally {
+ if (! result)
+ t.lexer.divisionPermitted = oldValue;
+ }
+ });
+ };
+
+ // Mark some productions "lazy" to allow grammar circularity, i.e. accessing
+ // later parsers from earlier ones.
+ // These lazy versions will be replaced with real ones, which they will
+ // access when run.
+ var expressionMaybeNoIn = {
+ 'false': Parsers.lazy(
+ 'expression',
+ function () { return expressionMaybeNoIn[false]; }),
+ 'true': Parsers.lazy(
+ 'expression',
+ function () { return expressionMaybeNoIn[true]; })
+ };
+ var expression = expressionMaybeNoIn[false];
+
+ var assignmentExpressionMaybeNoIn = {
+ 'false': Parsers.lazy(
+ 'expression',
+ function () { return assignmentExpressionMaybeNoIn[false]; }),
+ 'true': Parsers.lazy(
+ 'expression',
+ function () { return assignmentExpressionMaybeNoIn[true]; })
+ };
+ var assignmentExpression = assignmentExpressionMaybeNoIn[false];
+
+ var functionBody = Parsers.lazy(
+ 'statement', function () { return functionBody; });
+ var statement = Parsers.lazy(
+ 'statement', function () { return statement; });
+ ////
+
+ var arrayLiteral =
+ node('array',
+ seq(token('['),
+ opt(list(token(','))),
+ or(
+ lookAheadToken(']'),
+ list(
+ expecting(
+ 'expression',
+ or(assignmentExpression,
+ // count a peeked-at ']' as an expression
+ // to support elisions at end, e.g.
+ // `[1,2,3,,,,,,]`.
+ lookAheadToken(']'))),
+ // list seperator is one or more commas
+ // to support elision
+ list(token(',')))),
+ token(']')));
+
+ var propertyName = expecting('propertyName', or(
+ node('idPropName', tokenType('IDENTIFIER')),
+ node('numPropName', tokenType('NUMBER')),
+ node('strPropName', tokenType('STRING'))));
+ var nameColonValue = expecting(
+ 'propertyName',
+ node('prop', seq(propertyName, token(':'), assignmentExpression)));
+
+ var objectLiteral =
+ node('object',
+ seq(token('{'),
+ or(lookAheadToken('}'),
+ list(nameColonValue,
+ token(','))),
+ token('}')));
+
+ var functionMaybeNameRequired = booleanFlaggedParser(
+ function (nameRequired) {
+ return seq(token('function'),
+ (nameRequired ? tokenType('IDENTIFIER') :
+ or(tokenType('IDENTIFIER'),
+ and(lookAheadToken('('), constant(NIL)))),
+ token('('),
+ or(lookAheadToken(')'),
+ list(tokenType('IDENTIFIER'), token(','))),
+ token(')'),
+ token('{'),
+ functionBody,
+ token('}'));
+ });
+ var functionExpression = node('functionExpr',
+ functionMaybeNameRequired[false]);
+
+ var primaryOrFunctionExpression =
+ expecting('expression',
+ or(node('this', token('this')),
+ node('identifier', tokenType('IDENTIFIER')),
+ node('number', tokenType('NUMBER')),
+ node('boolean', tokenType('BOOLEAN')),
+ node('null', tokenType('NULL')),
+ node('regex', tokenType('REGEX')),
+ node('string', tokenType('STRING')),
+ node('parens',
+ seq(token('('), expression, token(')'))),
+ arrayLiteral,
+ objectLiteral,
+ functionExpression));
+
+ var dotEnding = seq(token('.'), tokenType('IDENTIFIER'));
+ var bracketEnding = seq(token('['), expression, token(']'));
+ var callArgs = seq(token('('),
+ or(lookAheadToken(')'),
+ list(assignmentExpression,
+ token(','))),
+ token(')'));
+
+ var newKeyword = token('new');
+
+ // This is a completely equivalent refactor of the spec's production
+ // for a LeftHandSideExpression.
+ //
+ // An lhsExpression is basically an expression that can serve as
+ // the left-hand-side of an assignment, though function calls and
+ // "new" invocation are included because they have the same
+ // precedence. Actually, the spec technically allows a function
+ // call to "return" a valid l-value, as in `foo(bar) = baz`,
+ // though no built-in or user-specifiable call has this property
+ // (it would have to be defined by a browser or other "host").
+ var lhsExpression = new Parser(
+ 'expression',
+ function (t) {
+ // Accumulate all initial "new" keywords, not yet knowing
+ // if they have a corresponding argument list later.
+ var news = [];
+ var n;
+ while ((n = newKeyword.parse(t)))
+ news.push(n);
+
+ // Read the primaryOrFunctionExpression that will be the "core"
+ // of this lhsExpression. It is preceded by zero or more `new`
+ // keywords, and followed by any sequence of (...), [...],
+ // and .foo add-ons.
+ // if we have 'new' keywords, we are committed and must
+ // match an expression or error.
+ var result = primaryOrFunctionExpression.parseRequiredIf(t, news.length);
+ if (! result)
+ return null;
+
+ // Our plan of attack is to apply each dot, bracket, or call
+ // as we come across it. Whether a call is a `new` call depends
+ // on whether there are `new` keywords we haven't used. If so,
+ // we pop one off the stack.
+ var done = false;
+ while (! done) {
+ var r;
+ if ((r = dotEnding.parse(t))) {
+ result = new ParseNode('dot', [result].concat(r));
+ } else if ((r = bracketEnding.parse(t))) {
+ result = new ParseNode('bracket', [result].concat(r));
+ } else if ((r = callArgs.parse(t))) {
+ if (news.length)
+ result = new ParseNode('newcall', [news.pop(), result].concat(r));
+ else
+ result = new ParseNode('call', [result].concat(r));
+ } else {
+ done = true;
+ }
+ }
+
+ // There may be more `new` keywords than calls, which is how
+ // paren-less constructions (`new Date`) are parsed. We've
+ // already handled `new foo().bar()`, now handle `new new foo().bar`.
+ while (news.length)
+ result = new ParseNode('new', [news.pop(), result]);
+
+ return result;
+ });
+
+ var postfixToken = token('++ --');
+ var postfixLookahead = lookAheadToken('++ --');
+ var postfixExpression = expecting(
+ 'expression',
+ mapResult(seq(lhsExpression,
+ opt(and(noLineTerminatorHere,
+ postfixLookahead,
+ postfixToken))),
+ function (v) {
+ if (v.length === 1)
+ return v[0];
+ return new ParseNode('postfix', v);
+ }));
+
+ var unaryExpression = Parsers.unary(
+ 'unary', postfixExpression,
+ or(token('delete void typeof'),
+ preSlashToken('++ -- + - ~ !', false)));
+
+ // The "noIn" business is all to facilitate parsing
+ // of for-in constructs, though the cases that make
+ // this required are quite obscure.
+ // The `for(var x in y)` form is allowed to take
+ // an initializer for `x` (which is only useful for
+ // its side effects, or if `y` has no properties).
+ // So an example might be:
+ // `for(var x = a().b in c);`
+ // In this example, `var x = a().b` is parsed without
+ // the `in`, which would otherwise be part of the
+ // varDecl, using varDeclNoIn.
+
+ // Our binaryExpression is the spec's LogicalORExpression,
+ // which includes all the higher-precendence operators.
+ var binaryExpressionMaybeNoIn = booleanFlaggedParser(
+ function (noIn) {
+ // high to low precedence
+ var binaryOps = [token('* / %'),
+ token('+ -'),
+ token('<< >> >>>'),
+ or(token('< > <= >='),
+ noIn ? token('instanceof') :
+ token('instanceof in')),
+ token('== != === !=='),
+ token('&'),
+ token('^'),
+ token('|'),
+ token('&&'),
+ token('||')];
+ return expecting(
+ 'expression',
+ Parsers.binaryLeft('binary', unaryExpression, binaryOps));
+ });
+ var binaryExpression = binaryExpressionMaybeNoIn[false];
+
+ var conditionalExpressionMaybeNoIn = booleanFlaggedParser(
+ function (noIn) {
+ return expecting(
+ 'expression',
+ mapResult(
+ seq(binaryExpressionMaybeNoIn[noIn],
+ opt(seq(
+ token('?'),
+ assignmentExpression, token(':'),
+ assignmentExpressionMaybeNoIn[noIn]))),
+ function (v) {
+ if (v.length === 1)
+ return v[0];
+ return new ParseNode('ternary', v);
+ }));
+ });
+ var conditionalExpression = conditionalExpressionMaybeNoIn[false];
+
+ var assignOp = token('= *= /= %= += -= <<= >>= >>>= &= ^= |=');
+
+ assignmentExpressionMaybeNoIn = booleanFlaggedParser(
+ function (noIn) {
+ return new Parser(
+ 'expression',
+ function (t) {
+ var r = conditionalExpressionMaybeNoIn[noIn].parse(t);
+ if (! r)
+ return null;
+
+ // Assignment is right-associative.
+ // Plan of attack: make a list of all the parts
+ // [expression, op, expression, op, ... expression]
+ // and then fold them up at the end.
+ var parts = [r];
+ var op;
+ while (isExpressionLHS(r) &&(op = assignOp.parse(t)))
+ parts.push(op,
+ conditionalExpressionMaybeNoIn[noIn].parseRequired(t));
+
+ var result = parts.pop();
+ while (parts.length) {
+ op = parts.pop();
+ var lhs = parts.pop();
+ result = new ParseNode('assignment', [lhs, op, result]);
+ }
+ return result;
+ });
+ });
+ assignmentExpression = assignmentExpressionMaybeNoIn[false];
+
+ expressionMaybeNoIn = booleanFlaggedParser(
+ function (noIn) {
+ return expecting(
+ 'expression',
+ mapResult(
+ list(assignmentExpressionMaybeNoIn[noIn], token(',')),
+ function (v) {
+ if (v.length === 1)
+ return v[0];
+ return new ParseNode('comma', v);
+ }));
+ });
+ expression = expressionMaybeNoIn[false];
+
+ // STATEMENTS
+
+ var statements = list(statement);
+
+ // implements JavaScript's semicolon "insertion" rules
+ var maybeSemicolon = expecting(
+ 'semicolon',
+ or(token(';'),
+ and(
+ or(
+ lookAheadToken('}'),
+ lookAheadTokenType('EOF'),
+ assertion(function (t) {
+ return t.isLineTerminatorHere;
+ })),
+ constant(new ParseNode(';', [])))));
+
+ var expressionStatement = node(
+ 'expressionStmnt',
+ and(
+ not(or(lookAheadToken('{'), lookAheadToken('function'))),
+ seq(expression,
+ expecting('semicolon',
+ or(maybeSemicolon,
+ // allow presence of colon to terminate
+ // statement legally, for the benefit of
+ // expressionOrLabelStatement. Basically assume
+ // an implicit semicolon. This
+ // is safe because a colon can never legally
+ // follow a semicolon anyway.
+ and(lookAheadToken(':'),
+ constant(new ParseNode(';', []))))))));
+
+ // it's hard to parse statement labels, as in
+ // `foo: x = 1`, because we can't tell from the
+ // first token whether we are looking at an expression
+ // statement or a label statement. To work around this,
+ // expressionOrLabelStatement parses the expression and
+ // then rewrites the result if it is an identifier
+ // followed by a colon.
+ var labelColonAndStatement = seq(token(':'), statement);
+ var noColon = expecting(
+ 'semicolon', not(lookAheadToken(':')));
+ var expressionOrLabelStatement = new Parser(
+ null,
+ function (t) {
+ var exprStmnt = expressionStatement.parse(t);
+ if (! exprStmnt)
+ return null;
+
+ var expr = exprStmnt.children[0];
+ var maybeSemi = exprStmnt.children[1];
+ if (expr.name !== 'identifier' ||
+ ! (maybeSemi instanceof ParseNode)) {
+ // We either have a non-identifier expression or a present
+ // semicolon. This is not a label.
+ //
+ // Fail now if we are looking at a colon, causing an
+ // error message on input like `1+1:` of the same kind
+ // you'd get without statement label parsing.
+ noColon.parseRequired(t);
+ return exprStmnt;
+ }
+
+ var rest = labelColonAndStatement.parse(t);
+ if (! rest)
+ return exprStmnt;
+
+ return new ParseNode('labelStmnt',
+ [expr.children[0]].concat(rest));
+ });
+
+ var emptyStatement = node('emptyStmnt', token(';')); // required semicolon
+
+ var blockStatement = expecting('block', node('blockStmnt', seq(
+ token('{'), or(lookAheadToken('}'), statements),
+ token('}'))));
+
+ var varDeclMaybeNoIn = booleanFlaggedParser(function (noIn) {
+ return node(
+ 'varDecl',
+ seq(tokenType('IDENTIFIER'),
+ opt(seq(token('='),
+ assignmentExpressionMaybeNoIn[noIn]))));
+ });
+ var varDecl = varDeclMaybeNoIn[false];
+
+ var variableStatement = node(
+ 'varStmnt',
+ seq(token('var'), list(varDecl, token(',')),
+ maybeSemicolon));
+
+ // A paren that may be followed by a statement
+ // beginning with a regex literal.
+ var closeParenBeforeStatement = preSlashToken(')', false);
+
+ var ifStatement = node(
+ 'ifStmnt',
+ seq(token('if'), token('('), expression,
+ closeParenBeforeStatement, statement,
+ opt(seq(token('else'), statement))));
+
+ var secondThirdClauses = expecting(
+ 'semicolon',
+ and(lookAheadToken(';'),
+ seq(
+ expecting('semicolon', token(';')),
+ or(and(lookAheadToken(';'),
+ constant(NIL)),
+ expression),
+ expecting('semicolon', token(';')),
+ or(and(lookAheadToken(')'),
+ constant(NIL)),
+ expression))));
+ var inExpr = seq(token('in'), expression);
+ var inExprExpectingSemi = expecting('semicolon',
+ seq(token('in'), expression));
+ var forSpec = mapResult(node(
+ 'forSpec',
+ or(seq(token('var'),
+ varDeclMaybeNoIn[true],
+ expecting(
+ 'commaOrIn',
+ or(inExpr,
+ seq(
+ or(
+ lookAheadToken(';'),
+ seq(token(','),
+ list(varDeclMaybeNoIn[true], token(',')))),
+ secondThirdClauses)))),
+ // get the case where the first clause is empty out of the way.
+ // the lookAhead's return value is the empty placeholder for the
+ // missing expression.
+ seq(and(lookAheadToken(';'),
+ constant(NIL)), secondThirdClauses),
+ // custom parser the non-var case because we have to
+ // read the first expression before we know if there's
+ // an "in".
+ new Parser(
+ null,
+ function (t) {
+ var firstExpr = expressionMaybeNoIn[true].parse(t);
+ if (! firstExpr)
+ return null;
+ var rest = secondThirdClauses.parse(t);
+ if (! rest) {
+ // we need a left-hand-side expression for a
+ // `for (x in y)` loop.
+ if (! isExpressionLHS(firstExpr))
+ throw t.getParseError("semicolon");
+ // if we don't see 'in' at this point, it's probably
+ // a missing semicolon
+ rest = inExprExpectingSemi.parseRequired(t);
+ }
+
+ return [firstExpr].concat(rest);
+ }))),
+ function (clauses) {
+ // There are four kinds of for-loop, and we call the
+ // part between the parens one of forSpec, forVarSpec,
+ // forInSpec, and forVarInSpec. Having parsed it
+ // already, we rewrite the node name based on how
+ // many items came out. forIn and forVarIn always
+ // have 3 and 4 items respectively. for has 5
+ // (the optional expressions are present as nils).
+ // forVar has 6 or more, because `for(var x;;);`
+ // produces [`var` `x` `;` nil `;` nil].
+ var numChildren = clauses.children.length;
+ if (numChildren === 3)
+ return new ParseNode('forInSpec', clauses.children);
+ else if (numChildren === 4)
+ return new ParseNode('forVarInSpec', clauses.children);
+ else if (numChildren >= 6)
+ return new ParseNode('forVarSpec', clauses.children);
+ return clauses;
+ });
+
+ var iterationStatement = or(
+ node('doStmnt', seq(token('do'), statement, token('while'),
+ token('('), expression, token(')'),
+ maybeSemicolon)),
+ node('whileStmnt', seq(token('while'), token('('), expression,
+ closeParenBeforeStatement, statement)),
+ // semicolons must be real, not maybeSemicolons
+ node('forStmnt', seq(
+ token('for'), token('('), forSpec, closeParenBeforeStatement,
+ statement)));
+
+ var returnStatement = node(
+ 'returnStmnt',
+ seq(token('return'), or(
+ and(noLineTerminatorHere, expression), constant(NIL)),
+ maybeSemicolon));
+ var continueStatement = node(
+ 'continueStmnt',
+ seq(token('continue'), or(
+ and(noLineTerminatorHere, tokenType('IDENTIFIER')), constant(NIL)),
+ maybeSemicolon));
+ var breakStatement = node(
+ 'breakStmnt',
+ seq(token('break'), or(
+ and(noLineTerminatorHere, tokenType('IDENTIFIER')), constant(NIL)),
+ maybeSemicolon));
+ var throwStatement = node(
+ 'throwStmnt',
+ seq(token('throw'),
+ and(or(noLineTerminatorHere,
+ // If there is a line break here and more tokens after,
+ // we want to error appropriately. `throw \n e` should
+ // complain about the "end of line", not the `e`.
+ and(not(lookAheadTokenType("EOF")),
+ new Parser(null,
+ function (t) {
+ throw t.getParseError('expression', 'end of line');
+ }))),
+ expression),
+ maybeSemicolon));
+
+ var withStatement = node(
+ 'withStmnt',
+ seq(token('with'), token('('), expression, closeParenBeforeStatement,
+ statement));
+
+ var switchCase = node(
+ 'case',
+ seq(token('case'), expression, token(':'),
+ or(lookAheadToken('}'),
+ lookAheadToken('case default'),
+ statements)));
+ var switchDefault = node(
+ 'default',
+ seq(token('default'), token(':'),
+ or(lookAheadToken('}'),
+ lookAheadToken('case'),
+ statements)));
+
+ var switchStatement = node(
+ 'switchStmnt',
+ seq(token('switch'), token('('), expression, token(')'),
+ token('{'),
+ or(lookAheadToken('}'),
+ lookAheadToken('default'),
+ list(switchCase)),
+ opt(seq(switchDefault,
+ opt(list(switchCase)))),
+ token('}')));
+
+ var catchFinally = expecting(
+ 'catch',
+ and(lookAheadToken('catch finally'),
+ seq(
+ or(node(
+ 'catch',
+ seq(token('catch'), token('('), tokenType('IDENTIFIER'),
+ token(')'), blockStatement)),
+ constant(NIL)),
+ or(node(
+ 'finally',
+ seq(token('finally'), blockStatement)),
+ constant(NIL)))));
+ var tryStatement = node(
+ 'tryStmnt',
+ seq(token('try'), blockStatement, catchFinally));
+ var debuggerStatement = node(
+ 'debuggerStmnt', seq(token('debugger'), maybeSemicolon));
+
+ statement = expecting('statement',
+ or(expressionOrLabelStatement,
+ emptyStatement,
+ blockStatement,
+ variableStatement,
+ ifStatement,
+ iterationStatement,
+ returnStatement,
+ continueStatement,
+ breakStatement,
+ withStatement,
+ switchStatement,
+ throwStatement,
+ tryStatement,
+ debuggerStatement));
+
+ // PROGRAM
+
+ var functionDecl = node(
+ 'functionDecl', functionMaybeNameRequired[true]);
+
+ var sourceElement = or(functionDecl, statement);
+ var sourceElements = list(sourceElement);
+
+ functionBody = expecting(
+ 'functionBody', or(lookAheadToken('}'), sourceElements));
+
+ var program = node(
+ 'program',
+ seq(opt(sourceElements),
+ // If not at EOF, complain "expecting statement"
+ expecting('statement', lookAheadTokenType("EOF"))));
+
+ return program.parse(this);
+};
+
+})();
\ No newline at end of file
diff --git a/packages/jsparse/parser_tests.js b/packages/jsparse/parser_tests.js
new file mode 100644
index 0000000000..4973395329
--- /dev/null
+++ b/packages/jsparse/parser_tests.js
@@ -0,0 +1,607 @@
+
+var allNodeNames = [
+ ";",
+ "array",
+ "assignment",
+ "binary",
+ "blockStmnt",
+ "boolean",
+ "bracket",
+ "breakStmnt",
+ "call",
+ "case",
+ "catch",
+ "comma",
+ "continueStmnt",
+ "debuggerStmnt",
+ "default",
+ "doStmnt",
+ "dot",
+ "emptyStmnt",
+ "expressionStmnt",
+ "finally",
+ "forInSpec",
+ "forSpec",
+ "forStmnt",
+ "forVarInSpec",
+ "forVarSpec",
+ "functionDecl",
+ "functionExpr",
+ "idPropName",
+ "identifier",
+ "ifStmnt",
+ "labelStmnt",
+ "new",
+ "newcall",
+ "nil",
+ "null",
+ "numPropName",
+ "number",
+ "object",
+ "parens",
+ "postfix",
+ "program",
+ "prop",
+ "regex",
+ "returnStmnt",
+ "strPropName",
+ "string",
+ "switchStmnt",
+ "ternary",
+ "this",
+ "throwStmnt",
+ "tryStmnt",
+ "unary",
+ "varDecl",
+ "varStmnt",
+ "whileStmnt",
+ "withStmnt"
+];
+
+var allNodeNamesSet = {};
+_.each(allNodeNames, function (n) { allNodeNamesSet[n] = true; });
+
+
+var makeTester = function (test) {
+ return {
+ // Parse code and make sure it matches expectedTreeString.
+ goodParse: function (code, expectedTreeString, regexTokenHints) {
+ var expectedTree = ParseNode.unstringify(expectedTreeString);
+
+ // first use lexer to collect all tokens
+ var lexer = new JSLexer(code);
+ var allTokensInOrder = [];
+ while (! lexer.next().isEOF()) {
+ var lex = lexer.lastLexeme;
+ if (lex.isError())
+ test.fail("Lexer error at " + lex.startPos());
+ if (lex.isToken())
+ allTokensInOrder.push(lex);
+ if (regexTokenHints && regexTokenHints[allTokensInOrder.length])
+ lexer.divisionPermitted = false;
+ }
+
+ var parser = new JSParser(code);
+ var actualTree = parser.getSyntaxTree();
+
+ var nextTokenIndex = 0;
+ var check = function (tree) {
+ if (tree instanceof ParseNode) {
+ // This is a NODE (non-terminal).
+ var nodeName = tree.name;
+ if (! (nodeName && typeof nodeName === "string" &&
+ allNodeNamesSet[nodeName] === true))
+ test.fail("Not a node name: " + nodeName);
+ _.each(tree.children, check);
+ } else if (typeof tree === 'object' &&
+ typeof tree.text === 'function') {
+ // This is a TOKEN (terminal).
+ // Make sure we are visiting every token once, in order.
+ if (nextTokenIndex >= allTokensInOrder.length)
+ test.fail("Too many tokens: " + (nextTokenIndex + 1));
+ var referenceToken = allTokensInOrder[nextTokenIndex++];
+ if (tree.text() !== referenceToken.text())
+ test.fail(tree.text() + " !== " + referenceToken.text());
+ if (tree.startPos() !== referenceToken.startPos())
+ test.fail(tree.startPos() + " !== " + referenceToken.startPos());
+ if (code.substring(tree.startPos(), tree.endPos()) !== tree.text())
+ test.fail("Didn't see " + tree.text() + " at " + tree.startPos() +
+ " in " + code);
+ } else {
+ test.fail("Unknown tree part: " + tree);
+ }
+ };
+
+ check(actualTree);
+ if (nextTokenIndex !== allTokensInOrder.length)
+ test.fail("Too few tokens: " + nextTokenIndex);
+
+ test.equal(parser.pos, code.length);
+
+ test.equal(ParseNode.stringify(actualTree),
+ ParseNode.stringify(expectedTree), code);
+ },
+ // Takes code with part of it surrounding with backticks.
+ // Removes the two backtick characters, tries to parse the code,
+ // and then asserts that there was a tokenization-level error,
+ // with the part that was between the backticks called out as
+ // the bad token.
+ //
+ // For example, the test "123`@`" will try to parse "123@" and
+ // assert that a tokenization error occurred at '@'.
+ badToken: function (code) {
+ var constructMessage = function (pos, text) {
+ return "Bad token at position " + pos + ", text `" + text + "`";
+ };
+ var pos = code.indexOf('`');
+ var text = code.match(/`(.*?)`/)[1];
+ code = code.replace(/`/g, '');
+
+ var parsed = false;
+ var error = null;
+ try {
+ var tree = new JSParser(code).getSyntaxTree();
+ parsed = true;
+ } catch (e) {
+ error = e;
+ }
+ test.isFalse(parsed);
+ test.isTrue(error);
+ test.equal(error.message, constructMessage(pos, text));
+ },
+ // Takes code with a backtick-quoted string embedded in it.
+ // Removes the backticks and their contents, tries to parse the code,
+ // and then asserts that there was a parse error at the location
+ // where the backtick-quoted string was embedded. The embedded
+ // string must match whatever the error message says was "expected".
+ //
+ // For example, the test "{`statement`" will try to parse the code
+ // "{" and then assert that an error occured at the end of the string
+ // saying "Expected statement". The test "1 `semicolon`2" will try
+ // to parse "1 2" and assert that the error "Expected semicolon"
+ // appeared after the space and before the 2.
+ //
+ // A second backtick-quoted string is used as the "found" token
+ // in the error message.
+ badParse: function (code) {
+ var constructMessage = function (whatExpected, pos, found, after) {
+ return "Expected " + whatExpected + (after ? " after " + after : "") +
+ " at position " + pos + ", found " + found;
+ };
+ var pos = code.indexOf('`');
+
+ var backticked = code.match(/`.*?`/g);
+ var whatExpected = backticked[0] && backticked[0].slice(1,-1);
+ var found = backticked[1] && backticked[1].slice(1, -1);
+ code = code.replace(/`.*?`/g, '');
+
+ var parsed = false;
+ var error = null;
+ var parser = new JSParser(code);
+ try {
+ var tree = parser.getSyntaxTree();
+ parsed = true;
+ } catch (e) {
+ error = e;
+ }
+ test.isFalse(parsed);
+ test.isTrue(error);
+ if (! parsed && error) {
+ var after = parser.oldToken;
+ found = (found || parser.newToken);
+ test.equal(error.message,
+ constructMessage(whatExpected, pos, found, after));
+ }
+ }
+ };
+};
+
+
+Tinytest.add("jsparse - basics", function (test) {
+ var tester = makeTester(test);
+ tester.goodParse('1', "program(expressionStmnt(number(1) ;()))");
+ tester.goodParse('1 + 1', "program(expressionStmnt(binary(number(1) + number(1)) ;()))");
+ tester.goodParse('1*2+3*4', "program(expressionStmnt(binary(binary(number(1) * number(2)) + " +
+ "binary(number(3) * number(4))) ;()))");
+ tester.goodParse('1 + 1;', "program(expressionStmnt(binary(number(1) + number(1)) ;))");
+ tester.goodParse('1 + 1;;', "program(expressionStmnt(binary(number(1) + number(1)) ;) emptyStmnt(;))");
+ tester.goodParse('', "program()");
+ tester.goodParse('\n', "program()");
+ tester.goodParse(';;;\n\n;\n', "program(emptyStmnt(;) emptyStmnt(;) emptyStmnt(;) emptyStmnt(;))");
+ tester.goodParse('foo', "program(expressionStmnt(identifier(foo) ;()))");
+ tester.goodParse('foo();', "program(expressionStmnt(call(identifier(foo) `(` `)`) ;))");
+ tester.goodParse('var x = 3', "program(varStmnt(var varDecl(x = number(3)) ;()))");
+ tester.goodParse('++x;', "program(expressionStmnt(unary(++ identifier(x)) ;))");
+ tester.goodParse('x++;', "program(expressionStmnt(postfix(identifier(x) ++) ;))");
+ tester.goodParse(
+ 'throw new Error',
+ "program(throwStmnt(throw new(new identifier(Error)) ;()))");
+ tester.goodParse(
+ 'var x = function () { return 123; };',
+ 'program(varStmnt(var varDecl(x = functionExpr(function nil() `(` `)` ' +
+ '{ returnStmnt(return number(123) ;) })) ;))');
+
+ tester.badParse("var x = `expression`");
+ tester.badParse("1 `semicolon`1");
+ tester.badParse("1+1`semicolon`:");
+});
+
+Tinytest.add("jsparse - tokenization errors", function (test) {
+ var tester = makeTester(test);
+ tester.badToken("123`@`");
+ tester.badToken("thisIsATestOf = `'unterminated `\n strings'");
+});
+
+Tinytest.add("jsparse - syntax forms", function (test) {
+ var tester = makeTester(test);
+ var trials = [
+ // STATEMENTS
+ ['1',
+ 'program(expressionStmnt(number(1) ;()))'],
+ ['1;;;;2',
+ 'program(expressionStmnt(number(1) ;) emptyStmnt(;) emptyStmnt(;) emptyStmnt(;) ' +
+ 'expressionStmnt(number(2) ;()))'],
+ ['{}',
+ 'program(blockStmnt({ }))'],
+ ['{null}',
+ 'program(blockStmnt({ expressionStmnt(null(null) ;()) }))'],
+ ['{\nfoo()\nbar();\n}',
+ 'program(blockStmnt({ expressionStmnt(call(identifier(foo) `(` `)`) ;()) ' +
+ 'expressionStmnt(call(identifier(bar) `(` `)`) ;) }))'],
+ ['{{{}}}',
+ 'program(blockStmnt({ blockStmnt({ blockStmnt({ }) }) }))'],
+ ['var x = y, z,\n a = b = c;',
+ 'program(varStmnt(var varDecl(x = identifier(y)) , varDecl(z) , varDecl(a = ' +
+ 'assignment(identifier(b) = identifier(c))) ;))'],
+ ['if (x === y);',
+ 'program(ifStmnt(if `(` binary(identifier(x) === identifier(y)) `)` emptyStmnt(;)))'],
+ ['if (z) return',
+ 'program(ifStmnt(if `(` identifier(z) `)` returnStmnt(return nil() ;())))'],
+ ['if (a) b; else c',
+ 'program(ifStmnt(if `(` identifier(a) `)` expressionStmnt(identifier(b) ;) else ' +
+ 'expressionStmnt(identifier(c) ;())))'],
+ ['if (n === 1) { foo(); } else if (n === 2) { bar(); } else { baz(); }',
+ 'program(ifStmnt(if `(` binary(identifier(n) === number(1)) `)` blockStmnt(' +
+ '{ expressionStmnt(call(identifier(foo) `(` `)`) ;) }) else ifStmnt(' +
+ 'if `(` binary(identifier(n) === number(2)) `)` blockStmnt(' +
+ '{ expressionStmnt(call(identifier(bar) `(` `)`) ;) }) else blockStmnt(' +
+ '{ expressionStmnt(call(identifier(baz) `(` `)`) ;) }))))'],
+ ['while (false);',
+ 'program(whileStmnt(while `(` boolean(false) `)` emptyStmnt(;)))'],
+ ['while (/foo/.test(bar.baz)) {\n bar = bar.baz;\n}',
+ 'program(whileStmnt(while `(` call(dot(regex(/foo/) . test) `(` ' +
+ 'dot(identifier(bar) . baz) `)`) `)` blockStmnt({ expressionStmnt(' +
+ 'assignment(identifier(bar) = dot(identifier(bar) . baz)) ;) })))'],
+ ['while (false) while (false);',
+ 'program(whileStmnt(while `(` boolean(false) `)` ' +
+ 'whileStmnt(while `(` boolean (false) `)` emptyStmnt(;))))'],
+ ['do a; while (b);',
+ 'program(doStmnt(do expressionStmnt(identifier(a) ;) while `(` identifier(b) `)` ;))'],
+ ['do { x-- } while (x);',
+ 'program(doStmnt(do blockStmnt({ expressionStmnt(postfix(identifier(x) --) ;()) }) ' +
+ 'while `(` identifier(x) `)` ;))'],
+ ['do a\n while (b)\n x++',
+ 'program(doStmnt(do expressionStmnt(identifier(a) ;()) while `(` identifier(b) `)` ;()) ' +
+ 'expressionStmnt(postfix(identifier(x) ++) ;()))'],
+ ["for(;;);",
+ "program(forStmnt(for `(` forSpec(nil() ; nil() ; nil()) `)` emptyStmnt(;)))"],
+ ["for(x in y);",
+ "program(forStmnt(for `(` forInSpec(identifier(x) in identifier(y)) `)` emptyStmnt(;)))"],
+ ["for(var x in y);",
+ "program(forStmnt(for `(` forVarInSpec(var varDecl(x) in identifier(y)) `)` emptyStmnt(;)))"],
+ ["for(var x;;);",
+ "program(forStmnt(for `(` forVarSpec(var varDecl(x) ; nil() ; nil()) `)` emptyStmnt(;)))"],
+ ["for(var i=0;i>h>>>ik<=l>=m instanceof n in o==p!=q===r!==s&t^u|v&&w||x",
+ "program(expressionStmnt(binary(binary(binary(binary(binary(binary(binary(" +
+ "binary(binary(binary(binary(binary(binary(binary(binary(binary(binary(binary(" +
+ "binary(binary(binary(binary(binary(identifier(a) * identifier(b)) / " +
+ "identifier(c)) % identifier(d)) + identifier(e)) - identifier(f)) << identifier(g)) " +
+ ">> identifier(h)) >>> identifier(i)) < identifier(j)) > identifier(k)) <= " +
+ "identifier(l)) >= identifier(m)) instanceof identifier(n)) in identifier(o)) == " +
+ "identifier(p)) != identifier(q)) === identifier(r)) !== identifier(s)) & " +
+ "identifier(t)) ^ identifier(u)) | identifier(v)) && identifier(w)) || " +
+ "identifier(x)) ;()))"],
+ ["a||b&&c|d^e&f!==g===h!=i==j in k instanceof l>=m<=n>>q>>r<= identifier(m)) <= identifier(n)) < " +
+ "identifier(o)) < binary(binary(binary(identifier(p) >>> identifier(q)) >> " +
+ "identifier(r)) << binary(binary(identifier(s) - identifier(t)) + " +
+ "binary(binary(binary(identifier(u) % identifier(v)) / identifier(w)) * " +
+ "identifier(x))))))))))) ;()))"],
+ ["a?b:c",
+ "program(expressionStmnt(ternary(identifier(a) ? identifier(b) : " +
+ "identifier(c)) ;()))"],
+ ["1==2?3=4:5=6",
+ "program(expressionStmnt(ternary(binary(number(1) == number(2)) ? " +
+ "assignment(number(3) = number(4)) : assignment(number(5) = number(6))) ;()))"],
+ ["a=b,c=d",
+ "program(expressionStmnt(comma(assignment(identifier(a) = identifier(b)) , " +
+ "assignment(identifier(c) = identifier(d))) ;()))"],
+ ["a=b=c=d",
+ "program(expressionStmnt(assignment(identifier(a) = assignment(identifier(b) " +
+ "= assignment(identifier(c) = identifier(d)))) ;()))"],
+ ["x[0]=x[1]=true",
+ "program(expressionStmnt(assignment(bracket(identifier(x) [ number(0) ]) = " +
+ "assignment(bracket(identifier(x) [ number(1) ]) = boolean(true))) ;()))"],
+ ["a*=b/=c%=d+=e-=f<<=g>>=h>>>=i&=j^=k|=l",
+ "program(expressionStmnt(assignment(identifier(a) *= assignment(identifier(b) " +
+ "/= assignment(identifier(c) %= assignment(identifier(d) += " +
+ "assignment(identifier(e) -= assignment(identifier(f) <<= " +
+ "assignment(identifier(g) >>= assignment(identifier(h) >>>= " +
+ "assignment(identifier(i) &= assignment(identifier(j) ^= " +
+ "assignment(identifier(k) |= identifier(l)))))))))))) ;()))"],
+ ["1;\n\n\n\n/* foo */\n// bar\n", // trailing whitespace and comments
+ "program(expressionStmnt(number(1) ;))"]
+ ];
+ _.each(trials, function (tr) {
+ tester.goodParse(tr[0], tr[1]);
+ });
+});
+
+Tinytest.add("jsparse - bad parses", function (test) {
+ var tester = makeTester(test);
+ var trials = [
+ '{`statement`',
+ 'if (`expression`)',
+ 'if `(`else',
+ 'var`varDecl`;',
+ 'while (`expression`);',
+ 'while`(`;',
+ 'do a `semicolon`while b;',
+ 'do a\n while `(`b;',
+ '1 `semicolon`2',
+ 'for (`forSpec`);',
+ 'for (1\n`semicolon`2\n3);',
+ 'continue `semicolon`1+1;',
+ 'break `semicolon`1+1;',
+ 'throw`expression`',
+ 'throw`expression`;',
+ 'throw\n`expression`',
+ 'throw\n`expression``end of line`e',
+ 'throw `expression`=;',
+ 'with(`expression`);',
+ 'switch(`expression`)',
+ 'switch(x)`{`;',
+ 'try`block`',
+ 'try {}`catch`',
+ 'try {} catch`(`;',
+ 'try {} catch(e)`block`;',
+ '1+1`semicolon`:',
+ '{a:`statement`}',
+ 'function `IDENTIFIER`() {}',
+ 'foo: `statement`function foo() {}',
+ '[`expression`=',
+ '[,,`expression`=',
+ '({`propertyName`true:3})',
+ '({1:2,3`:`})',
+ '({1:2,`propertyName`',
+ 'x.`IDENTIFIER`true',
+ 'foo;`semicolon`:;',
+ '1;`statement`=',
+ 'a+b`semicolon`=c;',
+ 'for(1+1 `semicolon`in {});',
+ '`statement`=',
+ 'for(;`expression`var;) {}'
+ ];
+ _.each(trials, function (tr) {
+ tester.badParse(tr);
+ });
+});
+
+Tinytest.add("jsparse - regex division ambiguity", function (test) {
+ var tester = makeTester(test);
+ tester.goodParse("if (e) /f/g;",
+ "program(ifStmnt(if `(` identifier(e) `)` expressionStmnt(regex(/f/g) ;)))",
+ {4: true});
+ tester.goodParse("++/x/.y;",
+ "program(expressionStmnt(unary(++ dot(regex(/x/) . y)) ;))",
+ {1: true});
+ tester.goodParse("x++/2/g;",
+ "program(expressionStmnt(binary(binary(postfix(identifier(x) ++) / " +
+ "number(2)) / identifier(g)) ;))");
+ tester.goodParse("(1+1)/2/g;",
+ "program(expressionStmnt(binary(binary(parens(`(` binary(number(1) + " +
+ "number(1)) `)`) / " +
+ "number(2)) / identifier(g)) ;))");
+ tester.goodParse("/x/",
+ "program(expressionStmnt(regex(/x/) ;()))");
+});
+
+Tinytest.add("jsparse - semicolon insertion", function (test) {
+ var tester = makeTester(test);
+ // Spec section 7.9.2
+ tester.badParse("{ 1 `semicolon`2 } 3");
+ tester.goodParse("{ 1\n2 } 3", "program(blockStmnt({ expressionStmnt(number(1) " +
+ ";()) expressionStmnt(number(2) ;()) }) expressionStmnt(number(3) ;()))");
+ tester.badParse("for (a; b\n`semicolon`)");
+ tester.goodParse("return\na + b",
+ "program(returnStmnt(return nil() ;()) " +
+ "expressionStmnt(binary(identifier(a) + identifier(b)) ;()))");
+ tester.goodParse("a = b\n++c",
+ "program(expressionStmnt(assignment(identifier(a) = identifier(b)) ;())" +
+ "expressionStmnt(unary(++ identifier(c)) ;()))");
+ tester.badParse("if (a > b)\n`statement`else c = d");
+ tester.goodParse("a = b + c\n(d + e).print()",
+ "program(expressionStmnt(assignment(identifier(a) = " +
+ "binary(identifier(b) + call(dot(call(identifier(c) `(` " +
+ "binary(identifier(d) + identifier(e)) `)`) . print) `(` `)`))) ;()))");
+});
+
+Tinytest.add("jsparse - comments", function (test) {
+ var tester = makeTester(test);
+ // newline in multi-line comment makes it into a line break for semicolon
+ // insertion purposes
+ tester.badParse("1/**/`semicolon`2");
+ tester.goodParse("1/*\n*/2",
+ "program(expressionStmnt(number(1) ;()) expressionStmnt(number(2) ;()))");
+});
\ No newline at end of file
diff --git a/packages/jsparse/parserlib.js b/packages/jsparse/parserlib.js
new file mode 100644
index 0000000000..a8bbcac670
--- /dev/null
+++ b/packages/jsparse/parserlib.js
@@ -0,0 +1,264 @@
+///// TOKENIZER AND PARSER COMBINATORS
+
+(function () {
+
+// XXX track line/col position, for errors and maybe token info
+
+var isArray = function (obj) {
+ return obj && (typeof obj === 'object') && (typeof obj.length === 'number');
+};
+
+ParseNode = function (name, children) {
+ this.name = name;
+ this.children = children;
+
+ if (! isArray(children))
+ throw new Error("Expected array in new ParseNode(" + name + ", ...)");
+};
+
+
+Parser = function (expecting, runFunc) {
+ this.expecting = expecting;
+ this._run = runFunc;
+};
+
+Parser.prototype.parse = function (t) {
+ return this._run(t);
+};
+
+Parser.prototype.parseRequired = function (t) {
+ return this.parseRequiredIf(t, true);
+};
+
+Parser.prototype.parseRequiredIf = function (t, required) {
+ var result = this._run(t);
+
+ if (required && ! result)
+ throw t.getParseError(this.expecting);
+
+ return result;
+};
+
+Parser.expecting = function (expecting, parser) {
+ return new Parser(expecting, parser._run);
+};
+
+
+// A parser that consume()s has to succeed.
+// Similarly, a parser that fails can't have consumed.
+
+Parsers = {};
+
+Parsers.assertion = function (test) {
+ return new Parser(
+ null, function (t) {
+ return test(t) ? [] : null;
+ });
+};
+
+Parsers.node = function (name, childrenParser) {
+ return new Parser(name, function (t) {
+ var children = childrenParser.parse(t);
+ if (! children)
+ return null;
+ if (! isArray(children))
+ children = [children];
+ return new ParseNode(name, children);
+ });
+};
+
+Parsers.or = function (/*parsers*/) {
+ var args = arguments;
+ return new Parser(
+ args[args.length - 1].expecting,
+ function (t) {
+ var result;
+ for(var i = 0, N = args.length; i < N; i++) {
+ result = args[i].parse(t);
+ if (result)
+ return result;
+ }
+ return null;
+ });
+};
+
+// Parses a left-recursive expression with zero or more occurrences
+// of a binary op. Leaves the term unwrapped if no op. For example
+// (in a hypothetical use case):
+// `1` => "1"
+// `1+2` => ["binary", "1", "+", "2"]
+// `1+2+3` => ["binary", ["binary", "1", "+", "2"], "+", "3"]
+//
+// opParsers is an array of op parsers from high to low
+// precedence (tightest-binding first)
+Parsers.binaryLeft = function (name, termParser, opParsers) {
+ var opParser;
+
+ if (opParsers.length === 1) {
+ // take single opParser out of its array
+ opParser = opParsers[0];
+ } else {
+ // pop off last opParser (non-destructively) and replace
+ // termParser with a recursive binaryLeft on the remaining
+ // ops.
+ termParser = Parsers.binaryLeft(name, termParser, opParsers.slice(0, -1));
+ opParser = opParsers[opParsers.length - 1];
+ }
+
+ return new Parser(
+ termParser.expecting,
+ function (t) {
+ var result = termParser.parse(t);
+ if (! result)
+ return null;
+
+ var op;
+ while ((op = opParser.parse(t))) {
+ result = new ParseNode(
+ name,
+ [result, op, termParser.parseRequired(t)]);
+ }
+ return result;
+ });
+};
+
+Parsers.unary = function (name, termParser, opParser) {
+ var unaryList = Parsers.opt(Parsers.list(opParser));
+ return new Parser(
+ termParser.expecting,
+ function (t) {
+ var unaries = unaryList.parse(t);
+ // if we have unaries, we are committed and
+ // have to match a term or error.
+ var result = termParser.parseRequiredIf(t, unaries.length);
+ if (! result)
+ return null;
+
+ while (unaries.length)
+ result = new ParseNode(name, [unaries.pop(), result]);
+ return result;
+ });
+};
+
+// Parses a list of one or more items with a separator, listing the
+// items and separators. (Separator is optional.) For example:
+// `x` => ["x"]
+// `x,y` => ["x", ",", "y"]
+// `x,y,z` => ["x", ",", "y", ",", "z"]
+// Unpacks.
+Parsers.list = function (itemParser, sepParser) {
+ var push = function(array, newThing) {
+ if (isArray(newThing))
+ array.push.apply(array, newThing);
+ else
+ array.push(newThing);
+ };
+ return new Parser(
+ itemParser.expecting,
+ function (t) {
+ var result = [];
+ var firstItem = itemParser.parse(t);
+ if (! firstItem)
+ return null;
+ push(result, firstItem);
+
+ if (sepParser) {
+ var sep;
+ while ((sep = sepParser.parse(t))) {
+ push(result, sep);
+ push(result, itemParser.parseRequired(t));
+ }
+ } else {
+ var item;
+ while ((item = itemParser.parse(t)))
+ push(result, item);
+ }
+ return result;
+ });
+};
+
+// Unpacks arrays (nested seqs).
+Parsers.seq = function (/*parsers*/) {
+ var args = arguments;
+ if (! args.length)
+ return Parsers.constant([]);
+
+ return new Parser(
+ args[0].expecting,
+ function (t) {
+ var result = [];
+ for (var i = 0, N = args.length; i < N; i++) {
+ // first item in sequence can fail, and we
+ // fail (without error); after that, error on failure
+ var r = args[i].parseRequiredIf(t, i > 0);
+ if (! r)
+ return null;
+
+ if (isArray(r)) // append array!
+ result.push.apply(result, r);
+ else
+ result.push(r);
+ }
+ return result;
+ });
+};
+
+// parsers except last must never consume
+Parsers.and = function (/*parsers*/) {
+ var args = arguments;
+ if (! args.length)
+ return Parsers.constant([]);
+
+ return new Parser(
+ args[args.length - 1].expecting,
+ function (t) {
+ var result;
+ for(var i = 0, N = args.length; i < N; i++) {
+ result = args[i].parse(t);
+ if (! result)
+ return null;
+ }
+ return result;
+ });
+};
+
+// parser must not consume
+Parsers.not = function (parser) {
+ return new Parser(
+ null,
+ function (t) {
+ return parser.parse(t) ? null : [];
+ });
+};
+
+// parser that looks at nothing and returns result
+Parsers.constant = function (result) {
+ return new Parser(null,
+ function (t) { return result; });
+};
+
+Parsers.opt = function (parser) {
+ return Parser.expecting(
+ parser.expecting,
+ Parsers.or(parser, Parsers.seq()));
+};
+
+Parsers.mapResult = function (parser, func) {
+ return new Parser(
+ parser.expecting,
+ function (t) {
+ var v = parser.parse(t);
+ return v ? func(v, t) : null;
+ });
+};
+
+Parsers.lazy = function (expecting, parserFunc) {
+ var inner = null;
+ return new Parser(expecting, function (t) {
+ if (! inner)
+ inner = parserFunc();
+ return inner.parse(t);
+ });
+};
+
+})();
diff --git a/packages/jsparse/stringify.js b/packages/jsparse/stringify.js
new file mode 100644
index 0000000000..816b765a64
--- /dev/null
+++ b/packages/jsparse/stringify.js
@@ -0,0 +1,122 @@
+(function() {
+
+// The "tree string" format is a simple format for representing syntax trees.
+//
+// For example, the parse of `x++;` is written as:
+// "program(expressionStmnt(postfix(identifier(x) ++) ;))"
+//
+// A Node is written as "name(item1 item2 item3)", with additional whitespace
+// allowed anywhere between the name, parentheses, and items.
+//
+// Tokens don't need to be escaped unless they contain '(', ')', whitespace, or
+// backticks, or are empty. If they do, they can be written enclosed in backticks.
+// To escape a backtick within backticks, double it.
+//
+// `stringify` generates "canonical" tree strings, which have no extra escaping
+// or whitespace, just one space between items in a Node.
+
+
+ParseNode.prototype.stringify = function () {
+ return ParseNode.stringify(this);
+};
+
+var backtickEscape = function (str) {
+ if (/[\s()`]/.test(str))
+ return '`' + str.replace(/`/g, '``') + '`';
+ else if (! str)
+ return '``';
+ else
+ return str;
+};
+
+var backtickUnescape = function (str) {
+ if (str.charAt(0) === '`') {
+ if (str.length === 1 || str.slice(-1) !== '`')
+ throw new Error("Mismatched ` in " + str);
+ if (str.length === 2)
+ str = '';
+ else
+ str = str.slice(1, -1).replace(/``/g, '`');
+ }
+ return str;
+};
+
+ParseNode.stringify = function (tree) {
+ if (tree instanceof ParseNode) {
+ var str = backtickEscape(tree.name);
+ str += '(';
+ var escapedChildren = [];
+ for(var i = 0, N = tree.children.length; i < N; i++)
+ escapedChildren.push(ParseNode.stringify(tree.children[i]));
+ str += escapedChildren.join(' ');
+ str += ')';
+ return str;
+ }
+
+ // Treat a token object or string as a token.
+ if (typeof tree.text === 'function')
+ tree = tree.text();
+ else if (typeof tree.text === 'string')
+ tree = tree.text;
+ return backtickEscape(String(tree));
+};
+
+ParseNode.unstringify = function (str) {
+ var lexemes = str.match(/\(|\)|`([^`]||``)*`|`|[^\s()`]+/g) || [];
+ var N = lexemes.length;
+ var state = {
+ i: 0,
+ getParseError: function (expecting) {
+ throw new Error("unstringify: Expecting " + expecting +", found " +
+ (lexemes[this.i] || "end of string"));
+ },
+ peek: function () { return lexemes[this.i]; },
+ advance: function () { this.i++; }
+ };
+ var paren = function (chr) {
+ return new Parser(chr, function (t) {
+ if (t.peek() !== chr)
+ return null;
+ t.advance();
+ return chr;
+ });
+ };
+ var EMPTY_STRING = [""];
+ var token = new Parser('token', function (t) {
+ var txt = t.peek();
+ if (!txt || txt.charAt(0) === '(' || txt.charAt(0) === ')')
+ return null;
+
+ t.advance();
+ // can't return falsy value from successful parser
+ return backtickUnescape(txt) || EMPTY_STRING;
+ });
+
+ // Make "item" lazy so it can be recursive.
+ var item = Parsers.lazy('token', function () { return item; });
+
+ // Parse a single node or token.
+ item = Parsers.mapResult(
+ Parsers.seq(token,
+ Parsers.opt(Parsers.seq(
+ paren('('), Parsers.opt(Parsers.list(item)), paren(')')))),
+ function (v) {
+ for(var i = 0, N = v.length; i < N; i++)
+ if (v[i] === EMPTY_STRING)
+ v[i] = "";
+
+ if (v.length === 1)
+ // token
+ return v[0];
+ // node. exclude parens
+ return new ParseNode(v[0], v.slice(2, -1));
+ });
+
+ var endOfString = new Parser("end of string", function (t) {
+ return t.i === N ? [] : null;
+ });
+
+ return Parsers.seq(item, endOfString).parseRequired(state)[0];
+};
+
+})();
\ No newline at end of file
diff --git a/packages/less/less_tests.js b/packages/less/less_tests.js
index cf983cc7b2..63ae396e0d 100644
--- a/packages/less/less_tests.js
+++ b/packages/less/less_tests.js
@@ -2,13 +2,12 @@
Tinytest.add("less - presence", function(test) {
var d = OnscreenDiv(Meteor.render(function() {
- return ''; }));
+ return ''; }));
d.node().style.display = 'block';
var p = d.node().firstChild;
- var leftBorder = getStyleProperty(p, 'border-left-width');
- test.equal(leftBorder, "13px");
+ var leftBorder = getStyleProperty(p, 'border-left-style');
+ test.equal(leftBorder, "dashed");
d.kill();
});
-
diff --git a/packages/less/less_tests.less b/packages/less/less_tests.less
index 2e064a4516..9d2c65c2a3 100644
--- a/packages/less/less_tests.less
+++ b/packages/less/less_tests.less
@@ -1,8 +1,8 @@
#less-tests { zoom: 1; /* prop this rule open */ }
-@unlucky: 13px;
+@dashy: dashed;
-.less-unlucky-left-border {
- border-left: @unlucky solid white;
+.less-dashy-left-border {
+ border-left: 1px @dashy black;
}
diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js
index a258968f05..2ef1423ef7 100644
--- a/packages/livedata/livedata_server.js
+++ b/packages/livedata/livedata_server.js
@@ -337,7 +337,13 @@ _.extend(Meteor._LivedataSession.prototype, {
// Store a function to re-run the handler in case we want to rerun
// subscriptions, for example when the current user id changes
sub._runHandler = function() {
- var res = handler.apply(sub, params || []);
+ try {
+ var res = handler.apply(sub, params || []);
+ } catch (e) {
+ Meteor._debug("Internal exception while starting subscription", sub_id,
+ e.stack);
+ return;
+ }
// if Meteor._RemoteCollectionDriver is available (defined in
// mongo-livedata), automatically wire up handlers that return a
diff --git a/packages/meteor/package.js b/packages/meteor/package.js
index 9b93219566..a194a549d0 100644
--- a/packages/meteor/package.js
+++ b/packages/meteor/package.js
@@ -40,6 +40,11 @@ Package.on_use(function (api, where) {
api.use('underscore', ['client', 'server']);
api.add_files('dynamics_browser.js', 'client');
api.add_files('dynamics_nodejs.js', 'server');
+
+ // note server before common. usually it is the other way around, but
+ // in this case server must load first.
+ api.add_files('url_server.js', 'server');
+ api.add_files('url_common.js', ['client', 'server']);
});
Package.on_test(function (api) {
@@ -50,4 +55,6 @@ Package.on_test(function (api) {
api.add_files('helpers_test.js', ['client', 'server']);
api.add_files('dynamics_test.js', ['client', 'server']);
+
+ api.add_files('url_tests.js', ['client', 'server']);
});
diff --git a/packages/absolute-url/url_common.js b/packages/meteor/url_common.js
similarity index 91%
rename from packages/absolute-url/url_common.js
rename to packages/meteor/url_common.js
index 4c65f71170..c6ac9b2770 100644
--- a/packages/absolute-url/url_common.js
+++ b/packages/meteor/url_common.js
@@ -27,6 +27,9 @@
!/http:\/\/127\.0\.0\.1[:\/]/.test(url)) // or 127.0.0.1
url = url.replace(/^http:/, 'https:');
+ if (options.replaceLocalhost)
+ url = url.replace(/^http:\/\/localhost([:\/].*)/, 'http://127.0.0.1$1');
+
return url;
};
diff --git a/packages/absolute-url/url_server.js b/packages/meteor/url_server.js
similarity index 100%
rename from packages/absolute-url/url_server.js
rename to packages/meteor/url_server.js
diff --git a/packages/absolute-url/url_tests.js b/packages/meteor/url_tests.js
similarity index 64%
rename from packages/absolute-url/url_tests.js
rename to packages/meteor/url_tests.js
index 6548dfc7ab..922fe045f8 100644
--- a/packages/absolute-url/url_tests.js
+++ b/packages/meteor/url_tests.js
@@ -36,6 +36,27 @@ Tinytest.add("absolute-url - basics", function(test) {
test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://127.0.0.1:3000',
secure: true}),
'http://127.0.0.1:3000/foo');
+
+ // test replaceLocalhost
+ test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://localhost:3000',
+ replaceLocalhost: true}),
+ 'http://127.0.0.1:3000/foo');
+ test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://localhost',
+ replaceLocalhost: true}),
+ 'http://127.0.0.1/foo');
+ test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://127.0.0.1:3000',
+ replaceLocalhost: true}),
+ 'http://127.0.0.1:3000/foo');
+ test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://127.0.0.1',
+ replaceLocalhost: true}),
+ 'http://127.0.0.1/foo');
+ // don't replace just any localhost
+ test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://foo.com/localhost',
+ replaceLocalhost: true}),
+ 'http://foo.com/localhost/foo');
+ test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://foo.localhost.com',
+ replaceLocalhost: true}),
+ 'http://foo.localhost.com/foo');
});
diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js
index c5d64c9296..6d165501a1 100644
--- a/packages/minimongo/minimongo_tests.js
+++ b/packages/minimongo/minimongo_tests.js
@@ -201,7 +201,10 @@ Tinytest.add("minimongo - misc", function (test) {
var a = {a: [1, 2, 3], b: "x", c: true, d: {x: 12, y: [12]},
f: null, g: new Date()};
var b = LocalCollection._deepcopy(a);
- test.isTrue(LocalCollection._f._equal(a, b));
+ // minimongo doesn't support Dates, so we *can't* test
+ // LocalCollection._f._equal here! (Currently _equal considers all dates equal
+ // on most browsers except IE7 where it considers all dates unequal.)
+ test.equal(a, b);
a.a.push(4);
test.length(b.a, 3);
a.c = false;
@@ -211,10 +214,10 @@ Tinytest.add("minimongo - misc", function (test) {
test.equal(b.d.z, 15);
a.d.y.push(88);
test.length(b.d.y, 1);
- test.equal(a.g, b.g)
+ test.equal(a.g, b.g);
b.g.setDate(b.g.getDate() + 1);
- test.notEqual(a.g, b.g)
-
+ test.notEqual(a.g, b.g);
+
a = {x: function () {}};
b = LocalCollection._deepcopy(a);
a.x.a = 14;
diff --git a/packages/mongo-livedata/mongo_driver.js b/packages/mongo-livedata/mongo_driver.js
index d4ee2883ac..9f0da038f6 100644
--- a/packages/mongo-livedata/mongo_driver.js
+++ b/packages/mongo-livedata/mongo_driver.js
@@ -164,7 +164,7 @@ _Mongo.prototype.remove = function (collection_name, selector) {
return;
}
- collection.remove(selector, {/* XXXsafe: true*/}, function (err) {
+ collection.remove(selector, {safe: true}, function (err) {
if (err) {
future.ret(err);
return;
diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js
index 1277048e6f..e01243a129 100644
--- a/packages/mongo-livedata/mongo_livedata_tests.js
+++ b/packages/mongo-livedata/mongo_livedata_tests.js
@@ -5,7 +5,7 @@
Meteor._FailureTestCollection =
new Meteor.Collection("___meteor_failure_test_collection");
-testAsyncMulti("mongo-livedata - database failure reporting", [
+testAsyncMulti("mongo-livedata - database error reporting", [
function (test, expect) {
var ftc = Meteor._FailureTestCollection;
diff --git a/packages/sass/sass_tests.js b/packages/sass/sass_tests.js
index 2c5622c8fa..e2e0e7b652 100644
--- a/packages/sass/sass_tests.js
+++ b/packages/sass/sass_tests.js
@@ -2,14 +2,13 @@
Tinytest.add("sass - presence", function(test) {
var d = OnscreenDiv(Meteor.render(function() {
- return ''; }));
+ return ''; }));
d.node().style.display = 'block';
var p = d.node().firstChild;
- var leftBorder = getStyleProperty(p, 'border-left-width');
- test.equal(leftBorder, "13px");
+ var leftBorder = getStyleProperty(p, 'border-left-style');
+ test.equal(leftBorder, "dashed");
d.kill();
});
-
diff --git a/packages/sass/sass_tests.sass b/packages/sass/sass_tests.sass
index cf109dbaf6..c555f4c59f 100644
--- a/packages/sass/sass_tests.sass
+++ b/packages/sass/sass_tests.sass
@@ -1,7 +1,7 @@
#sass-tests
:zoom 1
-unlucky: 13px
+dashy: dashed
-.sass-unlucky-left-border
- :border-left !unlucky solid white
+.sass-dashy-left-border
+ :border-left 1px !dashy black
diff --git a/packages/spark/patch_tests.js b/packages/spark/patch_tests.js
index 842b8c8e80..890c34e808 100644
--- a/packages/spark/patch_tests.js
+++ b/packages/spark/patch_tests.js
@@ -185,11 +185,11 @@ Tinytest.add("spark - patch - copyAttributes", function(test) {
{id:'foo', 'class':'bar',
style:'border:1px solid blue;', name:'baz'});
a.check();
- test.equal(a.node().style.borderColor, "blue");
+ test.equal(a.node().style.borderLeftColor, "blue");
a.copy({id: "foo", style:'border:1px solid red'});
a.check();
- test.equal(a.node().style.borderColor, "red");
+ test.equal(a.node().style.borderLeftColor, "red");
a.copy({id: "foo", 'class':'ha'});
a.check();
diff --git a/packages/spark/spark.js b/packages/spark/spark.js
index 3a9d3b7cfc..55d34e03af 100644
--- a/packages/spark/spark.js
+++ b/packages/spark/spark.js
@@ -91,8 +91,9 @@ var notifyWatchers = function (start, end) {
};
Spark._createId = function () {
+ // Chars can't include '-' to be safe inside HTML comments.
var chars =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+_";
var id = "";
for (var i = 0; i < 8; i++)
id += chars.substr(Math.floor(Meteor.random() * 64), 1);
diff --git a/packages/spiderable/spiderable.js b/packages/spiderable/spiderable.js
index 153baa4f0e..1b883c21bb 100644
--- a/packages/spiderable/spiderable.js
+++ b/packages/spiderable/spiderable.js
@@ -24,7 +24,12 @@
// Use '/dev/stdin' to avoid writing to a temporary file. Can't
// just omit the file, as PhantomJS takes that to mean 'use a
// REPL' and exits as soon as stdin closes.
- var cp = spawn('phantomjs', ['--load-images=no', '/dev/stdin']);
+ //
+ // However, Node 0.8 broke the ability to open /dev/stdin in the
+ // subprocess; see https://gist.github.com/3751746 for the gory
+ // details. Work around this with a not-so-useless use of cat.
+ var cp = spawn('bash',
+ ['-c', 'cat | phantomjs --load-images=no /dev/stdin']);
var data = '';
cp.stdout.setEncoding('utf8');
diff --git a/packages/stylus/stylus_tests.js b/packages/stylus/stylus_tests.js
index 304411ff74..0fddbd3420 100644
--- a/packages/stylus/stylus_tests.js
+++ b/packages/stylus/stylus_tests.js
@@ -2,12 +2,12 @@
Tinytest.add("stylus - presence", function(test) {
var d = OnscreenDiv(Meteor.render(function() {
- return ''; }));
+ return ''; }));
d.node().style.display = 'block';
var p = d.node().firstChild;
- var leftBorder = getStyleProperty(p, 'border-left-width');
- test.equal(leftBorder, "13px");
+ var leftBorder = getStyleProperty(p, 'border-left-style');
+ test.equal(leftBorder, "dashed");
d.kill();
diff --git a/packages/stylus/stylus_tests.styl b/packages/stylus/stylus_tests.styl
index 2dec111077..feedadbdf2 100644
--- a/packages/stylus/stylus_tests.styl
+++ b/packages/stylus/stylus_tests.styl
@@ -2,8 +2,8 @@
#stylus-tests
zoom: 1
-unlucky = 13px
+dashy = dashed
-.stylus-unlucky-left-border
- border-left: unlucky solid white
+.stylus-dashy-left-border
+ border-left: 1px dashy black
diff --git a/packages/test-in-browser/driver.css b/packages/test-in-browser/driver.css
index f8489a5a5f..29d9da59ff 100644
--- a/packages/test-in-browser/driver.css
+++ b/packages/test-in-browser/driver.css
@@ -44,6 +44,7 @@
line-height: 24px;
vertical-align: middle;
text-decoration: underline;
+ cursor: pointer;
}
.test_table .groupname {