Merge branch 'devel' into auth

Conflicts:
	packages/livedata/livedata_server.js
This commit is contained in:
Avital Oliver
2012-09-21 10:59:48 -07:00
52 changed files with 2712 additions and 115 deletions

View File

@@ -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

View File

@@ -1,4 +1,4 @@
meteor (0.4.0-1) unstable; urgency=low
meteor (0.4.1-1) unstable; urgency=low
* Automated debian build.

View File

@@ -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 $@

View File

@@ -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

View File

@@ -5,7 +5,7 @@
## example.
URLBASE="https://d3sqy0vbqsdhku.cloudfront.net"
VERSION="0.4.0"
VERSION="0.4.1"
PKGVERSION="${VERSION}-1"
UNAME=`uname`

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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" "$@"

View File

@@ -1,4 +1,4 @@
exports.CURRENT_VERSION = "0.4.0";
exports.CURRENT_VERSION = "0.4.1";
var fs = require("fs");
var http = require("http");

View File

@@ -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');

View File

@@ -28,6 +28,7 @@ put on the screen.
});
}
{{> api_box absoluteUrl}}
<h2 id="publishandsubscribe"><span>Publish and subscribe</span></h2>

View File

@@ -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)",

View File

@@ -11,7 +11,7 @@
</div>
<div id="main">
<div id="top"></div>
<h1 class="main-headline">Meteor 0.4.0</h1>
<h1 class="main-headline">Meteor 0.4.1</h1>
{{> introduction }}
{{> concepts }}
{{> api }}

View File

@@ -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",

View File

@@ -16,7 +16,6 @@ and removed with:
$ meteor remove <package_name>
{{> pkg_absolute_url}}
{{> pkg_amplify}}
{{> pkg_backbone}}
{{> pkg_bootstrap}}

View File

@@ -1,15 +0,0 @@
<template name="pkg_absolute_url">
{{#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}}
</template>

View File

@@ -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`\""
}
]
};

View File

@@ -0,0 +1 @@
local

View File

@@ -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

View File

@@ -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;
}

View File

@@ -0,0 +1,29 @@
<head>
<title>jsparser</title>
</head>
<body>
{{> page}}
</body>
<template name="page">
<div id="topbar">
<div id="topbarinner">
This is a demo of the new <code>jsparse</code> package.
<strong>Edit</strong> code on the left.
<strong>Click</strong> on an outlined box on the right
to select it in the code. The full spec is supported.
</div>
</div>
<div id="main">
<div id="inputarea">{{!
whitespace is significant here; browser swallows initial
newline in textarea
}}<textarea>
{{input}}</textarea>
</div>
<div id="output">
{{output}}
</div>
</div>
</template>

View File

@@ -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 += '<br>';
} else {
var text = Handlebars._escape(L.text || ' ');
text = text.replace(/(?!.)\s/g, '<br>'); // for multiline comments
text = text.replace(/\s/g, '&nbsp;');
html += '<span class="lex lex_' + L.type.toLowerCase() + '">' +
text + '</span>';
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,
'<span class="parseerror">' +
Handlebars._escape(errorLexeme.text() || '<EOF>') +
'</span>');
html = html.replace(/(?!.)\s/g, '<br>');
html += '<div class="parseerrormessage">' +
Handlebars._escape(parseError.toString()) + '</div>';
}
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,
'<div class="box named' + (isStatement ? ' statement' : '') +
'"><div class="box head">' + Handlebars._escape(head) + '</div>' +
_.map(children, toHtml).join('') + '</div>');
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, '&#8203;');
return Spark.setDataContext(
obj,
'<div class="box token">' + text + '</div>');
} else {
// other?
return '<div class="box other">' +
Handlebars._escape(JSON.stringify(obj)) + '</div>';
}
};
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);
};
}

2
meteor
View File

@@ -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.

View File

@@ -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.');
});

View File

@@ -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.

View File

@@ -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;

View File

@@ -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');

View File

@@ -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)

397
packages/jsparse/lexer.js Normal file
View File

@@ -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');
};
})();

View File

@@ -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']);
});

761
packages/jsparse/parser.js Normal file
View File

@@ -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);
};
})();

View File

@@ -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<N;i++) {}",
"program(forStmnt(for `(` forVarSpec(var varDecl(i = number(0)) ; " +
"binary(identifier(i) < identifier(N)) ; postfix(identifier(i) ++)) `)` blockStmnt({ })))"],
["for (var x=3 in y);",
"program(forStmnt(for `(` forVarInSpec(var varDecl(x = number(3)) in identifier(y)) `)` " +
"emptyStmnt(;)))"],
["for (x.foo in y);",
"program(forStmnt(for `(` forInSpec(dot(identifier(x) . foo) in identifier(y)) `)` emptyStmnt(;)))"],
["return",
"program(returnStmnt(return nil() ;()))"],
["return;",
"program(returnStmnt(return nil() ;))"],
["return null",
"program(returnStmnt(return null(null) ;()))"],
["return null;",
"program(returnStmnt(return null(null) ;))"],
["return\n1+1",
"program(returnStmnt(return nil() ;()) expressionStmnt(binary(number(1) + number(1)) ;()))"],
["return 1\n +1",
"program(returnStmnt(return binary(number(1) + number(1)) ;()))"],
["continue",
"program(continueStmnt(continue nil() ;()))"],
["continue foo",
"program(continueStmnt(continue foo ;()))"],
["continue foo;",
"program(continueStmnt(continue foo ;))"],
["continue\n foo;",
"program(continueStmnt(continue nil() ;()) expressionStmnt(identifier(foo) ;))"],
["break",
"program(breakStmnt(break nil() ;()))"],
["break foo",
"program(breakStmnt(break foo ;()))"],
["break foo;",
"program(breakStmnt(break foo ;))"],
["break\n foo;",
"program(breakStmnt(break nil() ;()) expressionStmnt(identifier(foo) ;))"],
["throw e;",
"program(throwStmnt(throw identifier(e) ;))"],
["throw e",
"program(throwStmnt(throw identifier(e) ;()))"],
["throw new Error;",
"program(throwStmnt(throw new(new identifier(Error)) ;))"],
["with(x);",
"program(withStmnt(with `(` identifier(x) `)` emptyStmnt(;)))"],
["with(a=b) {}",
"program(withStmnt(with `(` assignment(identifier(a) = identifier(b)) `)` blockStmnt({ })))"],
["switch(x) {}",
"program(switchStmnt(switch `(` identifier(x) `)` { }))"],
["switch(x) {case 1:case 2:case 3:default:case 4:}",
"program(switchStmnt(switch `(` identifier(x) `)` { " +
"case(case number(1) :) case(case number(2) :) case(case number(3) :) " +
"default(default :) case(case number(4) :) }))"],
["switch(x) {\ncase 1:\n return\ncase 2:\ncase 3:\n throw e}",
"program(switchStmnt(switch `(` identifier(x) `)` { " +
"case(case number(1) : returnStmnt(return nil() ;())) " +
"case(case number(2) :) case(case number(3) : " +
"throwStmnt(throw identifier(e) ;())) }))"],
["switch(x) {default:;}",
"program(switchStmnt(switch `(` identifier(x) `)` { default(default : emptyStmnt(;)) }))"],
["try {} catch (e) {} finally {}",
"program(tryStmnt(try blockStmnt({ }) catch(catch `(` e `)` blockStmnt({ })) " +
"finally(finally blockStmnt({ }))))"],
["try {} finally {}",
"program(tryStmnt(try blockStmnt({ }) nil() finally(finally blockStmnt({ }))))"],
["try {} catch (e) {}",
"program(tryStmnt(try blockStmnt({ }) catch(catch `(` e `)` blockStmnt({ })) nil()))"],
["a:;",
"program(labelStmnt(a : emptyStmnt(;)))"],
["{x:1}",
"program(blockStmnt({ labelStmnt(x : expressionStmnt(number(1) ;())) }))"],
["{x:y:z:1}",
"program(blockStmnt({ labelStmnt(x : labelStmnt(y : " +
"labelStmnt(z : expressionStmnt(number(1) ;())))) }))"],
[";;foo:\nfor(;;);",
"program(emptyStmnt(;) emptyStmnt(;) labelStmnt(foo : " +
"forStmnt(for `(` forSpec(nil() ; nil() ; nil()) `)` emptyStmnt(;))))"],
["debugger",
"program(debuggerStmnt(debugger ;()))"],
["debugger;",
"program(debuggerStmnt(debugger ;))"],
["function foo() {}",
"program(functionDecl(function foo `(` `)` { }))"],
["function foo() {function bar() {}}",
"program(functionDecl(function foo `(` `)` { functionDecl(function bar `(` `)` { }) }))"],
[";;function f() {};;",
"program(emptyStmnt(;) emptyStmnt(;) functionDecl(function f `(` `)` { }) " +
"emptyStmnt(;) emptyStmnt(;))"],
["function foo(a,b,c) {}",
"program(functionDecl(function foo `(` a , b , c `)` { }))"],
// EXPRESSIONS
["null + this - 3 + true",
"program(expressionStmnt(binary(binary(binary(null(null) + this(this)) - " +
"number(3)) + boolean(true)) ;()))"],
["a / /b/mgi / c",
"program(expressionStmnt(binary(binary(identifier(a) / " +
"regex(/b/mgi)) / identifier(c)) ;()))"],
["'a' + \"\" + \"b\" + '\\''",
"program(expressionStmnt(binary(binary(binary(string('a') + string(\"\")) + " +
"string(\"b\")) + string('\\'')) ;()))"],
["_ + x0123 + $",
"program(expressionStmnt(binary(binary(identifier(_) + " +
"identifier(x0123)) + identifier($)) ;()))"],
["if ((x = 1)) return ((1+2))*((1<<2));",
"program(ifStmnt(if `(` parens(`(` assignment(identifier(x) = number(1)) `)`) " +
"`)` returnStmnt(return binary(parens(`(` parens(`(` binary(number(1) + " +
"number(2)) `)`) `)`) * parens(`(` parens(`(` binary(number(1) << number(2)) " +
"`)`) `)`)) ;)))"],
["[];",
"program(expressionStmnt(array([ ]) ;))"],
["[,,,];",
"program(expressionStmnt(array([ , , , ]) ;))"],
["[(1,2),,3];",
"program(expressionStmnt(array([ parens(`(` comma(number(1) , " +
"number(2)) `)`) , , number(3) ]) ;))"],
["({});",
"program(expressionStmnt(parens(`(` object({ }) `)`) ;))"],
["({1:1});",
"program(expressionStmnt(parens(`(` object({ prop(numPropName(1) : number(1)) }) `)`) ;))"],
["({x:true});",
"program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : boolean(true)) }) `)`) ;))"],
["({'a':b, c:'d', 1:null});",
"program(expressionStmnt(parens(`(` object({ prop(strPropName('a') : " +
"identifier(b)) , prop(idPropName(c) : string('d')) , prop(numPropName(1) " +
": null(null)) }) `)`) ;))"],
["(function () {});",
"program(expressionStmnt(parens(`(` functionExpr(function nil() `(` `)` { }) `)`) ;))"],
["(function foo() {});",
"program(expressionStmnt(parens(`(` functionExpr(function foo `(` `)` { }) `)`) ;))"],
["x = function () {}.y;",
"program(expressionStmnt(assignment(identifier(x) = dot(functionExpr(" +
"function nil() `(` `)` { }) . y)) ;))"],
["(function (a) {})",
"program(expressionStmnt(parens(`(` functionExpr(function nil() " +
"`(` a `)` { }) `)`) ;()))"],
["(function (a,b,c) {})",
"program(expressionStmnt(parens(`(` functionExpr(function nil() `(` " +
"a , b , c `)` { }) `)`) ;()))"],
["foo.bar.baz;",
"program(expressionStmnt(dot(dot(identifier(foo) . bar) . baz) ;))"],
["foo[bar,bar][baz].qux[1+1];",
"program(expressionStmnt(bracket(dot(bracket(bracket(identifier(foo) " +
"[ comma(identifier(bar) , identifier(bar)) ]) [ identifier(baz) ]) . qux) " +
"[ binary(number(1) + number(1)) ]) ;))"],
["new new a.b.c[d]",
"program(expressionStmnt(new(new new(new bracket(dot(dot(identifier(a) " +
". b) . c) [ identifier(d) ]))) ;()))"],
["new new a.b.c[d]()",
"program(expressionStmnt(new(new newcall(new " +
"bracket(dot(dot(identifier(a) . b) . c) [ identifier(d) ]) `(` `)`)) ;()))"],
["new new a.b.c[d]()()",
"program(expressionStmnt(newcall(new newcall(new " +
"bracket(dot(dot(identifier(a) . b) . c) [ identifier(d) ]) `(` `)`) `(` `)`) ;()))"],
["new foo(x).bar(y)",
"program(expressionStmnt(call(dot(newcall(new identifier(foo) `(` " +
"identifier(x) `)`) . bar) `(` identifier(y) `)`) ;()))"],
["new new foo().bar",
"program(expressionStmnt(new(new dot(newcall(new identifier(foo) `(` `)`) . bar)) ;()))"],
["delete void typeof - + ~ ! -- ++ x;",
"program(expressionStmnt(unary(delete unary(void unary(typeof unary(- unary(+ " +
"unary(~ unary(! unary(-- unary(++ identifier(x)))))))))) ;))"],
["x++ + ++y",
"program(expressionStmnt(binary(postfix(identifier(x) ++) + " +
"unary(++ identifier(y))) ;()))"],
["1*2+3*4",
"program(expressionStmnt(binary(binary(number(1) * number(2)) " +
"+ binary(number(3) * number(4))) ;()))"],
["a*b/c%d+e-f<<g>>h>>>i<j>k<=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<o<p>>>q>>r<<s-t+u%v/w*x",
"program(expressionStmnt(binary(identifier(a) || binary(identifier(b) && " +
"binary(identifier(c) | binary(identifier(d) ^ binary(identifier(e) & " +
"binary(binary(binary(binary(identifier(f) !== identifier(g)) === identifier(h)) " +
"!= identifier(i)) == binary(binary(binary(binary(binary(binary(identifier(j) in " +
"identifier(k)) instanceof identifier(l)) >= 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) ;()))");
});

View File

@@ -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);
});
};
})();

View File

@@ -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];
};
})();

View File

@@ -2,13 +2,12 @@
Tinytest.add("less - presence", function(test) {
var d = OnscreenDiv(Meteor.render(function() {
return '<p class="less-unlucky-left-border"></p>'; }));
return '<p class="less-dashy-left-border"></p>'; }));
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();
});

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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']);
});

View File

@@ -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;
};

View File

@@ -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');
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -2,14 +2,13 @@
Tinytest.add("sass - presence", function(test) {
var d = OnscreenDiv(Meteor.render(function() {
return '<p class="sass-unlucky-left-border"></p>'; }));
return '<p class="sass-dashy-left-border"></p>'; }));
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();
});

View File

@@ -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

View File

@@ -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();

View File

@@ -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);

View File

@@ -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');

View File

@@ -2,12 +2,12 @@
Tinytest.add("stylus - presence", function(test) {
var d = OnscreenDiv(Meteor.render(function() {
return '<p class="stylus-unlucky-left-border"></p>'; }));
return '<p class="stylus-dashy-left-border"></p>'; }));
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();

View File

@@ -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

View File

@@ -44,6 +44,7 @@
line-height: 24px;
vertical-align: middle;
text-decoration: underline;
cursor: pointer;
}
.test_table .groupname {