diff --git a/History.md b/History.md index 5ec2d0a1fa..d9a1d171c1 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,64 @@ ## vNEXT +## v0.4.1 + +* New `email` smart package, with [`Email.send`](http://docs.meteor.com/#email) + API. + +* Upgrade Node from 0.6.17 to 0.8.8, as well as many Node modules in the dev + bundle; those that are user-exposed are: + * coffee-script: 1.3.3 (from 1.3.1) + * stylus: 0.29.0 (from 0.28.1) + * nib: 0.8.2 (from 0.7.0) + +* All publicly documented APIs now use `camelCase` rather than + `under_scores`. The old spellings continue to work for now. New names are: + - `Meteor.isClient`/`isServer` + - `this.isSimulation` inside a method invocation + - `Meteor.deps.Context.onInvalidate` + - `Meteor.status().retryCount`/`retryTime` + +* Spark improvements + * Optimize selector matching for event maps. + * Fix `Spark._currentRenderer` behavior in timer callbacks. + * Fix bug caused by interaction between `Template.foo.preserve` and + `{{#constant}}`. #323 + * Allow `{{#each}}` over a collection of objects without `_id`. #281 + * Spark now supports Firefox 3.6. + * Added a script to build a standalone spark.js that does not depend on + Meteor (it depends on jQuery or Sizzle if you need IE7 support, + and otherwise is fully standalone). + +* Database writes from within `Meteor.setTimeout`/`setInterval`/`defer` will be + batched with other writes from the current method invocation if they start + before the method completes. + +* Make `Meteor.Cursor.forEach` fully synchronous even if the user's callback + yields. #321. + +* Recover from exceptions thrown in `Meteor.publish` handlers. + +* Upgrade bootstrap to version 2.1.1. #336, #337, #288, #293 + +* Change the implementation of the `meteor deploy` password prompt to not crash + Emacs M-x shell. + +* Optimize `LocalCollection.remove(id)` to be O(1) rather than O(n). + +* Optimize client-side database performance when receiving updated data from the + server outside of method calls. + +* Better error reporting when a package in `.meteor/packages` does not exist. + +* Better error reporting for coffeescript. #331 + +* Better error handling in `Handlebars.Exception`. + + +Patches contributed by GitHub users fivethirty, tmeasday, and xenolf. + + ## v0.4.0 * Merge Spark, a new live page update engine diff --git a/admin/debian/changelog b/admin/debian/changelog index b4c3533497..8517d7b6be 100644 --- a/admin/debian/changelog +++ b/admin/debian/changelog @@ -1,4 +1,4 @@ -meteor (0.4.0-1) unstable; urgency=low +meteor (0.4.1-1) unstable; urgency=low * Automated debian build. diff --git a/admin/debian/rules b/admin/debian/rules index 7b223eeb5e..6a83b44185 100755 --- a/admin/debian/rules +++ b/admin/debian/rules @@ -17,11 +17,5 @@ override_dh_prep: tar -C debian/tmp/usr/lib -xzf $(TARBALL) echo -n 'deb' > debian/tmp/usr/lib/meteor/.package_stamp -# node fibers distributes copies of the library pre-compiled for many -# different architectures. This confuses shlibdeps. Just ignore the -# fibers library. -override_dh_shlibdeps: - dh_shlibdeps -Xfibers.node - %: dh $@ diff --git a/admin/generate-dev-bundle.sh b/admin/generate-dev-bundle.sh index efc27770e6..6f495e7c2d 100755 --- a/admin/generate-dev-bundle.sh +++ b/admin/generate-dev-bundle.sh @@ -1,8 +1,9 @@ #!/bin/bash set -e +set -u -BUNDLE_VERSION=0.2.2 +BUNDLE_VERSION=0.2.3 UNAME=$(uname) ARCH=$(uname -m) @@ -182,7 +183,6 @@ npm install mongodb@1.1.5 npm install uglify-js@1.3.3 npm install clean-css@0.6.0 npm install progress@0.0.5 -npm install fibers@0.6.9 npm install useragent@1.1.0 npm install request@2.11.0 npm install http-proxy@0.8.2 @@ -201,6 +201,19 @@ git clone http://github.com/akdubya/rbytes.git npm install sockjs@0.3.1 rm -rf rbytes +npm install fibers@0.6.9 +# Fibers ships with compiled versions of its C code for a dozen platforms. This +# bloats our dev bundle, and confuses dpkg-buildpackage and rpmbuild into +# thinking that the packages need to depend on both 32- and 64-bit versions of +# libstd++. Remove all the ones other than our architecture. (Expression based +# on build.js in fibers source.) +FIBERS_ARCH=$(node -p -e 'process.platform + "-" + process.arch + "-v8-" + /[0-9]+\.[0-9]+/.exec(process.versions.v8)[0]') +cd fibers/bin +mv $FIBERS_ARCH .. +rm -rf * +mv ../$FIBERS_ARCH . +cd ../.. + cd "$DIR" curl "$MONGO_URL" | tar -xz diff --git a/admin/install-s3.sh b/admin/install-s3.sh index d302a6ec5b..75b8502e9b 100755 --- a/admin/install-s3.sh +++ b/admin/install-s3.sh @@ -5,7 +5,7 @@ ## example. URLBASE="https://d3sqy0vbqsdhku.cloudfront.net" -VERSION="0.4.0" +VERSION="0.4.1" PKGVERSION="${VERSION}-1" UNAME=`uname` diff --git a/admin/manifest.json b/admin/manifest.json index ec2de4b0dc..d60784e251 100644 --- a/admin/manifest.json +++ b/admin/manifest.json @@ -1,6 +1,6 @@ { - "version": "0.4.0", - "deb_version": "0.4.0-1", - "rpm_version": "0.4.0-1", + "version": "0.4.1", + "deb_version": "0.4.1-1", + "rpm_version": "0.4.1-1", "urlbase": "https://d3sqy0vbqsdhku.cloudfront.net" } diff --git a/admin/meteor.spec b/admin/meteor.spec index 31233ed895..43dde1499e 100644 --- a/admin/meteor.spec +++ b/admin/meteor.spec @@ -5,7 +5,7 @@ Summary: Meteor platform and JavaScript application server Vendor: Meteor Name: meteor -Version: 0.4.0 +Version: 0.4.1 Release: 1 License: MIT Group: Networking/WWW diff --git a/admin/node.sh b/admin/node.sh index 3326cd3bc8..ba712deb85 100755 --- a/admin/node.sh +++ b/admin/node.sh @@ -12,4 +12,4 @@ fi cd "$ORIGDIR" export NODE_PATH="$TOPDIR/dev_bundle/lib/node_modules" -exec "$TOPDIR/dev_bundle/bin/node" $* +exec "$TOPDIR/dev_bundle/bin/node" "$@" diff --git a/app/lib/updater.js b/app/lib/updater.js index f15d1c04c6..2ff86cb452 100644 --- a/app/lib/updater.js +++ b/app/lib/updater.js @@ -1,4 +1,4 @@ -exports.CURRENT_VERSION = "0.4.0"; +exports.CURRENT_VERSION = "0.4.1"; var fs = require("fs"); var http = require("http"); diff --git a/app/meteor/post-upgrade.js b/app/meteor/post-upgrade.js index 5000bf0fda..a33008e577 100644 --- a/app/meteor/post-upgrade.js +++ b/app/meteor/post-upgrade.js @@ -2,7 +2,7 @@ try { // XXX can't get this from updater.js because in 0.3.7 and before the // updater didn't have the right NODE_PATH set. At some point we can // remove this and just use updater.CURRENT_VERSION. - var VERSION = "0.4.0"; + var VERSION = "0.4.1"; var fs = require('fs'); var path = require('path'); diff --git a/docs/client/api.html b/docs/client/api.html index 675f8e53a2..5984bd19e5 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -28,6 +28,7 @@ put on the screen. }); } +{{> api_box absoluteUrl}}

Publish and subscribe

diff --git a/docs/client/api.js b/docs/client/api.js index dd78bb410c..fc0306ac52 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -24,6 +24,36 @@ Template.api.startup = { ] }; +Template.api.absoluteUrl = { + id: "meteor_absoluteurl", + name: "Meteor.absoluteUrl([path], [options])", + locus: "Anywhere", + descr: ["Generate an absolute URL pointing to the application. The server " + + "reads from the `ROOT_URL` environment variable to determine " + + "where it is running. This is taken care of automatically for " + + "apps deployed with `meteor deploy`, but must be provided when " + + "using `meteor bundle`."], + args: [ + {name: "path", + type: "String", + descr: 'A path to append to the root URL. Do not include a leading "`/`".' + } + ], + options: [ + {name: "secure", + type: "Boolean", + descr: "Create an HTTPS URL." + }, + {name: "replaceLocalhost", + type: "Boolean", + descr: "Replace localhost with 127.0.0.1. Useful for services that don't recognize localhost as a domain name."}, + {name: "rootUrl", + type: "String", + descr: "Override the default ROOT_URL from the server environment. For example: \"`http://foo.example.com`\"" + } + ] +}; + Template.api.publish = { id: "meteor_publish", name: "Meteor.publish(name, func)", diff --git a/docs/client/docs.html b/docs/client/docs.html index 9306792c71..331460e720 100644 --- a/docs/client/docs.html +++ b/docs/client/docs.html @@ -11,7 +11,7 @@
-

Meteor 0.4.0

+

Meteor 0.4.1

{{> introduction }} {{> concepts }} {{> api }} diff --git a/docs/client/docs.js b/docs/client/docs.js index 165e679de5..871b46b975 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -1,4 +1,4 @@ -METEOR_VERSION = "0.4.0"; +METEOR_VERSION = "0.4.1"; Meteor.startup(function () { // XXX this is broken by the new multi-page layout. Also, it was @@ -82,7 +82,8 @@ var toc = [ "Core", [ "Meteor.isClient", "Meteor.isServer", - "Meteor.startup" + "Meteor.startup", + "Meteor.absoluteUrl" ], "Publish and subscribe", [ @@ -205,7 +206,6 @@ var toc = [ ], "Packages", [ [ - "absolute-url", "amplify", "backbone", "bootstrap", diff --git a/docs/client/packages.html b/docs/client/packages.html index 51eddc0373..1c12157077 100644 --- a/docs/client/packages.html +++ b/docs/client/packages.html @@ -16,7 +16,6 @@ and removed with: $ meteor remove -{{> pkg_absolute_url}} {{> pkg_amplify}} {{> pkg_backbone}} {{> pkg_bootstrap}} diff --git a/docs/client/packages/absolute-url.html b/docs/client/packages/absolute-url.html deleted file mode 100644 index 19a0401e63..0000000000 --- a/docs/client/packages/absolute-url.html +++ /dev/null @@ -1,15 +0,0 @@ - diff --git a/docs/client/packages/absolute-url.js b/docs/client/packages/absolute-url.js deleted file mode 100644 index 7a4ba1d036..0000000000 --- a/docs/client/packages/absolute-url.js +++ /dev/null @@ -1,24 +0,0 @@ -Template.pkg_absolute_url.absoluteUrl = { - id: "meteor_absoluteUrl", - name: "Meteor.absoluteUrl([path], [options])", - locus: "Anywhere", - descr: ["Generate an absolute URL pointing to the application."], - args: [ - {name: "path", - type: "String", - descr: 'A path to append to the root URL. Do not include a leading "`/`".' - } - ], - options: [ - {name: "secure", - type: "Boolean", - descr: "Create an HTTPS URL." - }, - {name: "rootUrl", - type: "String", - descr: "Override the default ROOT_URL from the server environment. For example: \"`http://foo.example.com`\"" - } - ] - -}; - diff --git a/examples/unfinished/jsparse-demo/.meteor/.gitignore b/examples/unfinished/jsparse-demo/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/examples/unfinished/jsparse-demo/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/examples/unfinished/jsparse-demo/.meteor/packages b/examples/unfinished/jsparse-demo/.meteor/packages new file mode 100644 index 0000000000..0c508c3289 --- /dev/null +++ b/examples/unfinished/jsparse-demo/.meteor/packages @@ -0,0 +1,7 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +autopublish +jsparse diff --git a/examples/unfinished/jsparse-demo/jsparse-demo.css b/examples/unfinished/jsparse-demo/jsparse-demo.css new file mode 100644 index 0000000000..bea973c397 --- /dev/null +++ b/examples/unfinished/jsparse-demo/jsparse-demo.css @@ -0,0 +1,147 @@ + +* { padding: 0; margin: 0; } +html, body { height: 100%; } + +#topbar { + position: absolute; + width: 100%; + top: 0; + height: 39px; + border-bottom: 1px solid #555; + overflow: auto; + background: #cfc; + font-size: 12px; +} + +#topbarinner { + padding: 7px; + font-family: sans-serif; +} + +#main { + position: absolute; + width: 100%; + top: 40px; + bottom: 0; +} + +#inputarea textarea { + border: 0; + border-right: 1px solid #555; + position: absolute; + height: 100%; + left: 0; + right: 50%; + font-family: monospace; + font-size: 100%; +} + +#output { + position: absolute; + height: 100%; + left: 50%; + right: 0; + overflow: auto; + + font-family: monospace; +} + +#inputarea textarea, #output { + line-height: 130%; +} + +.lex { border: 1px solid #333; } + +.lex_keyword { background: #0f0; } +.lex_identifier { background: #ff0; } +.lex_punctuation { background: #0ff; } +.lex_error { background: #f00; } +.lex_whitespace { background: #fcc; } +.lex_comment { background: #ccc; } + +.lex_regex { background: #f0f; } +.lex_null { background: #dac; } +.lex_boolean { background: #faf; } +.lex_number { background: #c3f; } +.lex_string { background: #fc3; } + +.parseerror { + background: #f99; + border: 1px solid blue; + cursor: pointer; +} +.parseerrormessage { color: #c00; } + +.box { + display: inline-block; + margin: 5px; + margin-top: 0; + background: #fff; +} + +.box.statement { + display: block; +} + +#output > .box { + margin-top: 5px; +} + +.box.named { + border: 1px solid #888; + border-radius: 5px; + cursor: pointer; + overflow: hidden; + /* position:relative breaks overflow:hidden effect of rounded corners? */ + position: static; +} + +.box.head { + font-family: sans-serif; + font-size: 70%; + font-weight: bold; + display: block; + margin-left: 0; + margin-right: 0; + background: #ccc; + color: #000; + padding-left: 5px; + padding-right: 5px; + border-bottom: 1px solid #888; +} + +.box.head:last-child { + margin: 0; + border-bottom: 0; +} + +.box.token { + background: #ddd; + /*border: 1px solid #999;*/ + border: 1px solid #00f; + cursor: pointer; + font-family: monospace; + font-weight: bold; + font-size: 120%; + padding: 1px; +} + +.box.named[mousehover] { + background: #cdf; + border: 1px solid #448; +} + +.box.token[mousehover] { + background: #ace; + border: 1px solid #448; +} + +.box.named[mousehover] > .box.head { + background: #58b; + border-bottom: 1px solid #448; +} + +.box.named[mousehover] > .box.head:last-child { + border-bottom: 0; +} + diff --git a/examples/unfinished/jsparse-demo/jsparse-demo.html b/examples/unfinished/jsparse-demo/jsparse-demo.html new file mode 100644 index 0000000000..d8a676b2b6 --- /dev/null +++ b/examples/unfinished/jsparse-demo/jsparse-demo.html @@ -0,0 +1,29 @@ + + jsparser + + + + {{> page}} + + + diff --git a/examples/unfinished/jsparse-demo/jsparse-demo.js b/examples/unfinished/jsparse-demo/jsparse-demo.js new file mode 100644 index 0000000000..9ea3d008de --- /dev/null +++ b/examples/unfinished/jsparse-demo/jsparse-demo.js @@ -0,0 +1,151 @@ + + +if (Meteor.is_client) { + Meteor.startup(function () { + if (! Session.get("input")) + Session.set("input", "var x = 3"); + }); + + Template.page.input = function () { + return Session.get("input") || ''; + }; + + Template.page.output = function () { + var input = Session.get("input") || ""; + + // LEXER + /* + if (! input) + return ""; + + var L = new Lexer(input); + var html = ""; + while (L.next() !== 'EOF') { + if (L.type === "NEWLINE") { + html += '
'; + } else { + var text = Handlebars._escape(L.text || ' '); + text = text.replace(/(?!.)\s/g, '
'); // for multiline comments + text = text.replace(/\s/g, ' '); + html += '' + + text + ''; + if (L.type === "ERROR") + break; + } + }*/ + + // PARSER + var html; + var tree = null; + var parser = new JSParser(input); + try { + tree = parser.getSyntaxTree(); + } catch (parseError) { + var errorLexeme = parser.lexer.lastLexeme; + + html = Handlebars._escape( + input.substring(0, errorLexeme.startPos())); + html += Spark.setDataContext( + errorLexeme, + '' + + Handlebars._escape(errorLexeme.text() || '') + + ''); + html = html.replace(/(?!.)\s/g, '
'); + html += '
' + + Handlebars._escape(parseError.toString()) + '
'; + } + if (tree) { + var curPos = 0; + var unclosedInfos = []; + var toHtml = function (obj) { + if (obj instanceof ParseNode) { + var head = obj.name || ''; + var children = obj.children; + var info = { startPos: curPos }; + var isStatement = (head.indexOf('Stmnt') >= 0); + var html = Spark.setDataContext( + info, + '
' + Handlebars._escape(head) + '
' + + _.map(children, toHtml).join('') + '
'); + unclosedInfos.push(info); + return html; + } else if (obj.text) { + // token + _.each(unclosedInfos, function (info) { + info.endPos = curPos; + }); + curPos = obj.endPos(); + unclosedInfos.length = 0; + var text = obj.text(); + // insert zero-width spaces to allow wrapping + text = text.replace(/.{20}/g, "$&\n"); + text = Handlebars._escape(text); + text = text.replace(/\n/g, '​'); + return Spark.setDataContext( + obj, + '
' + text + '
'); + } else { + // other? + return '
' + + Handlebars._escape(JSON.stringify(obj)) + '
'; + } + }; + html = toHtml(tree); + curPos = parser.lexer.pos; + _.each(unclosedInfos, function (info) { + info.endPos = curPos; + }); + } + + return new Handlebars.SafeString(html); + }; + + Template.page.events({ + 'keyup #inputarea textarea': function (event) { + var input = event.currentTarget.value; + Session.set("input", input); + }, + 'mouseover .box.named, mouseover .box.token': function (event) { + event.currentTarget.setAttribute('mousehover', 'mousehover'); + event.stopImmediatePropagation(); + }, + 'mouseout .box.named, mouseout .box.token': function (event) { + event.currentTarget.removeAttribute('mousehover'); + event.stopImmediatePropagation(); + }, + 'click .box.token': function (event) { + selectInputText(this.startPos(), this.endPos()); + return false; + }, + 'click .box.named': function (event) { + selectInputText(this.startPos, this.endPos); + return false; + }, + 'click .parseerror': function (event) { + selectInputText(this.startPos(), this.endPos()); + return false; + } + }); + + Template.page.preserve(['#inputarea textarea']); + + var selectTextInArea = function (e, start, end){ + e.focus(); + if (e.setSelectionRange) { + e.setSelectionRange(start, end); + } else if (e.createTextRange) { + var r = e.createTextRange(); + r.collapse(true); + r.moveEnd('character', end); + r.moveStart('character', start); + r.select(); + } + }; + + var selectInputText = function (start, end) { + var textarea = DomUtils.find(document, '#inputarea textarea'); + selectTextInArea(textarea, start, end); + }; + +} diff --git a/meteor b/meteor index 34c354c120..70e583c498 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/bin/bash -BUNDLE_VERSION=0.2.2 +BUNDLE_VERSION=0.2.3 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. diff --git a/packages/absolute-url/package.js b/packages/absolute-url/package.js index 796cba69b8..b84536203d 100644 --- a/packages/absolute-url/package.js +++ b/packages/absolute-url/package.js @@ -1,17 +1,10 @@ Package.describe({ - summary: "Generate absolute URLs pointing to the application" + summary: "DEPRECATED: Generate absolute URLs pointing to the application", + internal: true }); Package.on_use(function (api) { - // note server before common. usually it is the other way around, but - // in this case server must load first. - api.add_files('url_server.js', 'server'); - api.add_files('url_common.js', ['client', 'server']); -}); - -Package.on_test(function (api) { - api.use('absolute-url', ['client', 'server']); - api.use('tinytest'); - - api.add_files('url_tests.js', ['client', 'server']); + console.log('DEPRECATED. The `absolute-url` package has been folded into ' + + 'the `meteor` package and should not be used directly. Run ' + + '`meteor remove absolute-url` to resolve this.'); }); diff --git a/packages/domutils/domutils.js b/packages/domutils/domutils.js index fdc7e6efb7..53f266d601 100644 --- a/packages/domutils/domutils.js +++ b/packages/domutils/domutils.js @@ -5,11 +5,10 @@ DomUtils = {}; (function () { var qsaFindAllBySelector = function (selector, contextNode) { + // If IE7 users report the following error message, you + // can fix it with "meteor add jquery". if (! document.querySelectorAll) - // IE 7 - throw new Error( - "This browser doesn't support querySelectorAll. " + - "You need Sizzle or jQuery (`meteor add jquery`)."); + throw new Error("This browser doesn't support querySelectorAll."); // the search is constrained to descendants of `ancestor`, // but it doesn't affect the scope of the query. diff --git a/packages/email/email_tests.js b/packages/email/email_tests.js index f635104ba1..68c78eef47 100644 --- a/packages/email/email_tests.js +++ b/packages/email/email_tests.js @@ -1,6 +1,9 @@ streamBuffers = __meteor_bootstrap__.require('stream-buffers'); Tinytest.add("email - dev mode smoke test", function (test) { + // This only tests dev mode, so don't run the test if this is deployed. + if (process.env.MAIL_URL) return; + var old_stream = Email._output_stream; try { Email._output_stream = new streamBuffers.WritableStreamBuffer; diff --git a/packages/force-ssl/package.js b/packages/force-ssl/package.js index 90b5037578..bc413ad820 100644 --- a/packages/force-ssl/package.js +++ b/packages/force-ssl/package.js @@ -8,11 +8,6 @@ Package.on_use(function (api) { // server has been instantiated. api.use('livedata', 'server'); - // we don't really depend on absolute-url, but we do modify its - // behavior. If there were a way to say "if the other package is - // loaded, make sure we come after it", we should do that here. - api.use('absolute-url', ['client', 'server']); - api.add_files('force_ssl_common.js', ['client', 'server']); api.add_files('force_ssl_server.js', 'server'); diff --git a/packages/http/httpcall_tests.js b/packages/http/httpcall_tests.js index 2baecbaec4..edd0a29fbd 100644 --- a/packages/http/httpcall_tests.js +++ b/packages/http/httpcall_tests.js @@ -69,7 +69,7 @@ testAsyncMulti("httpcall - basic", [ "/foo?fruit=apple&dog=Spot+the+dog"); }]); -testAsyncMulti("httpcall - failure", [ +testAsyncMulti("httpcall - errors", [ function(test, expect) { // Accessing unknown server (should fail to make any connection) diff --git a/packages/jsparse/lexer.js b/packages/jsparse/lexer.js new file mode 100644 index 0000000000..c62e2c6bd4 --- /dev/null +++ b/packages/jsparse/lexer.js @@ -0,0 +1,397 @@ + +(function () { + +var regexEscape = function (str) { + return str.replace(/[\][^$\\.*+?(){}|]/g, '\\$&'); +}; + +// Adapted from source code of http://xregexp.com/plugins/#unicode +var unicodeCategories = { + Ll: "0061-007A00B500DF-00F600F8-00FF01010103010501070109010B010D010F01110113011501170119011B011D011F01210123012501270129012B012D012F01310133013501370138013A013C013E014001420144014601480149014B014D014F01510153015501570159015B015D015F01610163016501670169016B016D016F0171017301750177017A017C017E-0180018301850188018C018D019201950199-019B019E01A101A301A501A801AA01AB01AD01B001B401B601B901BA01BD-01BF01C601C901CC01CE01D001D201D401D601D801DA01DC01DD01DF01E101E301E501E701E901EB01ED01EF01F001F301F501F901FB01FD01FF02010203020502070209020B020D020F02110213021502170219021B021D021F02210223022502270229022B022D022F02310233-0239023C023F0240024202470249024B024D024F-02930295-02AF037103730377037B-037D039003AC-03CE03D003D103D5-03D703D903DB03DD03DF03E103E303E503E703E903EB03ED03EF-03F303F503F803FB03FC0430-045F04610463046504670469046B046D046F04710473047504770479047B047D047F0481048B048D048F04910493049504970499049B049D049F04A104A304A504A704A904AB04AD04AF04B104B304B504B704B904BB04BD04BF04C204C404C604C804CA04CC04CE04CF04D104D304D504D704D904DB04DD04DF04E104E304E504E704E904EB04ED04EF04F104F304F504F704F904FB04FD04FF05010503050505070509050B050D050F05110513051505170519051B051D051F05210523052505270561-05871D00-1D2B1D6B-1D771D79-1D9A1E011E031E051E071E091E0B1E0D1E0F1E111E131E151E171E191E1B1E1D1E1F1E211E231E251E271E291E2B1E2D1E2F1E311E331E351E371E391E3B1E3D1E3F1E411E431E451E471E491E4B1E4D1E4F1E511E531E551E571E591E5B1E5D1E5F1E611E631E651E671E691E6B1E6D1E6F1E711E731E751E771E791E7B1E7D1E7F1E811E831E851E871E891E8B1E8D1E8F1E911E931E95-1E9D1E9F1EA11EA31EA51EA71EA91EAB1EAD1EAF1EB11EB31EB51EB71EB91EBB1EBD1EBF1EC11EC31EC51EC71EC91ECB1ECD1ECF1ED11ED31ED51ED71ED91EDB1EDD1EDF1EE11EE31EE51EE71EE91EEB1EED1EEF1EF11EF31EF51EF71EF91EFB1EFD1EFF-1F071F10-1F151F20-1F271F30-1F371F40-1F451F50-1F571F60-1F671F70-1F7D1F80-1F871F90-1F971FA0-1FA71FB0-1FB41FB61FB71FBE1FC2-1FC41FC61FC71FD0-1FD31FD61FD71FE0-1FE71FF2-1FF41FF61FF7210A210E210F2113212F21342139213C213D2146-2149214E21842C30-2C5E2C612C652C662C682C6A2C6C2C712C732C742C76-2C7B2C812C832C852C872C892C8B2C8D2C8F2C912C932C952C972C992C9B2C9D2C9F2CA12CA32CA52CA72CA92CAB2CAD2CAF2CB12CB32CB52CB72CB92CBB2CBD2CBF2CC12CC32CC52CC72CC92CCB2CCD2CCF2CD12CD32CD52CD72CD92CDB2CDD2CDF2CE12CE32CE42CEC2CEE2CF32D00-2D252D272D2DA641A643A645A647A649A64BA64DA64FA651A653A655A657A659A65BA65DA65FA661A663A665A667A669A66BA66DA681A683A685A687A689A68BA68DA68FA691A693A695A697A723A725A727A729A72BA72DA72F-A731A733A735A737A739A73BA73DA73FA741A743A745A747A749A74BA74DA74FA751A753A755A757A759A75BA75DA75FA761A763A765A767A769A76BA76DA76FA771-A778A77AA77CA77FA781A783A785A787A78CA78EA791A793A7A1A7A3A7A5A7A7A7A9A7FAFB00-FB06FB13-FB17FF41-FF5A", + Lm: "02B0-02C102C6-02D102E0-02E402EC02EE0374037A0559064006E506E607F407F507FA081A0824082809710E460EC610FC17D718431AA71C78-1C7D1D2C-1D6A1D781D9B-1DBF2071207F2090-209C2C7C2C7D2D6F2E2F30053031-3035303B309D309E30FC-30FEA015A4F8-A4FDA60CA67FA717-A71FA770A788A7F8A7F9A9CFAA70AADDAAF3AAF4FF70FF9EFF9F", + Lo: "00AA00BA01BB01C0-01C3029405D0-05EA05F0-05F20620-063F0641-064A066E066F0671-06D306D506EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA0800-08150840-085808A008A2-08AC0904-0939093D09500958-09610972-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E450E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10D0-10FA10FD-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317DC1820-18421844-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541B05-1B331B45-1B4B1B83-1BA01BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C771CE9-1CEC1CEE-1CF11CF51CF62135-21382D30-2D672D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE3006303C3041-3096309F30A1-30FA30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCCA000-A014A016-A48CA4D0-A4F7A500-A60BA610-A61FA62AA62BA66EA6A0-A6E5A7FB-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2AA00-AA28AA40-AA42AA44-AA4BAA60-AA6FAA71-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADBAADCAAE0-AAEAAAF2AB01-AB06AB09-AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF66-FF6FFF71-FF9DFFA0-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC", + Lt: "01C501C801CB01F21F88-1F8F1F98-1F9F1FA8-1FAF1FBC1FCC1FFC", + Lu: "0041-005A00C0-00D600D8-00DE01000102010401060108010A010C010E01100112011401160118011A011C011E01200122012401260128012A012C012E01300132013401360139013B013D013F0141014301450147014A014C014E01500152015401560158015A015C015E01600162016401660168016A016C016E017001720174017601780179017B017D018101820184018601870189-018B018E-0191019301940196-0198019C019D019F01A001A201A401A601A701A901AC01AE01AF01B1-01B301B501B701B801BC01C401C701CA01CD01CF01D101D301D501D701D901DB01DE01E001E201E401E601E801EA01EC01EE01F101F401F6-01F801FA01FC01FE02000202020402060208020A020C020E02100212021402160218021A021C021E02200222022402260228022A022C022E02300232023A023B023D023E02410243-02460248024A024C024E03700372037603860388-038A038C038E038F0391-03A103A3-03AB03CF03D2-03D403D803DA03DC03DE03E003E203E403E603E803EA03EC03EE03F403F703F903FA03FD-042F04600462046404660468046A046C046E04700472047404760478047A047C047E0480048A048C048E04900492049404960498049A049C049E04A004A204A404A604A804AA04AC04AE04B004B204B404B604B804BA04BC04BE04C004C104C304C504C704C904CB04CD04D004D204D404D604D804DA04DC04DE04E004E204E404E604E804EA04EC04EE04F004F204F404F604F804FA04FC04FE05000502050405060508050A050C050E05100512051405160518051A051C051E05200522052405260531-055610A0-10C510C710CD1E001E021E041E061E081E0A1E0C1E0E1E101E121E141E161E181E1A1E1C1E1E1E201E221E241E261E281E2A1E2C1E2E1E301E321E341E361E381E3A1E3C1E3E1E401E421E441E461E481E4A1E4C1E4E1E501E521E541E561E581E5A1E5C1E5E1E601E621E641E661E681E6A1E6C1E6E1E701E721E741E761E781E7A1E7C1E7E1E801E821E841E861E881E8A1E8C1E8E1E901E921E941E9E1EA01EA21EA41EA61EA81EAA1EAC1EAE1EB01EB21EB41EB61EB81EBA1EBC1EBE1EC01EC21EC41EC61EC81ECA1ECC1ECE1ED01ED21ED41ED61ED81EDA1EDC1EDE1EE01EE21EE41EE61EE81EEA1EEC1EEE1EF01EF21EF41EF61EF81EFA1EFC1EFE1F08-1F0F1F18-1F1D1F28-1F2F1F38-1F3F1F48-1F4D1F591F5B1F5D1F5F1F68-1F6F1FB8-1FBB1FC8-1FCB1FD8-1FDB1FE8-1FEC1FF8-1FFB21022107210B-210D2110-211221152119-211D212421262128212A-212D2130-2133213E213F214521832C00-2C2E2C602C62-2C642C672C692C6B2C6D-2C702C722C752C7E-2C802C822C842C862C882C8A2C8C2C8E2C902C922C942C962C982C9A2C9C2C9E2CA02CA22CA42CA62CA82CAA2CAC2CAE2CB02CB22CB42CB62CB82CBA2CBC2CBE2CC02CC22CC42CC62CC82CCA2CCC2CCE2CD02CD22CD42CD62CD82CDA2CDC2CDE2CE02CE22CEB2CED2CF2A640A642A644A646A648A64AA64CA64EA650A652A654A656A658A65AA65CA65EA660A662A664A666A668A66AA66CA680A682A684A686A688A68AA68CA68EA690A692A694A696A722A724A726A728A72AA72CA72EA732A734A736A738A73AA73CA73EA740A742A744A746A748A74AA74CA74EA750A752A754A756A758A75AA75CA75EA760A762A764A766A768A76AA76CA76EA779A77BA77DA77EA780A782A784A786A78BA78DA790A792A7A0A7A2A7A4A7A6A7A8A7AAFF21-FF3A", + Mc: "0903093B093E-09400949-094C094E094F0982098309BE-09C009C709C809CB09CC09D70A030A3E-0A400A830ABE-0AC00AC90ACB0ACC0B020B030B3E0B400B470B480B4B0B4C0B570BBE0BBF0BC10BC20BC6-0BC80BCA-0BCC0BD70C01-0C030C41-0C440C820C830CBE0CC0-0CC40CC70CC80CCA0CCB0CD50CD60D020D030D3E-0D400D46-0D480D4A-0D4C0D570D820D830DCF-0DD10DD8-0DDF0DF20DF30F3E0F3F0F7F102B102C10311038103B103C105610571062-10641067-106D108310841087-108C108F109A-109C17B617BE-17C517C717C81923-19261929-192B193019311933-193819B0-19C019C819C91A19-1A1B1A551A571A611A631A641A6D-1A721B041B351B3B1B3D-1B411B431B441B821BA11BA61BA71BAA1BAC1BAD1BE71BEA-1BEC1BEE1BF21BF31C24-1C2B1C341C351CE11CF21CF3302E302FA823A824A827A880A881A8B4-A8C3A952A953A983A9B4A9B5A9BAA9BBA9BD-A9C0AA2FAA30AA33AA34AA4DAA7BAAEBAAEEAAEFAAF5ABE3ABE4ABE6ABE7ABE9ABEAABEC", + Mn: "0300-036F0483-04870591-05BD05BF05C105C205C405C505C70610-061A064B-065F067006D6-06DC06DF-06E406E706E806EA-06ED07110730-074A07A6-07B007EB-07F30816-0819081B-08230825-08270829-082D0859-085B08E4-08FE0900-0902093A093C0941-0948094D0951-095709620963098109BC09C1-09C409CD09E209E30A010A020A3C0A410A420A470A480A4B-0A4D0A510A700A710A750A810A820ABC0AC1-0AC50AC70AC80ACD0AE20AE30B010B3C0B3F0B41-0B440B4D0B560B620B630B820BC00BCD0C3E-0C400C46-0C480C4A-0C4D0C550C560C620C630CBC0CBF0CC60CCC0CCD0CE20CE30D41-0D440D4D0D620D630DCA0DD2-0DD40DD60E310E34-0E3A0E47-0E4E0EB10EB4-0EB90EBB0EBC0EC8-0ECD0F180F190F350F370F390F71-0F7E0F80-0F840F860F870F8D-0F970F99-0FBC0FC6102D-10301032-10371039103A103D103E10581059105E-10601071-1074108210851086108D109D135D-135F1712-17141732-1734175217531772177317B417B517B7-17BD17C617C9-17D317DD180B-180D18A91920-19221927192819321939-193B1A171A181A561A58-1A5E1A601A621A65-1A6C1A73-1A7C1A7F1B00-1B031B341B36-1B3A1B3C1B421B6B-1B731B801B811BA2-1BA51BA81BA91BAB1BE61BE81BE91BED1BEF-1BF11C2C-1C331C361C371CD0-1CD21CD4-1CE01CE2-1CE81CED1CF41DC0-1DE61DFC-1DFF20D0-20DC20E120E5-20F02CEF-2CF12D7F2DE0-2DFF302A-302D3099309AA66FA674-A67DA69FA6F0A6F1A802A806A80BA825A826A8C4A8E0-A8F1A926-A92DA947-A951A980-A982A9B3A9B6-A9B9A9BCAA29-AA2EAA31AA32AA35AA36AA43AA4CAAB0AAB2-AAB4AAB7AAB8AABEAABFAAC1AAECAAEDAAF6ABE5ABE8ABEDFB1EFE00-FE0FFE20-FE26", + Nd: "0030-00390660-066906F0-06F907C0-07C90966-096F09E6-09EF0A66-0A6F0AE6-0AEF0B66-0B6F0BE6-0BEF0C66-0C6F0CE6-0CEF0D66-0D6F0E50-0E590ED0-0ED90F20-0F291040-10491090-109917E0-17E91810-18191946-194F19D0-19D91A80-1A891A90-1A991B50-1B591BB0-1BB91C40-1C491C50-1C59A620-A629A8D0-A8D9A900-A909A9D0-A9D9AA50-AA59ABF0-ABF9FF10-FF19", + Nl: "16EE-16F02160-21822185-218830073021-30293038-303AA6E6-A6EF", + Pc: "005F203F20402054FE33FE34FE4D-FE4FFF3F" +}; + +var unicodeClass = function (abbrev) { + return '[' + + unicodeCategories[abbrev].replace(/[0-9A-F]{4}/ig, "\\u$&") + ']'; +}; + +// See ECMA-262 spec, 3rd edition, section 7 + +// Section 7.2 +// Match one or more characters of whitespace, excluding line terminators. +// Do this by matching reluctantly, stopping at a non-dot (line terminator +// or end of string) or a non-whitespace. +// We are taking advantage of the fact that we are parsing JS from JS in +// regexes like this by "passing through" the spec's definition of whitespace, +// which is the same in regexes and the lexical grammar. +var rWhiteSpace = /[^\S\u000A\u000D\u2028\u2029]+/g; +// Section 7.3 +// Match one line terminator. Same as (?!.)[\s\S] but more explicit. +var rLineTerminator = /[\u000A\u000D\u2028\u2029]/g; +// Section 7.4 +// Match one multi-line comment. +// [\s\S] is shorthand for any character, including newlines. +// The *? reluctant qualifier makes this easy. +var rMultiLineComment = /\/\*[\s\S]*?\*\//g; +// Match one single-line comment, not including the line terminator. +var rSingleLineComment = /\/\/.*/g; +// Section 7.6 +// Match one or more characters that can start an identifier. +// This is IdentifierStart+. +var rIdentifierPrefix = new RegExp( + "([a-zA-Z$_]+|\\\\u[0-9a-fA-F]{4}|" + + [unicodeClass('Lu'), unicodeClass('Ll'), unicodeClass('Lt'), + unicodeClass('Lm'), unicodeClass('Lo'), unicodeClass('Nl')].join('|') + + ")+", 'g'); +// Match one or more characters that can continue an identifier. +// This is (IdentifierPart and not IdentifierStart)+. +// To match a full identifier, match rIdentifierPrefix, then +// match rIdentifierMiddle followed by rIdentifierPrefix until they both fail. +var rIdentifierMiddle = new RegExp( + "([0-9]|" + [unicodeClass('Mn'), unicodeClass('Mc'), unicodeClass('Nd'), + unicodeClass('Pc')].join('|') + ")+", 'g'); +// Section 7.7 +// Match one punctuator (except for division punctuators). +var rPunctuator = new RegExp( + regexEscape("{ } ( ) [ ] . ; , < > <= >= == != === !== + - * % ++ -- << >> "+ + ">>> & | ^ ! ~ && || ? : = += -= *= %= <<= >>= >>>= &= |= ^=") + // sort from longest to shortest so that we don't match '==' for '===' and + // '*' for '*=', etc. + .split(' ').sort(function (a,b) { return b.length - a.length; }) + .join('|'), 'g'); +var rDivPunctuator = /\/=?/g; +// Section 7.8.3 +var rHexLiteral = /0x[0-9a-fA-F]+$/g; +var rOctLiteral = /0[0-7]+/g; // deprecated +var rDecLiteral = + /(((0|[1-9][0-9]*)(\.[0-9]*)?)|\.[0-9]+)([Ee][+-]?[0-9]+)?/g; +// Section 7.8.4 +var rStringQuote = /["']/g; +// Match one or more characters besides quotes, backslashes, or line ends +var rStringMiddle = /(?=.)[^"'\\]+?((?!.)|(?=["'\\]))/g; +// Match one escape sequence, including the backslash. +var rEscapeSequence = + /\\(['"\\bfnrtv]|0(?![0-9])|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|(?=.)[^ux0-9])/g; +// Section 7.8.5 +// Match one regex literal, including slashes, not including flags. +// XXX Add support for unescaped '/' in character class, allowed by 5th ed. +var rRegexLiteral = /\/(?![*\/])(\\.|(?=.)[^\\])+?\//g; +var rRegexFlags = /[a-zA-Z]*/g; + +var rDecider = + /((?=.)\s)|(\/[\/\*]?)|([\][{}().;,<>=!+*%&|^~?:-])|(\d)|(["'])|(.)|([\S\s])/g; + +var keywordLookup = { + ' break': 'KEYWORD', + ' case': 'KEYWORD', + ' catch': 'KEYWORD', + ' continue': 'KEYWORD', + ' debugger': 'KEYWORD', + ' default': 'KEYWORD', + ' delete': 'KEYWORD', + ' do': 'KEYWORD', + ' else': 'KEYWORD', + ' finally': 'KEYWORD', + ' for': 'KEYWORD', + ' function': 'KEYWORD', + ' if': 'KEYWORD', + ' in': 'KEYWORD', + ' instanceof': 'KEYWORD', + ' new': 'KEYWORD', + ' return': 'KEYWORD', + ' switch': 'KEYWORD', + ' this': 'KEYWORD', + ' throw': 'KEYWORD', + ' try': 'KEYWORD', + ' typeof': 'KEYWORD', + ' var': 'KEYWORD', + ' void': 'KEYWORD', + ' while': 'KEYWORD', + ' with': 'KEYWORD', + + ' false': 'BOOLEAN', + ' true': 'BOOLEAN', + + ' null': 'NULL' +}; + +var makeSet = function (array) { + var s = {}; + for (var i = 0, N = array.length; i < N; i++) + s[array[i]] = true; + return s; +}; + +var nonTokenTypes = makeSet('WHITESPACE COMMENT NEWLINE EOF ERROR'.split(' ')); + +var punctuationBeforeDivision = makeSet('] ) } ++ --'.split(' ')); +var keywordsBeforeDivision = makeSet('this'.split(' ')); + +var guessIsDivisionPermittedAfterToken = function (tok) { + // Figure out if a '/' character should be interpreted as division + // rather than the start of a regular expression when it follows the + // token, which must be a token lexeme per isToken(). + // The beginning of section 7 of the spec briefly + // explains what's going on; basically the lexical grammar can't + // distinguish, for example, `e/f/g` (division) from `e=/f/g` + // (assignment of a regular expression), among many other variations. + // + // THIS IS ONLY A HEURISTIC, though it will rarely fail. + // Here are the two cases I know of where help from the parser is needed: + // - if (foo) + // /ba/.test("banana") && console.log("matches"); + // (Close paren of a control structure before a statement starting with + // a regex literal. Starting a statement with a regex literal is + // unusual, of course, because it's hard to have a side effect.) + // - ++ /foo/.abc + // (Prefix `++` or `--` before an expression starting with a regex + // literal. This will run but I can't see any use for it.) + switch (tok.type()) { + case "PUNCTUATION": + // few punctuators can end an expression, but e.g. `)` + return !! punctuationBeforeDivision[tok.text()]; + case "KEYWORD": + // few keywords can end an expression, but e.g. `this` + return !! keywordsBeforeDivision[tok.text()]; + case "IDENTIFIER": + return true; + default: // literal + return true; + } +}; + +////////// PUBLIC API + +var Lexeme = function (pos, type, text) { + this._pos = pos; + this._type = type; + this._text = text; +}; + +Lexeme.prototype.startPos = function () { + return this._pos; +}; + +Lexeme.prototype.endPos = function () { + return this._pos + this._text.length; +}; + +Lexeme.prototype.type = function () { + return this._type; +}; + +Lexeme.prototype.text = function () { + return this._text; +}; + +Lexeme.prototype.isToken = function () { + return ! nonTokenTypes[this._type]; +}; + +Lexeme.prototype.isError = function () { + return this._type === "ERROR"; +}; + +Lexeme.prototype.isEOF = function () { + return this._type === "EOF"; +}; + +Lexeme.prototype.prev = function () { + return this._prev; +}; + +Lexeme.prototype.next = function () { + return this._next; +}; + +Lexeme.prototype.toString = function () { + return this.isError() ? "ERROR" : + this.isEOF() ? "EOF" : "`" + this.text() + "`"; +}; + +// Create a Lexer for the given string of JavaScript code. +// +// A lexer keeps a pointer `pos` into the string that is +// advanced when you ask for the next lexeme with `next()`. +// +// XXXXX UPDATE DOCS +// Properties: +// code: Original JavaScript code string. +// pos: Current index into the string. You can assign to it +// to continue lexing from a different position. After +// calling next(), it is the ending index of the most +// recent lexeme. +// lastPos: The starting index of the most recent lexeme. +// Equal to `pos - text.length`. +// text: Text of the last lexeme as a string. +// type: Type of the last lexeme, as returned by `next()`. +// divisionPermitted: Whether a '/' character should be interpreted +// as division rather than the start of a regular expression. +// This flag is set automatically during lexing based on the +// previous token (i.e. the most recent token lexeme), but +// it is technically only a heuristic. +// Thie flag can be read and set manually to affect the +// parsing of the next token. + +JSLexer = function (code) { + this.code = code; + this.pos = 0; + this.divisionPermitted = false; + this.lastLexeme = null; +}; + +JSLexer.Lexeme = Lexeme; + +// XXXX UPDATE DOCS +// Return the type of the next of lexeme starting at `pos`, and advance +// `pos` to the end of the lexeme. The text of the lexeme is available +// in `text`. The text is always the substring of `code` between the +// old and new values of `pos`. An "EOF" lexeme terminates +// the stream. "ERROR" lexemes indicate a bad input string. Out of all +// lexemes, only "EOF" has empty text, and it always has empty text. +// All others contain at least one character from the source code. +// +// Lexeme types: +// Literals: BOOLEAN, NULL, REGEX, NUMBER, STRING +// Whitespace-like: WHITESPACE, COMMENT, NEWLINE, EOF +// Other Tokens: IDENTIFIER, KEYWORD, PUNCTUATION +// ... and ERROR + +JSLexer.prototype.next = function () { + var self = this; + var code = self.code; + var origPos = self.pos; + var divisionPermitted = self.divisionPermitted; + + if (origPos > code.length) + throw new Error("out of range"); + + // Running regexes inside this function will move this local + // `pos` forward. + // When we commit to emitting a lexeme, we'll set self.pos + // based on it. + var pos = origPos; + + // Emit a lexeme. Always called as `return lexeme(type)`. + var lexeme = function (type) { + // If `pos` hasn't moved, we consider this an error. + // This means that grammar cases that only run one regex + // or an alternation ('||') of regexes don't need to + // check for failure. + // This also guarantees that only EOF lexemes are empty. + if (pos === origPos && type !== 'EOF') { + type = 'ERROR'; + pos = origPos + 1; + } + self.pos = pos; + var lex = new JSLexer.Lexeme(origPos, type, code.substring(origPos, pos)); + if (self.lastLexeme) { + self.lastLexeme._next = lex; + lex._prev = self.lastLexeme; + } + self.lastLexeme = lex; + if (lex.isToken()) + self.divisionPermitted = guessIsDivisionPermittedAfterToken(lex); + return lex; + }; + + if (pos === code.length) + return lexeme('EOF'); + + // Result of the regex match in the most recent call to `run`. + var match = null; + + // Run a regex starting from `pos`, recording the end of the matched + // string in `pos` and the match data in `match`. The regex must have + // the 'g' (global) flag. If it doesn't match at `pos`, set `match` + // to null. The caller should expect the regex to match at `pos`, as + // failure is too expensive to run in a tight loop. + var run = function (regex) { + // Cause regex matching to start at `pos`. + regex.lastIndex = pos; + match = regex.exec(code); + // Simulate "sticky" matching by throwing out the match if it + // didn't match exactly at `pos`. If it didn't, we may have + // just searched the entire string. + if (match && (match.index !== pos)) + match = null; + // Record the end position of the match back into `pos`. + if (match) + pos = regex.lastIndex; + return match; + }; + + // Decide which case of the grammar we are in based on one or two + // characters, then roll back `pos`. + run(rDecider); + pos = origPos; + + // Grammar cases + if (match[1]) { // \s + run(rWhiteSpace); + return lexeme('WHITESPACE'); + } + if (match[2]) { // one of //, /*, / + if (match[2] === '//') { + run(rSingleLineComment); + return lexeme('COMMENT'); + } + if (match[2] === '/*') { + run(rMultiLineComment); + return lexeme(match ? 'COMMENT' : 'ERROR'); + } + if (match[2] === '/') { + if (divisionPermitted) { + run(rDivPunctuator); + return lexeme('PUNCTUATION'); + } else { + run(rRegexLiteral); + if (! match) + return lexeme('ERROR'); + run(rRegexFlags); + return lexeme('REGEX'); + } + } + } + if (match[3]) { // any other punctuation char + run(rPunctuator); + return lexeme(match ? 'PUNCTUATION' : 'ERROR'); + } + if (match[4]) { // 0-9 + run(rDecLiteral) || run(rHexLiteral) || run(rOctLiteral); + return lexeme(match ? 'NUMBER' : 'ERROR'); + } + if (match[5]) { // " or ' + run(rStringQuote); + var quote = match[0]; + do { + run(rStringMiddle) || run(rEscapeSequence) || run(rStringQuote); + } while (match && match[0] !== quote); + if (! (match && match[0] === quote)) + return lexeme('ERROR'); + return lexeme('STRING'); + } + if (match[7]) { // non-dot (line terminator) + run(rLineTerminator); + return lexeme('NEWLINE'); + } + // dot (any non-line-terminator) + run(rIdentifierPrefix); + // Use non-short-circuiting OR, '|', to allow matching + // both regexes in sequence, returning false only if neither + // matched. + while (run(rIdentifierMiddle) | run(rIdentifierPrefix)) {/*continue*/} + var word = code.substring(origPos, pos); + return lexeme(keywordLookup[' '+word] || 'IDENTIFIER'); +}; + +})(); diff --git a/packages/jsparse/package.js b/packages/jsparse/package.js new file mode 100644 index 0000000000..3cb8c685a7 --- /dev/null +++ b/packages/jsparse/package.js @@ -0,0 +1,20 @@ +Package.describe({ + summary: "Full-featured JavaScript parser", + internal: true +}); + +Package.on_use(function (api) { + api.add_files(['lexer.js', 'parserlib.js', 'stringify.js', 'parser.js'], + ['client', 'server']); +}); + +Package.on_test(function (api) { + api.use('tinytest'); + api.use('jsparse', 'client'); + + api.add_files('parser_tests.js', + // Test just on client for faster running; should run + // identically on server. + 'client'); + //['client', 'server']); +}); diff --git a/packages/jsparse/parser.js b/packages/jsparse/parser.js new file mode 100644 index 0000000000..a2021f3298 --- /dev/null +++ b/packages/jsparse/parser.js @@ -0,0 +1,761 @@ +///// JAVASCRIPT PARSER + +// What we don't support from ECMA-262 5.1: +// - object literal trailing comma +// - object literal get/set + +(function () { + +var expecting = Parser.expecting; + +var assertion = Parsers.assertion; +var node = Parsers.node; +var or = Parsers.or; +var and = Parsers.and; +var not = Parsers.not; +var list = Parsers.list; +var seq = Parsers.seq; +var opt = Parsers.opt; +var constant = Parsers.constant; +var mapResult = Parsers.mapResult; + + +var makeSet = function (array) { + var s = {}; + for (var i = 0, N = array.length; i < N; i++) + s[array[i]] = true; + return s; +}; + + +JSParser = function (code, options) { + this.lexer = new JSLexer(code); + this.oldToken = null; + this.newToken = null; + this.pos = 0; + this.isLineTerminatorHere = false; + + options = options || {}; + // pass {tokens:'strings'} to get strings for + // tokens instead of token objects + if (options.tokens === 'strings') { + this.tokenFunc = function (tok) { + return tok.text(); + }; + } else { + this.tokenFunc = function (tok) { + return tok; + }; + } + + this.consumeNewToken(); +}; + +JSParser.prototype.consumeNewToken = function () { + var self = this; + var lexer = self.lexer; + self.oldToken = self.newToken; + self.isLineTerminatorHere = false; + var lex; + do { + lex = lexer.next(); + if (lex.isError()) + throw new Error("Bad token at position " + lex.startPos() + + ", text `" + lex.text() + "`"); + else if (lex.type() === "NEWLINE") + self.isLineTerminatorHere = true; + else if (lex.type() === "COMMENT" && ! /^.*$/.test(lex.text())) + // multiline comments containing line terminators count + // as line terminators. + self.isLineTerminatorHere = true; + } while (! lex.isEOF() && ! lex.isToken()); + self.newToken = lex; + self.pos = lex.startPos(); +}; + +JSParser.prototype.getParseError = function (expecting, found) { + var msg = (expecting ? "Expected " + expecting : "Unexpected token"); + if (this.oldToken) + msg += " after " + this.oldToken; + var pos = this.pos; + msg += " at position " + pos; + msg += ", found " + (found || this.newToken); + return new Error(msg); +}; + +JSParser.prototype.getSyntaxTree = function () { + var self = this; + + var NIL = new ParseNode('nil', []); + + var booleanFlaggedParser = function (parserConstructFunc) { + return { + false: parserConstructFunc(false), + true: parserConstructFunc(true) + }; + }; + + // Takes a space-separated list of either punctuation or keyword tokens + var lookAheadToken = function (tokens) { + var type = (/\w/.test(tokens) ? 'KEYWORD' : 'PUNCTUATION'); + var textSet = makeSet(tokens.split(' ')); + return expecting( + tokens.split(' ').join(', '), + assertion(function (t) { + return (t.newToken.type() === type && textSet[t.newToken.text()]); + })); + }; + + var lookAheadTokenType = function (type) { + return expecting(type, assertion(function (t) { + return t.newToken.type() === type; + })); + }; + + // Takes a space-separated list of either punctuation or keyword tokens + var token = function (tokens) { + var type = (/\w/.test(tokens) ? 'KEYWORD' : 'PUNCTUATION'); + var textSet = makeSet(tokens.split(' ')); + return new Parser( + tokens.split(' ').join(', '), + function (t) { + if (t.newToken.type() === type && textSet[t.newToken.text()]) { + t.consumeNewToken(); + return self.tokenFunc(t.oldToken); + } + return null; + }); + }; + + var tokenType = function (type) { + return new Parser(type, function (t) { + if (t.newToken.type() === type) { + t.consumeNewToken(); + return self.tokenFunc(t.oldToken); + } + return null; + }); + }; + + var noLineTerminatorHere = expecting( + 'noLineTerminator', assertion(function (t) { + return ! t.isLineTerminatorHere; + })); + + var nonLHSExpressionNames = makeSet( + 'unary binary postfix ternary assignment comma'.split(' ')); + var isExpressionLHS = function (exprNode) { + return ! nonLHSExpressionNames[exprNode.name]; + }; + + // Like token, but marks tokens that need to defy the lexer's + // heuristic about whether the next '/' is a division or + // starts a regex. + var preSlashToken = function (text, divisionNotRegex) { + var inner = token(text); + return new Parser( + inner.expecting, + function (t) { + // temporarily set divisionPermitted, + // restoring it if we don't match. + var oldValue = t.lexer.divisionPermitted; + var result; + try { + t.lexer.divisionPermitted = divisionNotRegex; + result = inner.parse(t); + return result; + } finally { + if (! result) + t.lexer.divisionPermitted = oldValue; + } + }); + }; + + // Mark some productions "lazy" to allow grammar circularity, i.e. accessing + // later parsers from earlier ones. + // These lazy versions will be replaced with real ones, which they will + // access when run. + var expressionMaybeNoIn = { + 'false': Parsers.lazy( + 'expression', + function () { return expressionMaybeNoIn[false]; }), + 'true': Parsers.lazy( + 'expression', + function () { return expressionMaybeNoIn[true]; }) + }; + var expression = expressionMaybeNoIn[false]; + + var assignmentExpressionMaybeNoIn = { + 'false': Parsers.lazy( + 'expression', + function () { return assignmentExpressionMaybeNoIn[false]; }), + 'true': Parsers.lazy( + 'expression', + function () { return assignmentExpressionMaybeNoIn[true]; }) + }; + var assignmentExpression = assignmentExpressionMaybeNoIn[false]; + + var functionBody = Parsers.lazy( + 'statement', function () { return functionBody; }); + var statement = Parsers.lazy( + 'statement', function () { return statement; }); + //// + + var arrayLiteral = + node('array', + seq(token('['), + opt(list(token(','))), + or( + lookAheadToken(']'), + list( + expecting( + 'expression', + or(assignmentExpression, + // count a peeked-at ']' as an expression + // to support elisions at end, e.g. + // `[1,2,3,,,,,,]`. + lookAheadToken(']'))), + // list seperator is one or more commas + // to support elision + list(token(',')))), + token(']'))); + + var propertyName = expecting('propertyName', or( + node('idPropName', tokenType('IDENTIFIER')), + node('numPropName', tokenType('NUMBER')), + node('strPropName', tokenType('STRING')))); + var nameColonValue = expecting( + 'propertyName', + node('prop', seq(propertyName, token(':'), assignmentExpression))); + + var objectLiteral = + node('object', + seq(token('{'), + or(lookAheadToken('}'), + list(nameColonValue, + token(','))), + token('}'))); + + var functionMaybeNameRequired = booleanFlaggedParser( + function (nameRequired) { + return seq(token('function'), + (nameRequired ? tokenType('IDENTIFIER') : + or(tokenType('IDENTIFIER'), + and(lookAheadToken('('), constant(NIL)))), + token('('), + or(lookAheadToken(')'), + list(tokenType('IDENTIFIER'), token(','))), + token(')'), + token('{'), + functionBody, + token('}')); + }); + var functionExpression = node('functionExpr', + functionMaybeNameRequired[false]); + + var primaryOrFunctionExpression = + expecting('expression', + or(node('this', token('this')), + node('identifier', tokenType('IDENTIFIER')), + node('number', tokenType('NUMBER')), + node('boolean', tokenType('BOOLEAN')), + node('null', tokenType('NULL')), + node('regex', tokenType('REGEX')), + node('string', tokenType('STRING')), + node('parens', + seq(token('('), expression, token(')'))), + arrayLiteral, + objectLiteral, + functionExpression)); + + var dotEnding = seq(token('.'), tokenType('IDENTIFIER')); + var bracketEnding = seq(token('['), expression, token(']')); + var callArgs = seq(token('('), + or(lookAheadToken(')'), + list(assignmentExpression, + token(','))), + token(')')); + + var newKeyword = token('new'); + + // This is a completely equivalent refactor of the spec's production + // for a LeftHandSideExpression. + // + // An lhsExpression is basically an expression that can serve as + // the left-hand-side of an assignment, though function calls and + // "new" invocation are included because they have the same + // precedence. Actually, the spec technically allows a function + // call to "return" a valid l-value, as in `foo(bar) = baz`, + // though no built-in or user-specifiable call has this property + // (it would have to be defined by a browser or other "host"). + var lhsExpression = new Parser( + 'expression', + function (t) { + // Accumulate all initial "new" keywords, not yet knowing + // if they have a corresponding argument list later. + var news = []; + var n; + while ((n = newKeyword.parse(t))) + news.push(n); + + // Read the primaryOrFunctionExpression that will be the "core" + // of this lhsExpression. It is preceded by zero or more `new` + // keywords, and followed by any sequence of (...), [...], + // and .foo add-ons. + // if we have 'new' keywords, we are committed and must + // match an expression or error. + var result = primaryOrFunctionExpression.parseRequiredIf(t, news.length); + if (! result) + return null; + + // Our plan of attack is to apply each dot, bracket, or call + // as we come across it. Whether a call is a `new` call depends + // on whether there are `new` keywords we haven't used. If so, + // we pop one off the stack. + var done = false; + while (! done) { + var r; + if ((r = dotEnding.parse(t))) { + result = new ParseNode('dot', [result].concat(r)); + } else if ((r = bracketEnding.parse(t))) { + result = new ParseNode('bracket', [result].concat(r)); + } else if ((r = callArgs.parse(t))) { + if (news.length) + result = new ParseNode('newcall', [news.pop(), result].concat(r)); + else + result = new ParseNode('call', [result].concat(r)); + } else { + done = true; + } + } + + // There may be more `new` keywords than calls, which is how + // paren-less constructions (`new Date`) are parsed. We've + // already handled `new foo().bar()`, now handle `new new foo().bar`. + while (news.length) + result = new ParseNode('new', [news.pop(), result]); + + return result; + }); + + var postfixToken = token('++ --'); + var postfixLookahead = lookAheadToken('++ --'); + var postfixExpression = expecting( + 'expression', + mapResult(seq(lhsExpression, + opt(and(noLineTerminatorHere, + postfixLookahead, + postfixToken))), + function (v) { + if (v.length === 1) + return v[0]; + return new ParseNode('postfix', v); + })); + + var unaryExpression = Parsers.unary( + 'unary', postfixExpression, + or(token('delete void typeof'), + preSlashToken('++ -- + - ~ !', false))); + + // The "noIn" business is all to facilitate parsing + // of for-in constructs, though the cases that make + // this required are quite obscure. + // The `for(var x in y)` form is allowed to take + // an initializer for `x` (which is only useful for + // its side effects, or if `y` has no properties). + // So an example might be: + // `for(var x = a().b in c);` + // In this example, `var x = a().b` is parsed without + // the `in`, which would otherwise be part of the + // varDecl, using varDeclNoIn. + + // Our binaryExpression is the spec's LogicalORExpression, + // which includes all the higher-precendence operators. + var binaryExpressionMaybeNoIn = booleanFlaggedParser( + function (noIn) { + // high to low precedence + var binaryOps = [token('* / %'), + token('+ -'), + token('<< >> >>>'), + or(token('< > <= >='), + noIn ? token('instanceof') : + token('instanceof in')), + token('== != === !=='), + token('&'), + token('^'), + token('|'), + token('&&'), + token('||')]; + return expecting( + 'expression', + Parsers.binaryLeft('binary', unaryExpression, binaryOps)); + }); + var binaryExpression = binaryExpressionMaybeNoIn[false]; + + var conditionalExpressionMaybeNoIn = booleanFlaggedParser( + function (noIn) { + return expecting( + 'expression', + mapResult( + seq(binaryExpressionMaybeNoIn[noIn], + opt(seq( + token('?'), + assignmentExpression, token(':'), + assignmentExpressionMaybeNoIn[noIn]))), + function (v) { + if (v.length === 1) + return v[0]; + return new ParseNode('ternary', v); + })); + }); + var conditionalExpression = conditionalExpressionMaybeNoIn[false]; + + var assignOp = token('= *= /= %= += -= <<= >>= >>>= &= ^= |='); + + assignmentExpressionMaybeNoIn = booleanFlaggedParser( + function (noIn) { + return new Parser( + 'expression', + function (t) { + var r = conditionalExpressionMaybeNoIn[noIn].parse(t); + if (! r) + return null; + + // Assignment is right-associative. + // Plan of attack: make a list of all the parts + // [expression, op, expression, op, ... expression] + // and then fold them up at the end. + var parts = [r]; + var op; + while (isExpressionLHS(r) &&(op = assignOp.parse(t))) + parts.push(op, + conditionalExpressionMaybeNoIn[noIn].parseRequired(t)); + + var result = parts.pop(); + while (parts.length) { + op = parts.pop(); + var lhs = parts.pop(); + result = new ParseNode('assignment', [lhs, op, result]); + } + return result; + }); + }); + assignmentExpression = assignmentExpressionMaybeNoIn[false]; + + expressionMaybeNoIn = booleanFlaggedParser( + function (noIn) { + return expecting( + 'expression', + mapResult( + list(assignmentExpressionMaybeNoIn[noIn], token(',')), + function (v) { + if (v.length === 1) + return v[0]; + return new ParseNode('comma', v); + })); + }); + expression = expressionMaybeNoIn[false]; + + // STATEMENTS + + var statements = list(statement); + + // implements JavaScript's semicolon "insertion" rules + var maybeSemicolon = expecting( + 'semicolon', + or(token(';'), + and( + or( + lookAheadToken('}'), + lookAheadTokenType('EOF'), + assertion(function (t) { + return t.isLineTerminatorHere; + })), + constant(new ParseNode(';', []))))); + + var expressionStatement = node( + 'expressionStmnt', + and( + not(or(lookAheadToken('{'), lookAheadToken('function'))), + seq(expression, + expecting('semicolon', + or(maybeSemicolon, + // allow presence of colon to terminate + // statement legally, for the benefit of + // expressionOrLabelStatement. Basically assume + // an implicit semicolon. This + // is safe because a colon can never legally + // follow a semicolon anyway. + and(lookAheadToken(':'), + constant(new ParseNode(';', [])))))))); + + // it's hard to parse statement labels, as in + // `foo: x = 1`, because we can't tell from the + // first token whether we are looking at an expression + // statement or a label statement. To work around this, + // expressionOrLabelStatement parses the expression and + // then rewrites the result if it is an identifier + // followed by a colon. + var labelColonAndStatement = seq(token(':'), statement); + var noColon = expecting( + 'semicolon', not(lookAheadToken(':'))); + var expressionOrLabelStatement = new Parser( + null, + function (t) { + var exprStmnt = expressionStatement.parse(t); + if (! exprStmnt) + return null; + + var expr = exprStmnt.children[0]; + var maybeSemi = exprStmnt.children[1]; + if (expr.name !== 'identifier' || + ! (maybeSemi instanceof ParseNode)) { + // We either have a non-identifier expression or a present + // semicolon. This is not a label. + // + // Fail now if we are looking at a colon, causing an + // error message on input like `1+1:` of the same kind + // you'd get without statement label parsing. + noColon.parseRequired(t); + return exprStmnt; + } + + var rest = labelColonAndStatement.parse(t); + if (! rest) + return exprStmnt; + + return new ParseNode('labelStmnt', + [expr.children[0]].concat(rest)); + }); + + var emptyStatement = node('emptyStmnt', token(';')); // required semicolon + + var blockStatement = expecting('block', node('blockStmnt', seq( + token('{'), or(lookAheadToken('}'), statements), + token('}')))); + + var varDeclMaybeNoIn = booleanFlaggedParser(function (noIn) { + return node( + 'varDecl', + seq(tokenType('IDENTIFIER'), + opt(seq(token('='), + assignmentExpressionMaybeNoIn[noIn])))); + }); + var varDecl = varDeclMaybeNoIn[false]; + + var variableStatement = node( + 'varStmnt', + seq(token('var'), list(varDecl, token(',')), + maybeSemicolon)); + + // A paren that may be followed by a statement + // beginning with a regex literal. + var closeParenBeforeStatement = preSlashToken(')', false); + + var ifStatement = node( + 'ifStmnt', + seq(token('if'), token('('), expression, + closeParenBeforeStatement, statement, + opt(seq(token('else'), statement)))); + + var secondThirdClauses = expecting( + 'semicolon', + and(lookAheadToken(';'), + seq( + expecting('semicolon', token(';')), + or(and(lookAheadToken(';'), + constant(NIL)), + expression), + expecting('semicolon', token(';')), + or(and(lookAheadToken(')'), + constant(NIL)), + expression)))); + var inExpr = seq(token('in'), expression); + var inExprExpectingSemi = expecting('semicolon', + seq(token('in'), expression)); + var forSpec = mapResult(node( + 'forSpec', + or(seq(token('var'), + varDeclMaybeNoIn[true], + expecting( + 'commaOrIn', + or(inExpr, + seq( + or( + lookAheadToken(';'), + seq(token(','), + list(varDeclMaybeNoIn[true], token(',')))), + secondThirdClauses)))), + // get the case where the first clause is empty out of the way. + // the lookAhead's return value is the empty placeholder for the + // missing expression. + seq(and(lookAheadToken(';'), + constant(NIL)), secondThirdClauses), + // custom parser the non-var case because we have to + // read the first expression before we know if there's + // an "in". + new Parser( + null, + function (t) { + var firstExpr = expressionMaybeNoIn[true].parse(t); + if (! firstExpr) + return null; + var rest = secondThirdClauses.parse(t); + if (! rest) { + // we need a left-hand-side expression for a + // `for (x in y)` loop. + if (! isExpressionLHS(firstExpr)) + throw t.getParseError("semicolon"); + // if we don't see 'in' at this point, it's probably + // a missing semicolon + rest = inExprExpectingSemi.parseRequired(t); + } + + return [firstExpr].concat(rest); + }))), + function (clauses) { + // There are four kinds of for-loop, and we call the + // part between the parens one of forSpec, forVarSpec, + // forInSpec, and forVarInSpec. Having parsed it + // already, we rewrite the node name based on how + // many items came out. forIn and forVarIn always + // have 3 and 4 items respectively. for has 5 + // (the optional expressions are present as nils). + // forVar has 6 or more, because `for(var x;;);` + // produces [`var` `x` `;` nil `;` nil]. + var numChildren = clauses.children.length; + if (numChildren === 3) + return new ParseNode('forInSpec', clauses.children); + else if (numChildren === 4) + return new ParseNode('forVarInSpec', clauses.children); + else if (numChildren >= 6) + return new ParseNode('forVarSpec', clauses.children); + return clauses; + }); + + var iterationStatement = or( + node('doStmnt', seq(token('do'), statement, token('while'), + token('('), expression, token(')'), + maybeSemicolon)), + node('whileStmnt', seq(token('while'), token('('), expression, + closeParenBeforeStatement, statement)), + // semicolons must be real, not maybeSemicolons + node('forStmnt', seq( + token('for'), token('('), forSpec, closeParenBeforeStatement, + statement))); + + var returnStatement = node( + 'returnStmnt', + seq(token('return'), or( + and(noLineTerminatorHere, expression), constant(NIL)), + maybeSemicolon)); + var continueStatement = node( + 'continueStmnt', + seq(token('continue'), or( + and(noLineTerminatorHere, tokenType('IDENTIFIER')), constant(NIL)), + maybeSemicolon)); + var breakStatement = node( + 'breakStmnt', + seq(token('break'), or( + and(noLineTerminatorHere, tokenType('IDENTIFIER')), constant(NIL)), + maybeSemicolon)); + var throwStatement = node( + 'throwStmnt', + seq(token('throw'), + and(or(noLineTerminatorHere, + // If there is a line break here and more tokens after, + // we want to error appropriately. `throw \n e` should + // complain about the "end of line", not the `e`. + and(not(lookAheadTokenType("EOF")), + new Parser(null, + function (t) { + throw t.getParseError('expression', 'end of line'); + }))), + expression), + maybeSemicolon)); + + var withStatement = node( + 'withStmnt', + seq(token('with'), token('('), expression, closeParenBeforeStatement, + statement)); + + var switchCase = node( + 'case', + seq(token('case'), expression, token(':'), + or(lookAheadToken('}'), + lookAheadToken('case default'), + statements))); + var switchDefault = node( + 'default', + seq(token('default'), token(':'), + or(lookAheadToken('}'), + lookAheadToken('case'), + statements))); + + var switchStatement = node( + 'switchStmnt', + seq(token('switch'), token('('), expression, token(')'), + token('{'), + or(lookAheadToken('}'), + lookAheadToken('default'), + list(switchCase)), + opt(seq(switchDefault, + opt(list(switchCase)))), + token('}'))); + + var catchFinally = expecting( + 'catch', + and(lookAheadToken('catch finally'), + seq( + or(node( + 'catch', + seq(token('catch'), token('('), tokenType('IDENTIFIER'), + token(')'), blockStatement)), + constant(NIL)), + or(node( + 'finally', + seq(token('finally'), blockStatement)), + constant(NIL))))); + var tryStatement = node( + 'tryStmnt', + seq(token('try'), blockStatement, catchFinally)); + var debuggerStatement = node( + 'debuggerStmnt', seq(token('debugger'), maybeSemicolon)); + + statement = expecting('statement', + or(expressionOrLabelStatement, + emptyStatement, + blockStatement, + variableStatement, + ifStatement, + iterationStatement, + returnStatement, + continueStatement, + breakStatement, + withStatement, + switchStatement, + throwStatement, + tryStatement, + debuggerStatement)); + + // PROGRAM + + var functionDecl = node( + 'functionDecl', functionMaybeNameRequired[true]); + + var sourceElement = or(functionDecl, statement); + var sourceElements = list(sourceElement); + + functionBody = expecting( + 'functionBody', or(lookAheadToken('}'), sourceElements)); + + var program = node( + 'program', + seq(opt(sourceElements), + // If not at EOF, complain "expecting statement" + expecting('statement', lookAheadTokenType("EOF")))); + + return program.parse(this); +}; + +})(); \ No newline at end of file diff --git a/packages/jsparse/parser_tests.js b/packages/jsparse/parser_tests.js new file mode 100644 index 0000000000..4973395329 --- /dev/null +++ b/packages/jsparse/parser_tests.js @@ -0,0 +1,607 @@ + +var allNodeNames = [ + ";", + "array", + "assignment", + "binary", + "blockStmnt", + "boolean", + "bracket", + "breakStmnt", + "call", + "case", + "catch", + "comma", + "continueStmnt", + "debuggerStmnt", + "default", + "doStmnt", + "dot", + "emptyStmnt", + "expressionStmnt", + "finally", + "forInSpec", + "forSpec", + "forStmnt", + "forVarInSpec", + "forVarSpec", + "functionDecl", + "functionExpr", + "idPropName", + "identifier", + "ifStmnt", + "labelStmnt", + "new", + "newcall", + "nil", + "null", + "numPropName", + "number", + "object", + "parens", + "postfix", + "program", + "prop", + "regex", + "returnStmnt", + "strPropName", + "string", + "switchStmnt", + "ternary", + "this", + "throwStmnt", + "tryStmnt", + "unary", + "varDecl", + "varStmnt", + "whileStmnt", + "withStmnt" +]; + +var allNodeNamesSet = {}; +_.each(allNodeNames, function (n) { allNodeNamesSet[n] = true; }); + + +var makeTester = function (test) { + return { + // Parse code and make sure it matches expectedTreeString. + goodParse: function (code, expectedTreeString, regexTokenHints) { + var expectedTree = ParseNode.unstringify(expectedTreeString); + + // first use lexer to collect all tokens + var lexer = new JSLexer(code); + var allTokensInOrder = []; + while (! lexer.next().isEOF()) { + var lex = lexer.lastLexeme; + if (lex.isError()) + test.fail("Lexer error at " + lex.startPos()); + if (lex.isToken()) + allTokensInOrder.push(lex); + if (regexTokenHints && regexTokenHints[allTokensInOrder.length]) + lexer.divisionPermitted = false; + } + + var parser = new JSParser(code); + var actualTree = parser.getSyntaxTree(); + + var nextTokenIndex = 0; + var check = function (tree) { + if (tree instanceof ParseNode) { + // This is a NODE (non-terminal). + var nodeName = tree.name; + if (! (nodeName && typeof nodeName === "string" && + allNodeNamesSet[nodeName] === true)) + test.fail("Not a node name: " + nodeName); + _.each(tree.children, check); + } else if (typeof tree === 'object' && + typeof tree.text === 'function') { + // This is a TOKEN (terminal). + // Make sure we are visiting every token once, in order. + if (nextTokenIndex >= allTokensInOrder.length) + test.fail("Too many tokens: " + (nextTokenIndex + 1)); + var referenceToken = allTokensInOrder[nextTokenIndex++]; + if (tree.text() !== referenceToken.text()) + test.fail(tree.text() + " !== " + referenceToken.text()); + if (tree.startPos() !== referenceToken.startPos()) + test.fail(tree.startPos() + " !== " + referenceToken.startPos()); + if (code.substring(tree.startPos(), tree.endPos()) !== tree.text()) + test.fail("Didn't see " + tree.text() + " at " + tree.startPos() + + " in " + code); + } else { + test.fail("Unknown tree part: " + tree); + } + }; + + check(actualTree); + if (nextTokenIndex !== allTokensInOrder.length) + test.fail("Too few tokens: " + nextTokenIndex); + + test.equal(parser.pos, code.length); + + test.equal(ParseNode.stringify(actualTree), + ParseNode.stringify(expectedTree), code); + }, + // Takes code with part of it surrounding with backticks. + // Removes the two backtick characters, tries to parse the code, + // and then asserts that there was a tokenization-level error, + // with the part that was between the backticks called out as + // the bad token. + // + // For example, the test "123`@`" will try to parse "123@" and + // assert that a tokenization error occurred at '@'. + badToken: function (code) { + var constructMessage = function (pos, text) { + return "Bad token at position " + pos + ", text `" + text + "`"; + }; + var pos = code.indexOf('`'); + var text = code.match(/`(.*?)`/)[1]; + code = code.replace(/`/g, ''); + + var parsed = false; + var error = null; + try { + var tree = new JSParser(code).getSyntaxTree(); + parsed = true; + } catch (e) { + error = e; + } + test.isFalse(parsed); + test.isTrue(error); + test.equal(error.message, constructMessage(pos, text)); + }, + // Takes code with a backtick-quoted string embedded in it. + // Removes the backticks and their contents, tries to parse the code, + // and then asserts that there was a parse error at the location + // where the backtick-quoted string was embedded. The embedded + // string must match whatever the error message says was "expected". + // + // For example, the test "{`statement`" will try to parse the code + // "{" and then assert that an error occured at the end of the string + // saying "Expected statement". The test "1 `semicolon`2" will try + // to parse "1 2" and assert that the error "Expected semicolon" + // appeared after the space and before the 2. + // + // A second backtick-quoted string is used as the "found" token + // in the error message. + badParse: function (code) { + var constructMessage = function (whatExpected, pos, found, after) { + return "Expected " + whatExpected + (after ? " after " + after : "") + + " at position " + pos + ", found " + found; + }; + var pos = code.indexOf('`'); + + var backticked = code.match(/`.*?`/g); + var whatExpected = backticked[0] && backticked[0].slice(1,-1); + var found = backticked[1] && backticked[1].slice(1, -1); + code = code.replace(/`.*?`/g, ''); + + var parsed = false; + var error = null; + var parser = new JSParser(code); + try { + var tree = parser.getSyntaxTree(); + parsed = true; + } catch (e) { + error = e; + } + test.isFalse(parsed); + test.isTrue(error); + if (! parsed && error) { + var after = parser.oldToken; + found = (found || parser.newToken); + test.equal(error.message, + constructMessage(whatExpected, pos, found, after)); + } + } + }; +}; + + +Tinytest.add("jsparse - basics", function (test) { + var tester = makeTester(test); + tester.goodParse('1', "program(expressionStmnt(number(1) ;()))"); + tester.goodParse('1 + 1', "program(expressionStmnt(binary(number(1) + number(1)) ;()))"); + tester.goodParse('1*2+3*4', "program(expressionStmnt(binary(binary(number(1) * number(2)) + " + + "binary(number(3) * number(4))) ;()))"); + tester.goodParse('1 + 1;', "program(expressionStmnt(binary(number(1) + number(1)) ;))"); + tester.goodParse('1 + 1;;', "program(expressionStmnt(binary(number(1) + number(1)) ;) emptyStmnt(;))"); + tester.goodParse('', "program()"); + tester.goodParse('\n', "program()"); + tester.goodParse(';;;\n\n;\n', "program(emptyStmnt(;) emptyStmnt(;) emptyStmnt(;) emptyStmnt(;))"); + tester.goodParse('foo', "program(expressionStmnt(identifier(foo) ;()))"); + tester.goodParse('foo();', "program(expressionStmnt(call(identifier(foo) `(` `)`) ;))"); + tester.goodParse('var x = 3', "program(varStmnt(var varDecl(x = number(3)) ;()))"); + tester.goodParse('++x;', "program(expressionStmnt(unary(++ identifier(x)) ;))"); + tester.goodParse('x++;', "program(expressionStmnt(postfix(identifier(x) ++) ;))"); + tester.goodParse( + 'throw new Error', + "program(throwStmnt(throw new(new identifier(Error)) ;()))"); + tester.goodParse( + 'var x = function () { return 123; };', + 'program(varStmnt(var varDecl(x = functionExpr(function nil() `(` `)` ' + + '{ returnStmnt(return number(123) ;) })) ;))'); + + tester.badParse("var x = `expression`"); + tester.badParse("1 `semicolon`1"); + tester.badParse("1+1`semicolon`:"); +}); + +Tinytest.add("jsparse - tokenization errors", function (test) { + var tester = makeTester(test); + tester.badToken("123`@`"); + tester.badToken("thisIsATestOf = `'unterminated `\n strings'"); +}); + +Tinytest.add("jsparse - syntax forms", function (test) { + var tester = makeTester(test); + var trials = [ + // STATEMENTS + ['1', + 'program(expressionStmnt(number(1) ;()))'], + ['1;;;;2', + 'program(expressionStmnt(number(1) ;) emptyStmnt(;) emptyStmnt(;) emptyStmnt(;) ' + + 'expressionStmnt(number(2) ;()))'], + ['{}', + 'program(blockStmnt({ }))'], + ['{null}', + 'program(blockStmnt({ expressionStmnt(null(null) ;()) }))'], + ['{\nfoo()\nbar();\n}', + 'program(blockStmnt({ expressionStmnt(call(identifier(foo) `(` `)`) ;()) ' + + 'expressionStmnt(call(identifier(bar) `(` `)`) ;) }))'], + ['{{{}}}', + 'program(blockStmnt({ blockStmnt({ blockStmnt({ }) }) }))'], + ['var x = y, z,\n a = b = c;', + 'program(varStmnt(var varDecl(x = identifier(y)) , varDecl(z) , varDecl(a = ' + + 'assignment(identifier(b) = identifier(c))) ;))'], + ['if (x === y);', + 'program(ifStmnt(if `(` binary(identifier(x) === identifier(y)) `)` emptyStmnt(;)))'], + ['if (z) return', + 'program(ifStmnt(if `(` identifier(z) `)` returnStmnt(return nil() ;())))'], + ['if (a) b; else c', + 'program(ifStmnt(if `(` identifier(a) `)` expressionStmnt(identifier(b) ;) else ' + + 'expressionStmnt(identifier(c) ;())))'], + ['if (n === 1) { foo(); } else if (n === 2) { bar(); } else { baz(); }', + 'program(ifStmnt(if `(` binary(identifier(n) === number(1)) `)` blockStmnt(' + + '{ expressionStmnt(call(identifier(foo) `(` `)`) ;) }) else ifStmnt(' + + 'if `(` binary(identifier(n) === number(2)) `)` blockStmnt(' + + '{ expressionStmnt(call(identifier(bar) `(` `)`) ;) }) else blockStmnt(' + + '{ expressionStmnt(call(identifier(baz) `(` `)`) ;) }))))'], + ['while (false);', + 'program(whileStmnt(while `(` boolean(false) `)` emptyStmnt(;)))'], + ['while (/foo/.test(bar.baz)) {\n bar = bar.baz;\n}', + 'program(whileStmnt(while `(` call(dot(regex(/foo/) . test) `(` ' + + 'dot(identifier(bar) . baz) `)`) `)` blockStmnt({ expressionStmnt(' + + 'assignment(identifier(bar) = dot(identifier(bar) . baz)) ;) })))'], + ['while (false) while (false);', + 'program(whileStmnt(while `(` boolean(false) `)` ' + + 'whileStmnt(while `(` boolean (false) `)` emptyStmnt(;))))'], + ['do a; while (b);', + 'program(doStmnt(do expressionStmnt(identifier(a) ;) while `(` identifier(b) `)` ;))'], + ['do { x-- } while (x);', + 'program(doStmnt(do blockStmnt({ expressionStmnt(postfix(identifier(x) --) ;()) }) ' + + 'while `(` identifier(x) `)` ;))'], + ['do a\n while (b)\n x++', + 'program(doStmnt(do expressionStmnt(identifier(a) ;()) while `(` identifier(b) `)` ;()) ' + + 'expressionStmnt(postfix(identifier(x) ++) ;()))'], + ["for(;;);", + "program(forStmnt(for `(` forSpec(nil() ; nil() ; nil()) `)` emptyStmnt(;)))"], + ["for(x in y);", + "program(forStmnt(for `(` forInSpec(identifier(x) in identifier(y)) `)` emptyStmnt(;)))"], + ["for(var x in y);", + "program(forStmnt(for `(` forVarInSpec(var varDecl(x) in identifier(y)) `)` emptyStmnt(;)))"], + ["for(var x;;);", + "program(forStmnt(for `(` forVarSpec(var varDecl(x) ; nil() ; nil()) `)` emptyStmnt(;)))"], + ["for(var i=0;i>h>>>ik<=l>=m instanceof n in o==p!=q===r!==s&t^u|v&&w||x", + "program(expressionStmnt(binary(binary(binary(binary(binary(binary(binary(" + + "binary(binary(binary(binary(binary(binary(binary(binary(binary(binary(binary(" + + "binary(binary(binary(binary(binary(identifier(a) * identifier(b)) / " + + "identifier(c)) % identifier(d)) + identifier(e)) - identifier(f)) << identifier(g)) " + + ">> identifier(h)) >>> identifier(i)) < identifier(j)) > identifier(k)) <= " + + "identifier(l)) >= identifier(m)) instanceof identifier(n)) in identifier(o)) == " + + "identifier(p)) != identifier(q)) === identifier(r)) !== identifier(s)) & " + + "identifier(t)) ^ identifier(u)) | identifier(v)) && identifier(w)) || " + + "identifier(x)) ;()))"], + ["a||b&&c|d^e&f!==g===h!=i==j in k instanceof l>=m<=n>>q>>r<= identifier(m)) <= identifier(n)) < " + + "identifier(o)) < binary(binary(binary(identifier(p) >>> identifier(q)) >> " + + "identifier(r)) << binary(binary(identifier(s) - identifier(t)) + " + + "binary(binary(binary(identifier(u) % identifier(v)) / identifier(w)) * " + + "identifier(x))))))))))) ;()))"], + ["a?b:c", + "program(expressionStmnt(ternary(identifier(a) ? identifier(b) : " + + "identifier(c)) ;()))"], + ["1==2?3=4:5=6", + "program(expressionStmnt(ternary(binary(number(1) == number(2)) ? " + + "assignment(number(3) = number(4)) : assignment(number(5) = number(6))) ;()))"], + ["a=b,c=d", + "program(expressionStmnt(comma(assignment(identifier(a) = identifier(b)) , " + + "assignment(identifier(c) = identifier(d))) ;()))"], + ["a=b=c=d", + "program(expressionStmnt(assignment(identifier(a) = assignment(identifier(b) " + + "= assignment(identifier(c) = identifier(d)))) ;()))"], + ["x[0]=x[1]=true", + "program(expressionStmnt(assignment(bracket(identifier(x) [ number(0) ]) = " + + "assignment(bracket(identifier(x) [ number(1) ]) = boolean(true))) ;()))"], + ["a*=b/=c%=d+=e-=f<<=g>>=h>>>=i&=j^=k|=l", + "program(expressionStmnt(assignment(identifier(a) *= assignment(identifier(b) " + + "/= assignment(identifier(c) %= assignment(identifier(d) += " + + "assignment(identifier(e) -= assignment(identifier(f) <<= " + + "assignment(identifier(g) >>= assignment(identifier(h) >>>= " + + "assignment(identifier(i) &= assignment(identifier(j) ^= " + + "assignment(identifier(k) |= identifier(l)))))))))))) ;()))"], + ["1;\n\n\n\n/* foo */\n// bar\n", // trailing whitespace and comments + "program(expressionStmnt(number(1) ;))"] + ]; + _.each(trials, function (tr) { + tester.goodParse(tr[0], tr[1]); + }); +}); + +Tinytest.add("jsparse - bad parses", function (test) { + var tester = makeTester(test); + var trials = [ + '{`statement`', + 'if (`expression`)', + 'if `(`else', + 'var`varDecl`;', + 'while (`expression`);', + 'while`(`;', + 'do a `semicolon`while b;', + 'do a\n while `(`b;', + '1 `semicolon`2', + 'for (`forSpec`);', + 'for (1\n`semicolon`2\n3);', + 'continue `semicolon`1+1;', + 'break `semicolon`1+1;', + 'throw`expression`', + 'throw`expression`;', + 'throw\n`expression`', + 'throw\n`expression``end of line`e', + 'throw `expression`=;', + 'with(`expression`);', + 'switch(`expression`)', + 'switch(x)`{`;', + 'try`block`', + 'try {}`catch`', + 'try {} catch`(`;', + 'try {} catch(e)`block`;', + '1+1`semicolon`:', + '{a:`statement`}', + 'function `IDENTIFIER`() {}', + 'foo: `statement`function foo() {}', + '[`expression`=', + '[,,`expression`=', + '({`propertyName`true:3})', + '({1:2,3`:`})', + '({1:2,`propertyName`', + 'x.`IDENTIFIER`true', + 'foo;`semicolon`:;', + '1;`statement`=', + 'a+b`semicolon`=c;', + 'for(1+1 `semicolon`in {});', + '`statement`=', + 'for(;`expression`var;) {}' + ]; + _.each(trials, function (tr) { + tester.badParse(tr); + }); +}); + +Tinytest.add("jsparse - regex division ambiguity", function (test) { + var tester = makeTester(test); + tester.goodParse("if (e) /f/g;", + "program(ifStmnt(if `(` identifier(e) `)` expressionStmnt(regex(/f/g) ;)))", + {4: true}); + tester.goodParse("++/x/.y;", + "program(expressionStmnt(unary(++ dot(regex(/x/) . y)) ;))", + {1: true}); + tester.goodParse("x++/2/g;", + "program(expressionStmnt(binary(binary(postfix(identifier(x) ++) / " + + "number(2)) / identifier(g)) ;))"); + tester.goodParse("(1+1)/2/g;", + "program(expressionStmnt(binary(binary(parens(`(` binary(number(1) + " + + "number(1)) `)`) / " + + "number(2)) / identifier(g)) ;))"); + tester.goodParse("/x/", + "program(expressionStmnt(regex(/x/) ;()))"); +}); + +Tinytest.add("jsparse - semicolon insertion", function (test) { + var tester = makeTester(test); + // Spec section 7.9.2 + tester.badParse("{ 1 `semicolon`2 } 3"); + tester.goodParse("{ 1\n2 } 3", "program(blockStmnt({ expressionStmnt(number(1) " + + ";()) expressionStmnt(number(2) ;()) }) expressionStmnt(number(3) ;()))"); + tester.badParse("for (a; b\n`semicolon`)"); + tester.goodParse("return\na + b", + "program(returnStmnt(return nil() ;()) " + + "expressionStmnt(binary(identifier(a) + identifier(b)) ;()))"); + tester.goodParse("a = b\n++c", + "program(expressionStmnt(assignment(identifier(a) = identifier(b)) ;())" + + "expressionStmnt(unary(++ identifier(c)) ;()))"); + tester.badParse("if (a > b)\n`statement`else c = d"); + tester.goodParse("a = b + c\n(d + e).print()", + "program(expressionStmnt(assignment(identifier(a) = " + + "binary(identifier(b) + call(dot(call(identifier(c) `(` " + + "binary(identifier(d) + identifier(e)) `)`) . print) `(` `)`))) ;()))"); +}); + +Tinytest.add("jsparse - comments", function (test) { + var tester = makeTester(test); + // newline in multi-line comment makes it into a line break for semicolon + // insertion purposes + tester.badParse("1/**/`semicolon`2"); + tester.goodParse("1/*\n*/2", + "program(expressionStmnt(number(1) ;()) expressionStmnt(number(2) ;()))"); +}); \ No newline at end of file diff --git a/packages/jsparse/parserlib.js b/packages/jsparse/parserlib.js new file mode 100644 index 0000000000..a8bbcac670 --- /dev/null +++ b/packages/jsparse/parserlib.js @@ -0,0 +1,264 @@ +///// TOKENIZER AND PARSER COMBINATORS + +(function () { + +// XXX track line/col position, for errors and maybe token info + +var isArray = function (obj) { + return obj && (typeof obj === 'object') && (typeof obj.length === 'number'); +}; + +ParseNode = function (name, children) { + this.name = name; + this.children = children; + + if (! isArray(children)) + throw new Error("Expected array in new ParseNode(" + name + ", ...)"); +}; + + +Parser = function (expecting, runFunc) { + this.expecting = expecting; + this._run = runFunc; +}; + +Parser.prototype.parse = function (t) { + return this._run(t); +}; + +Parser.prototype.parseRequired = function (t) { + return this.parseRequiredIf(t, true); +}; + +Parser.prototype.parseRequiredIf = function (t, required) { + var result = this._run(t); + + if (required && ! result) + throw t.getParseError(this.expecting); + + return result; +}; + +Parser.expecting = function (expecting, parser) { + return new Parser(expecting, parser._run); +}; + + +// A parser that consume()s has to succeed. +// Similarly, a parser that fails can't have consumed. + +Parsers = {}; + +Parsers.assertion = function (test) { + return new Parser( + null, function (t) { + return test(t) ? [] : null; + }); +}; + +Parsers.node = function (name, childrenParser) { + return new Parser(name, function (t) { + var children = childrenParser.parse(t); + if (! children) + return null; + if (! isArray(children)) + children = [children]; + return new ParseNode(name, children); + }); +}; + +Parsers.or = function (/*parsers*/) { + var args = arguments; + return new Parser( + args[args.length - 1].expecting, + function (t) { + var result; + for(var i = 0, N = args.length; i < N; i++) { + result = args[i].parse(t); + if (result) + return result; + } + return null; + }); +}; + +// Parses a left-recursive expression with zero or more occurrences +// of a binary op. Leaves the term unwrapped if no op. For example +// (in a hypothetical use case): +// `1` => "1" +// `1+2` => ["binary", "1", "+", "2"] +// `1+2+3` => ["binary", ["binary", "1", "+", "2"], "+", "3"] +// +// opParsers is an array of op parsers from high to low +// precedence (tightest-binding first) +Parsers.binaryLeft = function (name, termParser, opParsers) { + var opParser; + + if (opParsers.length === 1) { + // take single opParser out of its array + opParser = opParsers[0]; + } else { + // pop off last opParser (non-destructively) and replace + // termParser with a recursive binaryLeft on the remaining + // ops. + termParser = Parsers.binaryLeft(name, termParser, opParsers.slice(0, -1)); + opParser = opParsers[opParsers.length - 1]; + } + + return new Parser( + termParser.expecting, + function (t) { + var result = termParser.parse(t); + if (! result) + return null; + + var op; + while ((op = opParser.parse(t))) { + result = new ParseNode( + name, + [result, op, termParser.parseRequired(t)]); + } + return result; + }); +}; + +Parsers.unary = function (name, termParser, opParser) { + var unaryList = Parsers.opt(Parsers.list(opParser)); + return new Parser( + termParser.expecting, + function (t) { + var unaries = unaryList.parse(t); + // if we have unaries, we are committed and + // have to match a term or error. + var result = termParser.parseRequiredIf(t, unaries.length); + if (! result) + return null; + + while (unaries.length) + result = new ParseNode(name, [unaries.pop(), result]); + return result; + }); +}; + +// Parses a list of one or more items with a separator, listing the +// items and separators. (Separator is optional.) For example: +// `x` => ["x"] +// `x,y` => ["x", ",", "y"] +// `x,y,z` => ["x", ",", "y", ",", "z"] +// Unpacks. +Parsers.list = function (itemParser, sepParser) { + var push = function(array, newThing) { + if (isArray(newThing)) + array.push.apply(array, newThing); + else + array.push(newThing); + }; + return new Parser( + itemParser.expecting, + function (t) { + var result = []; + var firstItem = itemParser.parse(t); + if (! firstItem) + return null; + push(result, firstItem); + + if (sepParser) { + var sep; + while ((sep = sepParser.parse(t))) { + push(result, sep); + push(result, itemParser.parseRequired(t)); + } + } else { + var item; + while ((item = itemParser.parse(t))) + push(result, item); + } + return result; + }); +}; + +// Unpacks arrays (nested seqs). +Parsers.seq = function (/*parsers*/) { + var args = arguments; + if (! args.length) + return Parsers.constant([]); + + return new Parser( + args[0].expecting, + function (t) { + var result = []; + for (var i = 0, N = args.length; i < N; i++) { + // first item in sequence can fail, and we + // fail (without error); after that, error on failure + var r = args[i].parseRequiredIf(t, i > 0); + if (! r) + return null; + + if (isArray(r)) // append array! + result.push.apply(result, r); + else + result.push(r); + } + return result; + }); +}; + +// parsers except last must never consume +Parsers.and = function (/*parsers*/) { + var args = arguments; + if (! args.length) + return Parsers.constant([]); + + return new Parser( + args[args.length - 1].expecting, + function (t) { + var result; + for(var i = 0, N = args.length; i < N; i++) { + result = args[i].parse(t); + if (! result) + return null; + } + return result; + }); +}; + +// parser must not consume +Parsers.not = function (parser) { + return new Parser( + null, + function (t) { + return parser.parse(t) ? null : []; + }); +}; + +// parser that looks at nothing and returns result +Parsers.constant = function (result) { + return new Parser(null, + function (t) { return result; }); +}; + +Parsers.opt = function (parser) { + return Parser.expecting( + parser.expecting, + Parsers.or(parser, Parsers.seq())); +}; + +Parsers.mapResult = function (parser, func) { + return new Parser( + parser.expecting, + function (t) { + var v = parser.parse(t); + return v ? func(v, t) : null; + }); +}; + +Parsers.lazy = function (expecting, parserFunc) { + var inner = null; + return new Parser(expecting, function (t) { + if (! inner) + inner = parserFunc(); + return inner.parse(t); + }); +}; + +})(); diff --git a/packages/jsparse/stringify.js b/packages/jsparse/stringify.js new file mode 100644 index 0000000000..816b765a64 --- /dev/null +++ b/packages/jsparse/stringify.js @@ -0,0 +1,122 @@ +(function() { + +// The "tree string" format is a simple format for representing syntax trees. +// +// For example, the parse of `x++;` is written as: +// "program(expressionStmnt(postfix(identifier(x) ++) ;))" +// +// A Node is written as "name(item1 item2 item3)", with additional whitespace +// allowed anywhere between the name, parentheses, and items. +// +// Tokens don't need to be escaped unless they contain '(', ')', whitespace, or +// backticks, or are empty. If they do, they can be written enclosed in backticks. +// To escape a backtick within backticks, double it. +// +// `stringify` generates "canonical" tree strings, which have no extra escaping +// or whitespace, just one space between items in a Node. + + +ParseNode.prototype.stringify = function () { + return ParseNode.stringify(this); +}; + +var backtickEscape = function (str) { + if (/[\s()`]/.test(str)) + return '`' + str.replace(/`/g, '``') + '`'; + else if (! str) + return '``'; + else + return str; +}; + +var backtickUnescape = function (str) { + if (str.charAt(0) === '`') { + if (str.length === 1 || str.slice(-1) !== '`') + throw new Error("Mismatched ` in " + str); + if (str.length === 2) + str = ''; + else + str = str.slice(1, -1).replace(/``/g, '`'); + } + return str; +}; + +ParseNode.stringify = function (tree) { + if (tree instanceof ParseNode) { + var str = backtickEscape(tree.name); + str += '('; + var escapedChildren = []; + for(var i = 0, N = tree.children.length; i < N; i++) + escapedChildren.push(ParseNode.stringify(tree.children[i])); + str += escapedChildren.join(' '); + str += ')'; + return str; + } + + // Treat a token object or string as a token. + if (typeof tree.text === 'function') + tree = tree.text(); + else if (typeof tree.text === 'string') + tree = tree.text; + return backtickEscape(String(tree)); +}; + +ParseNode.unstringify = function (str) { + var lexemes = str.match(/\(|\)|`([^`]||``)*`|`|[^\s()`]+/g) || []; + var N = lexemes.length; + var state = { + i: 0, + getParseError: function (expecting) { + throw new Error("unstringify: Expecting " + expecting +", found " + + (lexemes[this.i] || "end of string")); + }, + peek: function () { return lexemes[this.i]; }, + advance: function () { this.i++; } + }; + var paren = function (chr) { + return new Parser(chr, function (t) { + if (t.peek() !== chr) + return null; + t.advance(); + return chr; + }); + }; + var EMPTY_STRING = [""]; + var token = new Parser('token', function (t) { + var txt = t.peek(); + if (!txt || txt.charAt(0) === '(' || txt.charAt(0) === ')') + return null; + + t.advance(); + // can't return falsy value from successful parser + return backtickUnescape(txt) || EMPTY_STRING; + }); + + // Make "item" lazy so it can be recursive. + var item = Parsers.lazy('token', function () { return item; }); + + // Parse a single node or token. + item = Parsers.mapResult( + Parsers.seq(token, + Parsers.opt(Parsers.seq( + paren('('), Parsers.opt(Parsers.list(item)), paren(')')))), + function (v) { + for(var i = 0, N = v.length; i < N; i++) + if (v[i] === EMPTY_STRING) + v[i] = ""; + + if (v.length === 1) + // token + return v[0]; + // node. exclude parens + return new ParseNode(v[0], v.slice(2, -1)); + }); + + var endOfString = new Parser("end of string", function (t) { + return t.i === N ? [] : null; + }); + + return Parsers.seq(item, endOfString).parseRequired(state)[0]; +}; + +})(); \ No newline at end of file diff --git a/packages/less/less_tests.js b/packages/less/less_tests.js index cf983cc7b2..63ae396e0d 100644 --- a/packages/less/less_tests.js +++ b/packages/less/less_tests.js @@ -2,13 +2,12 @@ Tinytest.add("less - presence", function(test) { var d = OnscreenDiv(Meteor.render(function() { - return '

'; })); + return '

'; })); d.node().style.display = 'block'; var p = d.node().firstChild; - var leftBorder = getStyleProperty(p, 'border-left-width'); - test.equal(leftBorder, "13px"); + var leftBorder = getStyleProperty(p, 'border-left-style'); + test.equal(leftBorder, "dashed"); d.kill(); }); - diff --git a/packages/less/less_tests.less b/packages/less/less_tests.less index 2e064a4516..9d2c65c2a3 100644 --- a/packages/less/less_tests.less +++ b/packages/less/less_tests.less @@ -1,8 +1,8 @@ #less-tests { zoom: 1; /* prop this rule open */ } -@unlucky: 13px; +@dashy: dashed; -.less-unlucky-left-border { - border-left: @unlucky solid white; +.less-dashy-left-border { + border-left: 1px @dashy black; } diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index a258968f05..2ef1423ef7 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -337,7 +337,13 @@ _.extend(Meteor._LivedataSession.prototype, { // Store a function to re-run the handler in case we want to rerun // subscriptions, for example when the current user id changes sub._runHandler = function() { - var res = handler.apply(sub, params || []); + try { + var res = handler.apply(sub, params || []); + } catch (e) { + Meteor._debug("Internal exception while starting subscription", sub_id, + e.stack); + return; + } // if Meteor._RemoteCollectionDriver is available (defined in // mongo-livedata), automatically wire up handlers that return a diff --git a/packages/meteor/package.js b/packages/meteor/package.js index 9b93219566..a194a549d0 100644 --- a/packages/meteor/package.js +++ b/packages/meteor/package.js @@ -40,6 +40,11 @@ Package.on_use(function (api, where) { api.use('underscore', ['client', 'server']); api.add_files('dynamics_browser.js', 'client'); api.add_files('dynamics_nodejs.js', 'server'); + + // note server before common. usually it is the other way around, but + // in this case server must load first. + api.add_files('url_server.js', 'server'); + api.add_files('url_common.js', ['client', 'server']); }); Package.on_test(function (api) { @@ -50,4 +55,6 @@ Package.on_test(function (api) { api.add_files('helpers_test.js', ['client', 'server']); api.add_files('dynamics_test.js', ['client', 'server']); + + api.add_files('url_tests.js', ['client', 'server']); }); diff --git a/packages/absolute-url/url_common.js b/packages/meteor/url_common.js similarity index 91% rename from packages/absolute-url/url_common.js rename to packages/meteor/url_common.js index 4c65f71170..c6ac9b2770 100644 --- a/packages/absolute-url/url_common.js +++ b/packages/meteor/url_common.js @@ -27,6 +27,9 @@ !/http:\/\/127\.0\.0\.1[:\/]/.test(url)) // or 127.0.0.1 url = url.replace(/^http:/, 'https:'); + if (options.replaceLocalhost) + url = url.replace(/^http:\/\/localhost([:\/].*)/, 'http://127.0.0.1$1'); + return url; }; diff --git a/packages/absolute-url/url_server.js b/packages/meteor/url_server.js similarity index 100% rename from packages/absolute-url/url_server.js rename to packages/meteor/url_server.js diff --git a/packages/absolute-url/url_tests.js b/packages/meteor/url_tests.js similarity index 64% rename from packages/absolute-url/url_tests.js rename to packages/meteor/url_tests.js index 6548dfc7ab..922fe045f8 100644 --- a/packages/absolute-url/url_tests.js +++ b/packages/meteor/url_tests.js @@ -36,6 +36,27 @@ Tinytest.add("absolute-url - basics", function(test) { test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://127.0.0.1:3000', secure: true}), 'http://127.0.0.1:3000/foo'); + + // test replaceLocalhost + test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://localhost:3000', + replaceLocalhost: true}), + 'http://127.0.0.1:3000/foo'); + test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://localhost', + replaceLocalhost: true}), + 'http://127.0.0.1/foo'); + test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://127.0.0.1:3000', + replaceLocalhost: true}), + 'http://127.0.0.1:3000/foo'); + test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://127.0.0.1', + replaceLocalhost: true}), + 'http://127.0.0.1/foo'); + // don't replace just any localhost + test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://foo.com/localhost', + replaceLocalhost: true}), + 'http://foo.com/localhost/foo'); + test.equal(Meteor.absoluteUrl('foo', {rootUrl: 'http://foo.localhost.com', + replaceLocalhost: true}), + 'http://foo.localhost.com/foo'); }); diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index c5d64c9296..6d165501a1 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -201,7 +201,10 @@ Tinytest.add("minimongo - misc", function (test) { var a = {a: [1, 2, 3], b: "x", c: true, d: {x: 12, y: [12]}, f: null, g: new Date()}; var b = LocalCollection._deepcopy(a); - test.isTrue(LocalCollection._f._equal(a, b)); + // minimongo doesn't support Dates, so we *can't* test + // LocalCollection._f._equal here! (Currently _equal considers all dates equal + // on most browsers except IE7 where it considers all dates unequal.) + test.equal(a, b); a.a.push(4); test.length(b.a, 3); a.c = false; @@ -211,10 +214,10 @@ Tinytest.add("minimongo - misc", function (test) { test.equal(b.d.z, 15); a.d.y.push(88); test.length(b.d.y, 1); - test.equal(a.g, b.g) + test.equal(a.g, b.g); b.g.setDate(b.g.getDate() + 1); - test.notEqual(a.g, b.g) - + test.notEqual(a.g, b.g); + a = {x: function () {}}; b = LocalCollection._deepcopy(a); a.x.a = 14; diff --git a/packages/mongo-livedata/mongo_driver.js b/packages/mongo-livedata/mongo_driver.js index d4ee2883ac..9f0da038f6 100644 --- a/packages/mongo-livedata/mongo_driver.js +++ b/packages/mongo-livedata/mongo_driver.js @@ -164,7 +164,7 @@ _Mongo.prototype.remove = function (collection_name, selector) { return; } - collection.remove(selector, {/* XXXsafe: true*/}, function (err) { + collection.remove(selector, {safe: true}, function (err) { if (err) { future.ret(err); return; diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index 1277048e6f..e01243a129 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -5,7 +5,7 @@ Meteor._FailureTestCollection = new Meteor.Collection("___meteor_failure_test_collection"); -testAsyncMulti("mongo-livedata - database failure reporting", [ +testAsyncMulti("mongo-livedata - database error reporting", [ function (test, expect) { var ftc = Meteor._FailureTestCollection; diff --git a/packages/sass/sass_tests.js b/packages/sass/sass_tests.js index 2c5622c8fa..e2e0e7b652 100644 --- a/packages/sass/sass_tests.js +++ b/packages/sass/sass_tests.js @@ -2,14 +2,13 @@ Tinytest.add("sass - presence", function(test) { var d = OnscreenDiv(Meteor.render(function() { - return '

'; })); + return '

'; })); d.node().style.display = 'block'; var p = d.node().firstChild; - var leftBorder = getStyleProperty(p, 'border-left-width'); - test.equal(leftBorder, "13px"); + var leftBorder = getStyleProperty(p, 'border-left-style'); + test.equal(leftBorder, "dashed"); d.kill(); }); - diff --git a/packages/sass/sass_tests.sass b/packages/sass/sass_tests.sass index cf109dbaf6..c555f4c59f 100644 --- a/packages/sass/sass_tests.sass +++ b/packages/sass/sass_tests.sass @@ -1,7 +1,7 @@ #sass-tests :zoom 1 -unlucky: 13px +dashy: dashed -.sass-unlucky-left-border - :border-left !unlucky solid white +.sass-dashy-left-border + :border-left 1px !dashy black diff --git a/packages/spark/patch_tests.js b/packages/spark/patch_tests.js index 842b8c8e80..890c34e808 100644 --- a/packages/spark/patch_tests.js +++ b/packages/spark/patch_tests.js @@ -185,11 +185,11 @@ Tinytest.add("spark - patch - copyAttributes", function(test) { {id:'foo', 'class':'bar', style:'border:1px solid blue;', name:'baz'}); a.check(); - test.equal(a.node().style.borderColor, "blue"); + test.equal(a.node().style.borderLeftColor, "blue"); a.copy({id: "foo", style:'border:1px solid red'}); a.check(); - test.equal(a.node().style.borderColor, "red"); + test.equal(a.node().style.borderLeftColor, "red"); a.copy({id: "foo", 'class':'ha'}); a.check(); diff --git a/packages/spark/spark.js b/packages/spark/spark.js index 3a9d3b7cfc..55d34e03af 100644 --- a/packages/spark/spark.js +++ b/packages/spark/spark.js @@ -91,8 +91,9 @@ var notifyWatchers = function (start, end) { }; Spark._createId = function () { + // Chars can't include '-' to be safe inside HTML comments. var chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+_"; var id = ""; for (var i = 0; i < 8; i++) id += chars.substr(Math.floor(Meteor.random() * 64), 1); diff --git a/packages/spiderable/spiderable.js b/packages/spiderable/spiderable.js index 153baa4f0e..1b883c21bb 100644 --- a/packages/spiderable/spiderable.js +++ b/packages/spiderable/spiderable.js @@ -24,7 +24,12 @@ // Use '/dev/stdin' to avoid writing to a temporary file. Can't // just omit the file, as PhantomJS takes that to mean 'use a // REPL' and exits as soon as stdin closes. - var cp = spawn('phantomjs', ['--load-images=no', '/dev/stdin']); + // + // However, Node 0.8 broke the ability to open /dev/stdin in the + // subprocess; see https://gist.github.com/3751746 for the gory + // details. Work around this with a not-so-useless use of cat. + var cp = spawn('bash', + ['-c', 'cat | phantomjs --load-images=no /dev/stdin']); var data = ''; cp.stdout.setEncoding('utf8'); diff --git a/packages/stylus/stylus_tests.js b/packages/stylus/stylus_tests.js index 304411ff74..0fddbd3420 100644 --- a/packages/stylus/stylus_tests.js +++ b/packages/stylus/stylus_tests.js @@ -2,12 +2,12 @@ Tinytest.add("stylus - presence", function(test) { var d = OnscreenDiv(Meteor.render(function() { - return '

'; })); + return '

'; })); d.node().style.display = 'block'; var p = d.node().firstChild; - var leftBorder = getStyleProperty(p, 'border-left-width'); - test.equal(leftBorder, "13px"); + var leftBorder = getStyleProperty(p, 'border-left-style'); + test.equal(leftBorder, "dashed"); d.kill(); diff --git a/packages/stylus/stylus_tests.styl b/packages/stylus/stylus_tests.styl index 2dec111077..feedadbdf2 100644 --- a/packages/stylus/stylus_tests.styl +++ b/packages/stylus/stylus_tests.styl @@ -2,8 +2,8 @@ #stylus-tests zoom: 1 -unlucky = 13px +dashy = dashed -.stylus-unlucky-left-border - border-left: unlucky solid white +.stylus-dashy-left-border + border-left: 1px dashy black diff --git a/packages/test-in-browser/driver.css b/packages/test-in-browser/driver.css index f8489a5a5f..29d9da59ff 100644 --- a/packages/test-in-browser/driver.css +++ b/packages/test-in-browser/driver.css @@ -44,6 +44,7 @@ line-height: 24px; vertical-align: middle; text-decoration: underline; + cursor: pointer; } .test_table .groupname {