diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..ad594f1f1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+node_modules/
+.DS_Store
+public/index.html
+public/styleguide.html
+public/styleguide/html/styleguide.html
+public/css/*
+public/fonts/*
+public/js/*
+public/images/*
+public/patterns/*
+public/styleguide/css/*
+public/styleguide/js/*
+config.ini
+latest-change.txt
+/public/styleguide/js/styleguide-ck.js
+/public/listeners/synclisteners-ck.js
+/public/styleguide/js/data-saver-ck.js
+/public/styleguide/js/url-handler-ck.js
+patternlab.json
+.sass-cache/*
+/sass-cache
\ No newline at end of file
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 000000000..9c0effa10
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,13 @@
+THIS CHANGELOG IS AN ATTEMPT TO DOCUMENT CHANGES TO THIS PROJECT.
+
+PL-node-v0.0.3
+ - FIX: Install documentation was incomplete, should not have assumed grunt
+ - FIX: Remove SASS/SCSS dependency which was causing clean installs from failing
+
+PL-node-v0.0.2
+ - FIX: Sub Nav Items now strip hyphens and are styled like patternlab-php.
+ - FIX: Exclude patterns by using an underscore
+ - FIX: Grunt watching styleguide scss
+
+PL-node-v0.0.1
+ - Minimum Viable Product! At this point, I feel you could use Pattern Lab Node to build a atomic design-drive website.
\ No newline at end of file
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 000000000..652589a2c
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,68 @@
+module.exports = function(grunt) {
+
+ // Project configuration.
+ grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'),
+ sass: {
+ build: {
+ options: {
+ style: 'expanded',
+ precision: 8
+ },
+ files: {
+ './source/css/style.css': './source/css/style.scss',
+ './public/styleguide/css/static.css': './public/styleguide/css/static.scss',
+ './public/styleguide/css/styleguide.css': './public/styleguide/css/styleguide.scss'
+ }
+ }
+ },
+ copy: {
+ main: {
+ files: [
+ { expand: true, cwd: './source/js/', src: '*', dest: './public/js/'},
+ { expand: true, cwd: './source/css/', src: 'style.css', dest: './public/css/' },
+ { expand: true, cwd: './source/images/', src: '*', dest: './public/images/' },
+ { expand: true, cwd: './source/images/sample/', src: '*', dest: './public/images/sample/'},
+ { expand: true, cwd: './source/fonts/', src: '*', dest: './public/fonts/'}
+ ]
+ }
+ },
+ jshint: {
+ options: {
+ "curly": true,
+ "eqnull": true,
+ "eqeqeq": true,
+ "undef": true,
+ "forin": true,
+ //"unused": true,
+ "node": true
+ },
+ patternlab: ['Gruntfile.js', './builder/lib/patternlab.js']
+ },
+ watch: {
+ // scss: { //scss can be watched if you like
+ // files: ['source/css/**/*.scss', 'public/styleguide/css/*.scss'],
+ // tasks: ['default']
+ // },
+ mustache: {
+ files: ['source/_patterns/**/*.mustache'],
+ tasks: ['default']
+ },
+ data: {
+ files: ['source/_patterns/**/*.json'],
+ tasks: ['default']
+ }
+ }
+ });
+
+ grunt.loadNpmTasks('grunt-contrib-copy');
+ grunt.loadNpmTasks('grunt-contrib-watch');
+ grunt.loadNpmTasks('grunt-contrib-sass');
+ grunt.loadNpmTasks('grunt-contrib-jshint');
+
+ //load the patternlab task
+ grunt.task.loadTasks('./builder/');
+
+ //if you choose to use scss, or any preprocessor, you can add it here
+ grunt.registerTask('default', ['patternlab', /*'sass',*/ 'copy']);
+};
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..05d36bf48
--- /dev/null
+++ b/README.md
@@ -0,0 +1,78 @@
+## About the Node Version of Pattern Lab
+
+The Node version of Pattern Lab is, at its core, a static site generator. It combines platform-agnostic assets, like the [Mustache](http://mustache.github.io/)-based patterns and the JavaScript-based viewer, with a Node-based "builder" that transforms and dynamically builds the Pattern Lab site. By making it a static site generator, the Node version of Pattern Lab strongly separates patterns, data, and presentation from build logic. The Node version is a work in progress, the [PHP version](https://github.com/pattern-lab/patternlab-php) should be seen as a reference for other developers to improve upon as they build their own Pattern Lab Builders in their language of choice.
+
+## Under Active Development
+
+The Node version of Pattern Lab is under active development by [@bmuenzenmeyer](https://twitter.com/bmuenzenmeyer) and (hopefully) [@Wiscow](https://twitter.com/Wiscow). Contributions welcome. We both have kids under 2 years of age, so patience and coffee is requested :D
+
+### Getting Started
+
+To run patternlab-node, just do the following from the command line at the root of patternlab-node:
+
+1. `npm install`
+2. `npm install -g grunt-cli`
+3. `grunt`
+
+This creates all patterns, the styleguide, and the patternlab site. `patternlab.json` is a file created for debugging purposes. It tells you all the secrets in tidy json.
+
+To have patternlab-node watch for changes to either a mustache template, data, or stylesheets, run `grunt watch`. The `Gruntfile` governs what is watched. It should be easy to add scss or whatever preprocessor you fancy.
+
+#### Watching Progress
+
+Patternlab Node has reached [minimum viable product](http://en.wikipedia.org/wiki/Minimum_viable_product) status. The main branch will always have the most up to date version of patternlab-node. Watch the dev branch for what it coming next!
+
+#### Roadmap
+* Full Patternlab site support. (This is the uber cool navigation found at [demo.pattern-lab.info](http://demo.pattern-lab.info)).
+* More Documentation
+* Tests
+
+**THE FOLLOWING IS FROM THE PATTERNLAB-PHP PROJECT. A LOT STILL APPLIES TO PATTERNLAB-NODE, BUT IT HAS NOT BEEN ADAPTED YET. USE AT YOUR OWN PERIL**
+
+
+===
+
+## Demo
+
+You can play with a demo of the front-end of the PHP version of Pattern Lab at [demo.pattern-lab.info](http://demo.pattern-lab.info).
+
+## Getting Started
+
+The PHP version of Pattern Lab should be relatively easy for anyone to get up and running.
+
+* [Requirements](https://github.com/pattern-lab/patternlab-php/wiki/Requirements)
+* [Installing the PHP Version of Pattern Lab](https://github.com/pattern-lab/patternlab-php/wiki/Installing-the-PHP-Version-of-Pattern-Lab)
+* [Generating the Pattern Lab Website for the First Time](https://github.com/pattern-lab/patternlab-php/wiki/Generating-the-Pattern-Lab-Website-for-the-First-Time)
+* [Editing the Pattern Lab Website Source Files](https://github.com/pattern-lab/patternlab-php/wiki/Editing-the-Pattern-Lab-Website-Source-Files)
+* [Using the Command-line Options](https://github.com/pattern-lab/patternlab-php/wiki/Using-the-Command-line-Options)
+
+## Working with Patterns
+
+Patterns are the core element of Pattern Lab. Understanding how they work is the key to getting the most out of the system. Patterns use [Mustache](http://mustache.github.io/) so please read [Mustache's docs](http://mustache.github.io/mustache.5.html) as well.
+
+* [How Patterns Are Organized](https://github.com/pattern-lab/patternlab-php/wiki/How-Patterns-Are-Organized)
+* [Adding New Patterns](https://github.com/pattern-lab/patternlab-php/wiki/Adding-New-Patterns)
+* [Reorganizing Patterns](https://github.com/pattern-lab/patternlab-php/wiki/Reorganizing-Patterns)
+* [Converting Old Patterns](https://github.com/pattern-lab/patternlab-php/wiki/Converting-Old-Patterns)
+* ["Hiding" Patterns in the Navigation](https://github.com/pattern-lab/patternlab-php/wiki/Hiding-Patterns-in-the-Navigation)
+* [Including One Pattern Within Another via Partials](https://github.com/pattern-lab/patternlab-php/wiki/Including-One-Pattern-Within-Another)
+* [Linking Directly to a Pattern](https://github.com/pattern-lab/patternlab-php/wiki/Linking-Directly-to-a-Pattern)
+* [Managing Assets for a Pattern: JavaScript, images, CSS, etc.](https://github.com/pattern-lab/patternlab-php/wiki/Managing-Assets-for-a-Pattern)
+* [Modifying the Standard Header & Footer for Patterns](https://github.com/pattern-lab/patternlab-php/wiki/Modifying-the-Standard-Header-&-Footer-for-Patterns)
+
+## Creating & Working With Dynamic Data for a Pattern
+
+The PHP version of Pattern Lab utilizes Mustache as the template language for patterns. In addition to allowing for the [inclusion of one pattern within another](https://github.com/pattern-lab/patternlab-php/wiki/Including-One-Pattern-Within-Another) it also gives pattern developers the ability to include variables. This means that attributes like image sources can be centralized in one file for easy modification across one or more patterns. The PHP version of Pattern Lab uses a JSON file, `source/_data/data.json`, to centralize many of these attributes.
+
+* [Introduction to JSON & Mustache Variables](http://github.com/pattern-lab/patternlab-php/wiki/Introduction-to-JSON-&-Mustache-Variables)
+* [Overriding the Central `data.json` Values with Pattern-specific Values](https://github.com/pattern-lab/patternlab-php/wiki/Overriding-the-Central-%60data.json%60-Values-with-Pattern-specific-Values)
+* [Linking to Patterns with Pattern Lab's Default `link` Variable](https://github.com/pattern-lab/patternlab-php/wiki/Linking-to-Patterns-with-Pattern-Lab's-Default-%60link%60-Variable)
+* [Creating Lists with Pattern Lab's Default `listItems` Variable](https://github.com/pattern-lab/patternlab-php/wiki/Creating-Lists-with-Pattern-Lab's-Default-%60listItems%60-Variable)
+
+## Using Pattern Lab's Advanced Features
+
+By default, the Pattern Lab assets can be manually generated and the Pattern Lab site manually refreshed but who wants to waste time doing that? Here are some ways that the PHP version of Pattern Lab can make your development workflow a little smoother:
+
+* [Watching for Changes and Auto-Regenerating Patterns](https://github.com/pattern-lab/patternlab-php/wiki/Watching-for-Changes-and-Auto-Regenerating-Patterns)
+* [Auto-Reloading the Browser Window When Changes Are Made](https://github.com/pattern-lab/patternlab-php/wiki/Auto-Reloading-the-Browser-Window-When-Changes-Are-Made)
+* [Multi-browser & Multi-device Testing with Page Follow](https://github.com/pattern-lab/patternlab-php/wiki/Multi-browser-&-Multi-device-Testing-with-Page-Follow)
diff --git a/builder/lib/Mustache/.gitignore b/builder/lib/Mustache/.gitignore
new file mode 100644
index 000000000..90007769a
--- /dev/null
+++ b/builder/lib/Mustache/.gitignore
@@ -0,0 +1,10 @@
+.DS_Store
+.rvmrc
+node_modules
+runner.js
+jquery.mustache.js
+qooxdoo.mustache.js
+dojox
+yui3
+requirejs.mustache.js
+
diff --git a/builder/lib/Mustache/.gitmodules b/builder/lib/Mustache/.gitmodules
new file mode 100644
index 000000000..9e2fdf850
--- /dev/null
+++ b/builder/lib/Mustache/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "test/spec"]
+ path = test/spec
+ url = https://github.com/mustache/spec
diff --git a/builder/lib/Mustache/.jshintrc b/builder/lib/Mustache/.jshintrc
new file mode 100644
index 000000000..28dff7105
--- /dev/null
+++ b/builder/lib/Mustache/.jshintrc
@@ -0,0 +1,5 @@
+{
+ "eqnull": true,
+ "evil": true
+}
+
diff --git a/builder/lib/Mustache/.travis.yml b/builder/lib/Mustache/.travis.yml
new file mode 100644
index 000000000..3d839b0ef
--- /dev/null
+++ b/builder/lib/Mustache/.travis.yml
@@ -0,0 +1,4 @@
+language: node_js
+node_js:
+ - 0.6
+
diff --git a/builder/lib/Mustache/CHANGES b/builder/lib/Mustache/CHANGES
new file mode 100644
index 000000000..ad80e95f4
--- /dev/null
+++ b/builder/lib/Mustache/CHANGES
@@ -0,0 +1,43 @@
+= HEAD
+
+ * Don't require the original template to be passed to the rendering function
+ when using compiled templates. This is still required when using higher-order
+ functions in order to be able to extract the portion of the template
+ that was contained by that section. Fixes #262.
+ * Performance improvements.
+
+= 0.7.2 / 27 Dec 2012
+
+ * Fixed a rendering bug (#274) when using nested higher-order sections.
+ * Better error reporting on failed parse.
+ * Converted tests to use mocha instead of vows.
+
+= 0.7.1 / 6 Dec 2012
+
+ * Handle empty templates gracefully. Fixes #265, #267, and #270.
+ * Cache partials by template, not by name. Fixes #257.
+ * Added Mustache.compileTokens to compile the output of Mustache.parse. Fixes
+ #258.
+
+= 0.7.0 / 10 Sep 2012
+
+ * Rename Renderer => Writer.
+ * Allow partials to be loaded dynamically using a callback (thanks
+ @TiddoLangerak for the suggestion).
+ * Fixed a bug with higher-order sections that prevented them from being
+ passed the raw text of the section from the original template.
+ * More concise token format. Tokens also include start/end indices in the
+ original template.
+ * High-level API is consistent with the Writer API.
+ * Allow partials to be passed to the pre-compiled function (thanks
+ @fallenice).
+ * Don't use eval (thanks @cweider).
+
+= 0.6.0 / 31 Aug 2012
+
+ * Use JavaScript's definition of falsy when determining whether to render an
+ inverted section or not. Issue #186.
+ * Use Mustache.escape to escape values inside {{}}. This function may be
+ reassigned to alter the default escaping behavior. Issue #244.
+ * Fixed a bug that clashed with QUnit (thanks @kannix).
+ * Added volo support (thanks @guybedford).
diff --git a/builder/lib/Mustache/LICENSE b/builder/lib/Mustache/LICENSE
new file mode 100644
index 000000000..6626848b3
--- /dev/null
+++ b/builder/lib/Mustache/LICENSE
@@ -0,0 +1,10 @@
+The MIT License
+
+Copyright (c) 2009 Chris Wanstrath (Ruby)
+Copyright (c) 2010 Jan Lehnardt (JavaScript)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/builder/lib/Mustache/README.md b/builder/lib/Mustache/README.md
new file mode 100644
index 000000000..95c92788b
--- /dev/null
+++ b/builder/lib/Mustache/README.md
@@ -0,0 +1,405 @@
+# mustache.js - Logic-less {{mustache}} templates with JavaScript
+
+> What could be more logical awesome than no logic at all?
+
+[mustache.js](http://github.com/janl/mustache.js) is an implementation of the [mustache](http://mustache.github.com/) template system in JavaScript.
+
+[Mustache](http://mustache.github.com/) is a logic-less template syntax. It can be used for HTML, config files, source code - anything. It works by expanding tags in a template using values provided in a hash or object.
+
+We call it "logic-less" because there are no if statements, else clauses, or for loops. Instead there are only tags. Some tags are replaced with a value, some nothing, and others a series of values.
+
+For a language-agnostic overview of mustache's template syntax, see the `mustache(5)` [manpage](http://mustache.github.com/mustache.5.html).
+
+## Where to use mustache.js?
+
+You can use mustache.js to render mustache templates anywhere you can use JavaScript. This includes web browsers, server-side environments such as [node](http://nodejs.org/), and [CouchDB](http://couchdb.apache.org/) views.
+
+mustache.js ships with support for both the [CommonJS](http://www.commonjs.org/) module API and the [Asynchronous Module Definition](https://github.com/amdjs/amdjs-api/wiki/AMD) API, or AMD.
+
+## Who uses mustache.js?
+
+An updated list of mustache.js users is kept [on the Github wiki](http://wiki.github.com/janl/mustache.js/beard-competition). Add yourself or your company if you use mustache.js!
+
+## Usage
+
+Below is quick example how to use mustache.js:
+
+ var view = {
+ title: "Joe",
+ calc: function () {
+ return 2 + 4;
+ }
+ };
+
+ var output = Mustache.render("{{title}} spends {{calc}}", view);
+
+In this example, the `Mustache.render` function takes two parameters: 1) the [mustache](http://mustache.github.com/) template and 2) a `view` object that contains the data and code needed to render the template.
+
+## Templates
+
+A [mustache](http://mustache.github.com/) template is a string that contains any number of mustache tags. Tags are indicated by the double mustaches that surround them. `{{person}}` is a tag, as is `{{#person}}`. In both examples we refer to `person` as the tag's key.
+
+There are several types of tags available in mustache.js.
+
+### Variables
+
+The most basic tag type is a simple variable. A `{{name}}` tag renders the value of the `name` key in the current context. If there is no such key, nothing is rendered.
+
+All variables are HTML-escaped by default. If you want to render unescaped HTML, use the triple mustache: `{{{name}}}`. You can also use `&` to unescape a variable.
+
+View:
+
+ {
+ "name": "Chris",
+ "company": "GitHub"
+ }
+
+Template:
+
+ * {{name}}
+ * {{age}}
+ * {{company}}
+ * {{{company}}}
+ * {{&company}}
+
+Output:
+
+ * Chris
+ *
+ * <b>GitHub</b>
+ * GitHub
+ * GitHub
+
+JavaScript's dot notation may be used to access keys that are properties of objects in a view.
+
+View:
+
+ {
+ "name": {
+ "first": "Michael",
+ "last": "Jackson"
+ },
+ "age": "RIP"
+ }
+
+Template:
+
+ * {{name.first}} {{name.last}}
+ * {{age}}
+
+Output:
+
+ * Michael Jackson
+ * RIP
+
+### Sections
+
+Sections render blocks of text one or more times, depending on the value of the key in the current context.
+
+A section begins with a pound and ends with a slash. That is, `{{#person}}` begins a `person` section, while `{{/person}}` ends it. The text between the two tags is referred to as that section's "block".
+
+The behavior of the section is determined by the value of the key.
+
+#### False Values or Empty Lists
+
+If the `person` key does not exist, or exists and has a value of `null`, `undefined`, or `false`, or is an empty list, the block will not be rendered.
+
+View:
+
+ {
+ "person": false
+ }
+
+Template:
+
+ Shown.
+ {{#person}}
+ Never shown!
+ {{/person}}
+
+Output:
+
+ Shown.
+
+#### Non-Empty Lists
+
+If the `person` key exists and is not `null`, `undefined`, or `false`, and is not an empty list the block will be rendered one or more times.
+
+When the value is a list, the block is rendered once for each item in the list. The context of the block is set to the current item in the list for each iteration. In this way we can loop over collections.
+
+View:
+
+ {
+ "stooges": [
+ { "name": "Moe" },
+ { "name": "Larry" },
+ { "name": "Curly" }
+ ]
+ }
+
+Template:
+
+ {{#stooges}}
+ {{name}}
+ {{/stooges}}
+
+Output:
+
+ Moe
+ Larry
+ Curly
+
+When looping over an array of strings, a `.` can be used to refer to the current item in the list.
+
+View:
+
+ {
+ "musketeers": ["Athos", "Aramis", "Porthos", "D'Artagnan"]
+ }
+
+Template:
+
+ {{#musketeers}}
+ * {{.}}
+ {{/musketeers}}
+
+Output:
+
+ * Athos
+ * Aramis
+ * Porthos
+ * D'Artagnan
+
+If the value of a section variable is a function, it will be called in the context of the current item in the list on each iteration.
+
+View:
+
+ {
+ "beatles": [
+ { "firstName": "John", "lastName": "Lennon" },
+ { "firstName": "Paul", "lastName": "McCartney" },
+ { "firstName": "George", "lastName": "Harrison" },
+ { "firstName": "Ringo", "lastName": "Starr" }
+ ],
+ "name": function () {
+ return this.firstName + " " + this.lastName;
+ }
+ }
+
+Template:
+
+ {{#beatles}}
+ * {{name}}
+ {{/beatles}}
+
+Output:
+
+ * John Lennon
+ * Paul McCartney
+ * George Harrison
+ * Ringo Starr
+
+#### Functions
+
+If the value of a section key is a function, it is called with the section's literal block of text, un-rendered, as its first argument. The second argument is a special rendering function that uses the current view as its view argument. It is called in the context of the current view object.
+
+View:
+
+ {
+ "name": "Tater",
+ "bold": function () {
+ return function (text, render) {
+ return "" + render(text) + "";
+ }
+ }
+ }
+
+Template:
+
+ {{#bold}}Hi {{name}}.{{/bold}}
+
+Output:
+
+ Hi Tater.
+
+### Inverted Sections
+
+An inverted section opens with `{{^section}}` instead of `{{#section}}`. The block of an inverted section is rendered only if the value of that section's tag is `null`, `undefined`, `false`, or an empty list.
+
+View:
+
+ {
+ "repos": []
+ }
+
+Template:
+
+ {{#repos}}{{name}}{{/repos}}
+ {{^repos}}No repos :({{/repos}}
+
+Output:
+
+ No repos :(
+
+### Comments
+
+Comments begin with a bang and are ignored. The following template:
+
+
Today{{! ignore me }}.
+
+Will render as follows:
+
+
Today.
+
+Comments may contain newlines.
+
+### Partials
+
+Partials begin with a greater than sign, like {{> box}}.
+
+Partials are rendered at runtime (as opposed to compile time), so recursive partials are possible. Just avoid infinite loops.
+
+They also inherit the calling context. Whereas in ERB you may have this:
+
+ <%= partial :next_more, :start => start, :size => size %>
+
+Mustache requires only this:
+
+ {{> next_more}}
+
+Why? Because the `next_more.mustache` file will inherit the `size` and `start` variables from the calling context. In this way you may want to think of partials as includes, or template expansion, even though it's not literally true.
+
+For example, this template and partial:
+
+ base.mustache:
+
Names
+ {{#names}}
+ {{> user}}
+ {{/names}}
+
+ user.mustache:
+ {{name}}
+
+Can be thought of as a single, expanded template:
+
+
Names
+ {{#names}}
+ {{name}}
+ {{/names}}
+
+In mustache.js an object of partials may be passed as the third argument to `Mustache.render`. The object should be keyed by the name of the partial, and its value should be the partial text.
+
+### Set Delimiter
+
+Set Delimiter tags start with an equals sign and change the tag delimiters from `{{` and `}}` to custom strings.
+
+Consider the following contrived example:
+
+ * {{ default_tags }}
+ {{=<% %>=}}
+ * <% erb_style_tags %>
+ <%={{ }}=%>
+ * {{ default_tags_again }}
+
+Here we have a list with three items. The first item uses the default tag style, the second uses ERB style as defined by the Set Delimiter tag, and the third returns to the default style after yet another Set Delimiter declaration.
+
+According to [ctemplates](http://google-ctemplate.googlecode.com/svn/trunk/doc/howto.html), this "is useful for languages like TeX, where double-braces may occur in the text and are awkward to use for markup."
+
+Custom delimiters may not contain whitespace or the equals sign.
+
+### Compiled Templates
+
+Mustache templates can be compiled into JavaScript functions using `Mustache.compile` for improved rendering performance.
+
+If you have template views that are rendered multiple times, compiling your template into a JavaScript function will minimise the amount of work required for each re-render.
+
+Pre-compiled templates can also be generated server-side, for delivery to the browser as ready to use JavaScript functions, further reducing the amount of client side processing required for initialising templates.
+
+**Mustache.compile**
+
+Use `Mustache.compile` to compile standard Mustache string templates into reusable Mustache template functions.
+
+ var compiledTemplate = Mustache.compile(stringTemplate);
+
+The function returned from `Mustache.compile` can then be called directly, passing in the template data as an argument (with an object of partials as an optional second parameter), to generate the final output.
+
+ var templateOutput = compiledTemplate(templateData);
+
+**Mustache.compilePartial**
+
+Template partials can also be compiled using the `Mustache.compilePartial` function. The first parameter of this function, is the name of the partial as it appears within parent templates.
+
+ Mustache.compilePartial('partial-name', stringTemplate);
+
+Compiled partials are then available to both `Mustache.render` and `Mustache.compile`.
+
+## Plugins for JavaScript Libraries
+
+mustache.js may be built specifically for several different client libraries, including the following:
+
+ - [jQuery](http://jquery.com/)
+ - [MooTools](http://mootools.net/)
+ - [Dojo](http://www.dojotoolkit.org/)
+ - [YUI](http://developer.yahoo.com/yui/)
+ - [qooxdoo](http://qooxdoo.org/)
+
+These may be built using [Rake](http://rake.rubyforge.org/) and one of the following commands:
+
+ $ rake jquery
+ $ rake mootools
+ $ rake dojo
+ $ rake yui3
+ $ rake qooxdoo
+
+## Testing
+
+The mustache.js test suite uses the [mocha](http://visionmedia.github.com/mocha/) testing framework. In order to run the tests you'll need to install [node](http://nodejs.org/). Once that's done you can install mocha using [npm](http://npmjs.org/).
+
+ $ npm install -g mocha
+
+You also need to install the sub module containing [Mustache specifications](http://github.com/mustache/spec) in the project root.
+
+ $ git submodule init
+ $ git submodule update
+
+Then run the tests.
+
+ $ mocha test
+
+The test suite consists of both unit and integration tests. If a template isn't rendering correctly for you, you can make a test for it by doing the following:
+
+ 1. Create a template file named `mytest.mustache` in the `test/_files`
+ directory. Replace `mytest` with the name of your test.
+ 2. Create a corresponding view file named `mytest.js` in the same directory.
+ This file should contain a JavaScript object literal enclosed in
+ parentheses. See any of the other view files for an example.
+ 3. Create a file with the expected output in `mytest.txt` in the same
+ directory.
+
+Then, you can run the test with:
+
+ $ TEST=mytest mocha test/render_test.js
+
+## Thanks
+
+mustache.js wouldn't kick ass if it weren't for these fine souls:
+
+ * Chris Wanstrath / defunkt
+ * Alexander Lang / langalex
+ * Sebastian Cohnen / tisba
+ * J Chris Anderson / jchris
+ * Tom Robinson / tlrobinson
+ * Aaron Quint / quirkey
+ * Douglas Crockford
+ * Nikita Vasilyev / NV
+ * Elise Wood / glytch
+ * Damien Mathieu / dmathieu
+ * Jakub Kuźma / qoobaa
+ * Will Leinweber / will
+ * dpree
+ * Jason Smith / jhs
+ * Aaron Gibralter / agibralter
+ * Ross Boucher / boucher
+ * Matt Sanford / mzsanford
+ * Ben Cherry / bcherry
+ * Michael Jackson / mjijackson
diff --git a/builder/lib/Mustache/Rakefile b/builder/lib/Mustache/Rakefile
new file mode 100644
index 000000000..bc3217586
--- /dev/null
+++ b/builder/lib/Mustache/Rakefile
@@ -0,0 +1,68 @@
+require 'rake'
+require 'rake/clean'
+
+task :default => :test
+
+ROOT = File.expand_path('..', __FILE__)
+MUSTACHE_JS = File.read(File.join(ROOT, 'mustache.js'))
+
+def mustache_version
+ match = MUSTACHE_JS.match(/exports\.version = "([^"]+)";/)
+ match[1]
+end
+
+def minified_file
+ ENV['FILE'] || 'mustache.min.js'
+end
+
+desc "Run all tests, requires vows (see http://vowsjs.org)"
+task :test do
+ sh "vows --spec"
+end
+
+desc "Minify to #{minified_file}, requires UglifyJS (see http://marijnhaverbeke.nl/uglifyjs)"
+task :minify do
+ sh "uglifyjs mustache.js > #{minified_file}"
+end
+
+desc "Run JSHint, requires jshint (see http://www.jshint.com)"
+task :lint do
+ sh "jshint mustache.js"
+end
+
+# Creates a task that uses the various template wrappers to make a wrapped
+# output file. There is some extra complexity because Dojo and YUI use
+# different final locations.
+def templated_build(name, opts={})
+ short = name.downcase
+ source = File.join("wrappers", short)
+ dependencies = ["mustache.js"] + Dir.glob("#{source}/*.tpl.*")
+ target_js = opts[:location] ? "mustache.js" : "#{short}.mustache.js"
+
+ CLEAN.include(opts[:location] ? opts[:location] : target_js)
+
+ desc "Package for #{name}"
+ task short.to_sym => dependencies do
+ puts "Packaging for #{name}"
+
+ mkdir_p opts[:location] if opts[:location]
+
+ files = [
+ "#{source}/mustache.js.pre",
+ 'mustache.js',
+ "#{source}/mustache.js.post"
+ ]
+
+ open("#{opts[:location] || '.'}/#{target_js}", 'w') do |f|
+ files.each {|file| f << File.read(file) }
+ end
+
+ puts "Done, see #{opts[:location] || '.'}/#{target_js}"
+ end
+end
+
+templated_build "jQuery"
+templated_build "MooTools"
+templated_build "Dojo", :location => "dojox/string"
+templated_build "YUI3", :location => "yui3/mustache"
+templated_build "qooxdoo"
diff --git a/builder/lib/Mustache/mustache.js b/builder/lib/Mustache/mustache.js
new file mode 100644
index 000000000..bee26f965
--- /dev/null
+++ b/builder/lib/Mustache/mustache.js
@@ -0,0 +1,536 @@
+/*!
+ * mustache.js - Logic-less {{mustache}} templates with JavaScript
+ * http://github.com/janl/mustache.js
+ */
+
+/*global define: false*/
+
+(function (root, factory) {
+ if (typeof exports === "object" && exports) {
+ factory(exports); // CommonJS
+ } else {
+ var mustache = {};
+ factory(mustache);
+ if (typeof define === "function" && define.amd) {
+ define(mustache); // AMD
+ } else {
+ root.Mustache = mustache; //
diff --git a/builder/lib/Mustache/test/_files/backslashes.txt b/builder/lib/Mustache/test/_files/backslashes.txt
new file mode 100644
index 000000000..038dd37e4
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/backslashes.txt
@@ -0,0 +1,7 @@
+* \abc
+* \abc
+* \abc
+
diff --git a/builder/lib/Mustache/test/_files/bug_11_eating_whitespace.js b/builder/lib/Mustache/test/_files/bug_11_eating_whitespace.js
new file mode 100644
index 000000000..e41ccd15d
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/bug_11_eating_whitespace.js
@@ -0,0 +1,3 @@
+({
+ tag: "yo"
+})
diff --git a/builder/lib/Mustache/test/_files/bug_11_eating_whitespace.mustache b/builder/lib/Mustache/test/_files/bug_11_eating_whitespace.mustache
new file mode 100644
index 000000000..8d5cd921a
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/bug_11_eating_whitespace.mustache
@@ -0,0 +1 @@
+{{tag}} foo
diff --git a/builder/lib/Mustache/test/_files/bug_11_eating_whitespace.txt b/builder/lib/Mustache/test/_files/bug_11_eating_whitespace.txt
new file mode 100644
index 000000000..f5bbc85ce
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/bug_11_eating_whitespace.txt
@@ -0,0 +1 @@
+yo foo
diff --git a/builder/lib/Mustache/test/_files/changing_delimiters.js b/builder/lib/Mustache/test/_files/changing_delimiters.js
new file mode 100644
index 000000000..b808f4c8b
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/changing_delimiters.js
@@ -0,0 +1,4 @@
+({
+ "foo": "foooooooooooooo",
+ "bar": "bar!"
+})
diff --git a/builder/lib/Mustache/test/_files/changing_delimiters.mustache b/builder/lib/Mustache/test/_files/changing_delimiters.mustache
new file mode 100644
index 000000000..0cd044c93
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/changing_delimiters.mustache
@@ -0,0 +1 @@
+{{=<% %>=}}<% foo %> {{foo}} <%{bar}%> {{{bar}}}
diff --git a/builder/lib/Mustache/test/_files/changing_delimiters.txt b/builder/lib/Mustache/test/_files/changing_delimiters.txt
new file mode 100644
index 000000000..1b1510dab
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/changing_delimiters.txt
@@ -0,0 +1 @@
+foooooooooooooo {{foo}} bar! {{{bar}}}
diff --git a/builder/lib/Mustache/test/_files/check_falsy.js b/builder/lib/Mustache/test/_files/check_falsy.js
new file mode 100644
index 000000000..5a599cab7
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/check_falsy.js
@@ -0,0 +1,7 @@
+({
+ number: function(text, render) {
+ return function(text, render) {
+ return +render(text);
+ }
+ }
+})
diff --git a/builder/lib/Mustache/test/_files/check_falsy.mustache b/builder/lib/Mustache/test/_files/check_falsy.mustache
new file mode 100644
index 000000000..30e2547f4
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/check_falsy.mustache
@@ -0,0 +1 @@
+
{{#number}}0{{/number}}
diff --git a/builder/lib/Mustache/test/_files/check_falsy.txt b/builder/lib/Mustache/test/_files/check_falsy.txt
new file mode 100644
index 000000000..3bb2f51f6
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/check_falsy.txt
@@ -0,0 +1 @@
+
0
diff --git a/builder/lib/Mustache/test/_files/comments.js b/builder/lib/Mustache/test/_files/comments.js
new file mode 100644
index 000000000..f20b8b11c
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/comments.js
@@ -0,0 +1,5 @@
+({
+ title: function () {
+ return "A Comedy of Errors";
+ }
+})
diff --git a/builder/lib/Mustache/test/_files/comments.mustache b/builder/lib/Mustache/test/_files/comments.mustache
new file mode 100644
index 000000000..503680186
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/comments.mustache
@@ -0,0 +1 @@
+
{{title}}{{! just something interesting... or not... }}
diff --git a/builder/lib/Mustache/test/_files/comments.txt b/builder/lib/Mustache/test/_files/comments.txt
new file mode 100644
index 000000000..0133517bb
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/comments.txt
@@ -0,0 +1 @@
+
diff --git a/builder/lib/Mustache/test/_files/dot_notation.txt b/builder/lib/Mustache/test/_files/dot_notation.txt
new file mode 100644
index 000000000..08afa0529
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/dot_notation.txt
@@ -0,0 +1,9 @@
+
+
A Book
+
Authors:
John Power
Jamie Walsh
+
Price: $200 USD In Stock
+
VAT: $40
+
+
Test truthy false values:
+
Zero: 0
+
False: false
diff --git a/builder/lib/Mustache/test/_files/double_render.js b/builder/lib/Mustache/test/_files/double_render.js
new file mode 100644
index 000000000..28acb2c1a
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/double_render.js
@@ -0,0 +1,5 @@
+({
+ foo: true,
+ bar: "{{win}}",
+ win: "FAIL"
+})
diff --git a/builder/lib/Mustache/test/_files/double_render.mustache b/builder/lib/Mustache/test/_files/double_render.mustache
new file mode 100644
index 000000000..4500fd76c
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/double_render.mustache
@@ -0,0 +1 @@
+{{#foo}}{{bar}}{{/foo}}
diff --git a/builder/lib/Mustache/test/_files/double_render.txt b/builder/lib/Mustache/test/_files/double_render.txt
new file mode 100644
index 000000000..b6e652d05
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/double_render.txt
@@ -0,0 +1 @@
+{{win}}
diff --git a/builder/lib/Mustache/test/_files/empty_list.js b/builder/lib/Mustache/test/_files/empty_list.js
new file mode 100644
index 000000000..c0e115942
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/empty_list.js
@@ -0,0 +1,3 @@
+({
+ jobs: []
+})
diff --git a/builder/lib/Mustache/test/_files/empty_list.mustache b/builder/lib/Mustache/test/_files/empty_list.mustache
new file mode 100644
index 000000000..4fdf13d00
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/empty_list.mustache
@@ -0,0 +1,4 @@
+These are the jobs:
+{{#jobs}}
+{{.}}
+{{/jobs}}
diff --git a/builder/lib/Mustache/test/_files/empty_list.txt b/builder/lib/Mustache/test/_files/empty_list.txt
new file mode 100644
index 000000000..d9b4a6729
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/empty_list.txt
@@ -0,0 +1 @@
+These are the jobs:
diff --git a/builder/lib/Mustache/test/_files/empty_sections.js b/builder/lib/Mustache/test/_files/empty_sections.js
new file mode 100644
index 000000000..b4100a597
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/empty_sections.js
@@ -0,0 +1 @@
+({})
diff --git a/builder/lib/Mustache/test/_files/empty_sections.mustache b/builder/lib/Mustache/test/_files/empty_sections.mustache
new file mode 100644
index 000000000..b6065dbb6
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/empty_sections.mustache
@@ -0,0 +1 @@
+{{#foo}}{{/foo}}foo{{#bar}}{{/bar}}
diff --git a/builder/lib/Mustache/test/_files/empty_sections.txt b/builder/lib/Mustache/test/_files/empty_sections.txt
new file mode 100644
index 000000000..257cc5642
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/empty_sections.txt
@@ -0,0 +1 @@
+foo
diff --git a/builder/lib/Mustache/test/_files/empty_string.js b/builder/lib/Mustache/test/_files/empty_string.js
new file mode 100644
index 000000000..be6e05876
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/empty_string.js
@@ -0,0 +1,6 @@
+({
+ description: "That is all!",
+ child: {
+ description: ""
+ }
+})
diff --git a/builder/lib/Mustache/test/_files/empty_string.mustache b/builder/lib/Mustache/test/_files/empty_string.mustache
new file mode 100644
index 000000000..f568441c0
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/empty_string.mustache
@@ -0,0 +1 @@
+{{description}}{{#child}}{{description}}{{/child}}
diff --git a/builder/lib/Mustache/test/_files/empty_string.txt b/builder/lib/Mustache/test/_files/empty_string.txt
new file mode 100644
index 000000000..22e2a6e58
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/empty_string.txt
@@ -0,0 +1 @@
+That is all!
diff --git a/builder/lib/Mustache/test/_files/empty_template.js b/builder/lib/Mustache/test/_files/empty_template.js
new file mode 100644
index 000000000..b4100a597
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/empty_template.js
@@ -0,0 +1 @@
+({})
diff --git a/builder/lib/Mustache/test/_files/empty_template.mustache b/builder/lib/Mustache/test/_files/empty_template.mustache
new file mode 100644
index 000000000..bb2367a20
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/empty_template.mustache
@@ -0,0 +1 @@
+
Test
\ No newline at end of file
diff --git a/builder/lib/Mustache/test/_files/empty_template.txt b/builder/lib/Mustache/test/_files/empty_template.txt
new file mode 100644
index 000000000..bb2367a20
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/empty_template.txt
@@ -0,0 +1 @@
+
Test
\ No newline at end of file
diff --git a/builder/lib/Mustache/test/_files/error_not_found.js b/builder/lib/Mustache/test/_files/error_not_found.js
new file mode 100644
index 000000000..10e470918
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/error_not_found.js
@@ -0,0 +1,3 @@
+({
+ bar: 2
+})
diff --git a/builder/lib/Mustache/test/_files/error_not_found.mustache b/builder/lib/Mustache/test/_files/error_not_found.mustache
new file mode 100644
index 000000000..24369f73a
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/error_not_found.mustache
@@ -0,0 +1 @@
+{{foo}}
\ No newline at end of file
diff --git a/builder/lib/Mustache/test/_files/error_not_found.txt b/builder/lib/Mustache/test/_files/error_not_found.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/builder/lib/Mustache/test/_files/escaped.js b/builder/lib/Mustache/test/_files/escaped.js
new file mode 100644
index 000000000..cd77c1f49
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/escaped.js
@@ -0,0 +1,6 @@
+({
+ title: function () {
+ return "Bear > Shark";
+ },
+ entities: "" \"'<>/"
+})
diff --git a/builder/lib/Mustache/test/_files/escaped.mustache b/builder/lib/Mustache/test/_files/escaped.mustache
new file mode 100644
index 000000000..93e800b31
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/escaped.mustache
@@ -0,0 +1,2 @@
+
{{title}}
+And even {{entities}}, but not {{{entities}}}.
diff --git a/builder/lib/Mustache/test/_files/escaped.txt b/builder/lib/Mustache/test/_files/escaped.txt
new file mode 100644
index 000000000..c1527d510
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/escaped.txt
@@ -0,0 +1,2 @@
+
Bear > Shark
+And even " "'<>/, but not " "'<>/.
diff --git a/builder/lib/Mustache/test/_files/falsy.js b/builder/lib/Mustache/test/_files/falsy.js
new file mode 100644
index 000000000..ae9b9bf25
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/falsy.js
@@ -0,0 +1,8 @@
+({
+ "emptyString": "",
+ "emptyArray": [],
+ "zero": 0,
+ "null": null,
+ "undefined": undefined,
+ "NaN": 0/0
+})
\ No newline at end of file
diff --git a/builder/lib/Mustache/test/_files/falsy.mustache b/builder/lib/Mustache/test/_files/falsy.mustache
new file mode 100644
index 000000000..f3698da64
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/falsy.mustache
@@ -0,0 +1,12 @@
+{{#emptyString}}empty string{{/emptyString}}
+{{^emptyString}}inverted empty string{{/emptyString}}
+{{#emptyArray}}empty array{{/emptyArray}}
+{{^emptyArray}}inverted empty array{{/emptyArray}}
+{{#zero}}zero{{/zero}}
+{{^zero}}inverted zero{{/zero}}
+{{#null}}null{{/null}}
+{{^null}}inverted null{{/null}}
+{{#undefined}}undefined{{/undefined}}
+{{^undefined}}inverted undefined{{/undefined}}
+{{#NaN}}NaN{{/NaN}}
+{{^NaN}}inverted NaN{{/NaN}}
diff --git a/builder/lib/Mustache/test/_files/falsy.txt b/builder/lib/Mustache/test/_files/falsy.txt
new file mode 100644
index 000000000..9b7cde39c
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/falsy.txt
@@ -0,0 +1,12 @@
+
+inverted empty string
+
+inverted empty array
+
+inverted zero
+
+inverted null
+
+inverted undefined
+
+inverted NaN
diff --git a/builder/lib/Mustache/test/_files/grandparent_context.js b/builder/lib/Mustache/test/_files/grandparent_context.js
new file mode 100644
index 000000000..97dbfd398
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/grandparent_context.js
@@ -0,0 +1,19 @@
+({
+ grand_parent_id: 'grand_parent1',
+ parent_contexts: [
+ {
+ parent_id: 'parent1',
+ child_contexts: [
+ { child_id: 'parent1-child1' },
+ { child_id: 'parent1-child2' }
+ ]
+ },
+ {
+ parent_id: 'parent2',
+ child_contexts: [
+ { child_id: 'parent2-child1' },
+ { child_id: 'parent2-child2' }
+ ]
+ }
+ ]
+})
diff --git a/builder/lib/Mustache/test/_files/grandparent_context.mustache b/builder/lib/Mustache/test/_files/grandparent_context.mustache
new file mode 100644
index 000000000..e6c07a221
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/grandparent_context.mustache
@@ -0,0 +1,10 @@
+{{grand_parent_id}}
+{{#parent_contexts}}
+{{grand_parent_id}}
+{{parent_id}}
+{{#child_contexts}}
+{{grand_parent_id}}
+{{parent_id}}
+{{child_id}}
+{{/child_contexts}}
+{{/parent_contexts}}
diff --git a/builder/lib/Mustache/test/_files/grandparent_context.txt b/builder/lib/Mustache/test/_files/grandparent_context.txt
new file mode 100644
index 000000000..64996ad4c
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/grandparent_context.txt
@@ -0,0 +1,17 @@
+grand_parent1
+grand_parent1
+parent1
+grand_parent1
+parent1
+parent1-child1
+grand_parent1
+parent1
+parent1-child2
+grand_parent1
+parent2
+grand_parent1
+parent2
+parent2-child1
+grand_parent1
+parent2
+parent2-child2
diff --git a/builder/lib/Mustache/test/_files/higher_order_sections.js b/builder/lib/Mustache/test/_files/higher_order_sections.js
new file mode 100644
index 000000000..bacb0a44a
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/higher_order_sections.js
@@ -0,0 +1,9 @@
+({
+ name: "Tater",
+ helper: "To tinker?",
+ bolder: function () {
+ return function (text, render) {
+ return text + ' => ' + render(text) + ' ' + this.helper;
+ }
+ }
+})
diff --git a/builder/lib/Mustache/test/_files/higher_order_sections.mustache b/builder/lib/Mustache/test/_files/higher_order_sections.mustache
new file mode 100644
index 000000000..04f5318df
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/higher_order_sections.mustache
@@ -0,0 +1 @@
+{{#bolder}}Hi {{name}}.{{/bolder}}
diff --git a/builder/lib/Mustache/test/_files/higher_order_sections.txt b/builder/lib/Mustache/test/_files/higher_order_sections.txt
new file mode 100644
index 000000000..be50ad764
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/higher_order_sections.txt
@@ -0,0 +1 @@
+Hi {{name}}. => Hi Tater. To tinker?
diff --git a/builder/lib/Mustache/test/_files/included_tag.js b/builder/lib/Mustache/test/_files/included_tag.js
new file mode 100644
index 000000000..eb032a42c
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/included_tag.js
@@ -0,0 +1,3 @@
+({
+ html: "I like {{mustache}}"
+})
diff --git a/builder/lib/Mustache/test/_files/included_tag.mustache b/builder/lib/Mustache/test/_files/included_tag.mustache
new file mode 100644
index 000000000..70631c259
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/included_tag.mustache
@@ -0,0 +1 @@
+You said "{{{html}}}" today
diff --git a/builder/lib/Mustache/test/_files/included_tag.txt b/builder/lib/Mustache/test/_files/included_tag.txt
new file mode 100644
index 000000000..1af45567f
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/included_tag.txt
@@ -0,0 +1 @@
+You said "I like {{mustache}}" today
diff --git a/builder/lib/Mustache/test/_files/inverted_section.js b/builder/lib/Mustache/test/_files/inverted_section.js
new file mode 100644
index 000000000..f8f08fd26
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/inverted_section.js
@@ -0,0 +1,3 @@
+({
+ "repos": []
+})
diff --git a/builder/lib/Mustache/test/_files/inverted_section.mustache b/builder/lib/Mustache/test/_files/inverted_section.mustache
new file mode 100644
index 000000000..b0a183b10
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/inverted_section.mustache
@@ -0,0 +1,3 @@
+{{#repos}}{{name}}{{/repos}}
+{{^repos}}No repos :({{/repos}}
+{{^nothin}}Hello!{{/nothin}}
diff --git a/builder/lib/Mustache/test/_files/inverted_section.txt b/builder/lib/Mustache/test/_files/inverted_section.txt
new file mode 100644
index 000000000..b421582e7
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/inverted_section.txt
@@ -0,0 +1,3 @@
+
+No repos :(
+Hello!
diff --git a/builder/lib/Mustache/test/_files/keys_with_questionmarks.js b/builder/lib/Mustache/test/_files/keys_with_questionmarks.js
new file mode 100644
index 000000000..becd63102
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/keys_with_questionmarks.js
@@ -0,0 +1,5 @@
+({
+ "person?": {
+ name: "Jon"
+ }
+})
diff --git a/builder/lib/Mustache/test/_files/keys_with_questionmarks.mustache b/builder/lib/Mustache/test/_files/keys_with_questionmarks.mustache
new file mode 100644
index 000000000..417f17f3c
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/keys_with_questionmarks.mustache
@@ -0,0 +1,3 @@
+{{#person?}}
+ Hi {{name}}!
+{{/person?}}
diff --git a/builder/lib/Mustache/test/_files/keys_with_questionmarks.txt b/builder/lib/Mustache/test/_files/keys_with_questionmarks.txt
new file mode 100644
index 000000000..0f69b9433
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/keys_with_questionmarks.txt
@@ -0,0 +1 @@
+ Hi Jon!
diff --git a/builder/lib/Mustache/test/_files/malicious_template.js b/builder/lib/Mustache/test/_files/malicious_template.js
new file mode 100644
index 000000000..b4100a597
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/malicious_template.js
@@ -0,0 +1 @@
+({})
diff --git a/builder/lib/Mustache/test/_files/malicious_template.mustache b/builder/lib/Mustache/test/_files/malicious_template.mustache
new file mode 100644
index 000000000..b956867ec
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/malicious_template.mustache
@@ -0,0 +1,5 @@
+{{"+(function () {throw "evil"})()+"}}
+{{{"+(function () {throw "evil"})()+"}}}
+{{> "+(function () {throw "evil"})()+"}}
+{{# "+(function () {throw "evil"})()+"}}
+{{/ "+(function () {throw "evil"})()+"}}
diff --git a/builder/lib/Mustache/test/_files/malicious_template.txt b/builder/lib/Mustache/test/_files/malicious_template.txt
new file mode 100644
index 000000000..139597f9c
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/malicious_template.txt
@@ -0,0 +1,2 @@
+
+
diff --git a/builder/lib/Mustache/test/_files/multiline_comment.js b/builder/lib/Mustache/test/_files/multiline_comment.js
new file mode 100644
index 000000000..b4100a597
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/multiline_comment.js
@@ -0,0 +1 @@
+({})
diff --git a/builder/lib/Mustache/test/_files/multiline_comment.mustache b/builder/lib/Mustache/test/_files/multiline_comment.mustache
new file mode 100644
index 000000000..dff0893d0
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/multiline_comment.mustache
@@ -0,0 +1,6 @@
+{{!
+
+This is a multi-line comment.
+
+}}
+Hello world!
diff --git a/builder/lib/Mustache/test/_files/multiline_comment.txt b/builder/lib/Mustache/test/_files/multiline_comment.txt
new file mode 100644
index 000000000..cd0875583
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/multiline_comment.txt
@@ -0,0 +1 @@
+Hello world!
diff --git a/builder/lib/Mustache/test/_files/nested_higher_order_sections.js b/builder/lib/Mustache/test/_files/nested_higher_order_sections.js
new file mode 100644
index 000000000..3ccf4d372
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/nested_higher_order_sections.js
@@ -0,0 +1,8 @@
+({
+ bold: function () {
+ return function (text, render) {
+ return '' + render(text) + '';
+ };
+ },
+ person: { name: 'Jonas' }
+});
diff --git a/builder/lib/Mustache/test/_files/nested_higher_order_sections.mustache b/builder/lib/Mustache/test/_files/nested_higher_order_sections.mustache
new file mode 100644
index 000000000..e312fe79a
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/nested_higher_order_sections.mustache
@@ -0,0 +1 @@
+{{#bold}}{{#person}}My name is {{name}}!{{/person}}{{/bold}}
diff --git a/builder/lib/Mustache/test/_files/nested_higher_order_sections.txt b/builder/lib/Mustache/test/_files/nested_higher_order_sections.txt
new file mode 100644
index 000000000..0ee6a406a
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/nested_higher_order_sections.txt
@@ -0,0 +1 @@
+My name is Jonas!
diff --git a/builder/lib/Mustache/test/_files/nested_iterating.js b/builder/lib/Mustache/test/_files/nested_iterating.js
new file mode 100644
index 000000000..2708b2db5
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/nested_iterating.js
@@ -0,0 +1,8 @@
+({
+ inner: [{
+ foo: 'foo',
+ inner: [{
+ bar: 'bar'
+ }]
+ }]
+})
diff --git a/builder/lib/Mustache/test/_files/nested_iterating.mustache b/builder/lib/Mustache/test/_files/nested_iterating.mustache
new file mode 100644
index 000000000..1a3bb1a2e
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/nested_iterating.mustache
@@ -0,0 +1 @@
+{{#inner}}{{foo}}{{#inner}}{{bar}}{{/inner}}{{/inner}}
diff --git a/builder/lib/Mustache/test/_files/nested_iterating.txt b/builder/lib/Mustache/test/_files/nested_iterating.txt
new file mode 100644
index 000000000..323fae03f
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/nested_iterating.txt
@@ -0,0 +1 @@
+foobar
diff --git a/builder/lib/Mustache/test/_files/nesting.js b/builder/lib/Mustache/test/_files/nesting.js
new file mode 100644
index 000000000..264cc2f77
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/nesting.js
@@ -0,0 +1,7 @@
+({
+ foo: [
+ {a: {b: 1}},
+ {a: {b: 2}},
+ {a: {b: 3}}
+ ]
+})
diff --git a/builder/lib/Mustache/test/_files/nesting.mustache b/builder/lib/Mustache/test/_files/nesting.mustache
new file mode 100644
index 000000000..551366dea
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/nesting.mustache
@@ -0,0 +1,5 @@
+{{#foo}}
+ {{#a}}
+ {{b}}
+ {{/a}}
+{{/foo}}
diff --git a/builder/lib/Mustache/test/_files/nesting.txt b/builder/lib/Mustache/test/_files/nesting.txt
new file mode 100644
index 000000000..7db34b172
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/nesting.txt
@@ -0,0 +1,3 @@
+ 1
+ 2
+ 3
diff --git a/builder/lib/Mustache/test/_files/nesting_same_name.js b/builder/lib/Mustache/test/_files/nesting_same_name.js
new file mode 100644
index 000000000..10a0c1401
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/nesting_same_name.js
@@ -0,0 +1,8 @@
+({
+ items: [
+ {
+ name: 'name',
+ items: [1, 2, 3, 4]
+ }
+ ]
+})
diff --git a/builder/lib/Mustache/test/_files/nesting_same_name.mustache b/builder/lib/Mustache/test/_files/nesting_same_name.mustache
new file mode 100644
index 000000000..777dbd685
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/nesting_same_name.mustache
@@ -0,0 +1 @@
+{{#items}}{{name}}{{#items}}{{.}}{{/items}}{{/items}}
diff --git a/builder/lib/Mustache/test/_files/nesting_same_name.txt b/builder/lib/Mustache/test/_files/nesting_same_name.txt
new file mode 100644
index 000000000..34fcfd3e8
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/nesting_same_name.txt
@@ -0,0 +1 @@
+name1234
diff --git a/builder/lib/Mustache/test/_files/null_string.js b/builder/lib/Mustache/test/_files/null_string.js
new file mode 100644
index 000000000..984ee516a
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/null_string.js
@@ -0,0 +1,10 @@
+({
+ name: "Elise",
+ glytch: true,
+ binary: false,
+ value: null,
+ undef: undefined,
+ numeric: function() {
+ return NaN;
+ }
+})
diff --git a/builder/lib/Mustache/test/_files/null_string.mustache b/builder/lib/Mustache/test/_files/null_string.mustache
new file mode 100644
index 000000000..a6f33009f
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/null_string.mustache
@@ -0,0 +1,6 @@
+Hello {{name}}
+glytch {{glytch}}
+binary {{binary}}
+value {{value}}
+undef {{undef}}
+numeric {{numeric}}
diff --git a/builder/lib/Mustache/test/_files/null_string.txt b/builder/lib/Mustache/test/_files/null_string.txt
new file mode 100644
index 000000000..bcabe0a5a
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/null_string.txt
@@ -0,0 +1,6 @@
+Hello Elise
+glytch true
+binary false
+value
+undef
+numeric NaN
diff --git a/builder/lib/Mustache/test/_files/null_view.js b/builder/lib/Mustache/test/_files/null_view.js
new file mode 100644
index 000000000..dbdae728e
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/null_view.js
@@ -0,0 +1,4 @@
+({
+ name: 'Joe',
+ friends: null
+})
diff --git a/builder/lib/Mustache/test/_files/null_view.mustache b/builder/lib/Mustache/test/_files/null_view.mustache
new file mode 100644
index 000000000..115b376b5
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/null_view.mustache
@@ -0,0 +1 @@
+{{name}}'s friends: {{#friends}}{{name}}, {{/friends}}
\ No newline at end of file
diff --git a/builder/lib/Mustache/test/_files/null_view.txt b/builder/lib/Mustache/test/_files/null_view.txt
new file mode 100644
index 000000000..15ed2abe1
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/null_view.txt
@@ -0,0 +1 @@
+Joe's friends:
\ No newline at end of file
diff --git a/builder/lib/Mustache/test/_files/partial_array.js b/builder/lib/Mustache/test/_files/partial_array.js
new file mode 100644
index 000000000..2a6ddf1cf
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_array.js
@@ -0,0 +1,3 @@
+({
+ array: ['1', '2', '3', '4']
+})
diff --git a/builder/lib/Mustache/test/_files/partial_array.mustache b/builder/lib/Mustache/test/_files/partial_array.mustache
new file mode 100644
index 000000000..7a336fee8
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_array.mustache
@@ -0,0 +1 @@
+{{>partial}}
\ No newline at end of file
diff --git a/builder/lib/Mustache/test/_files/partial_array.partial b/builder/lib/Mustache/test/_files/partial_array.partial
new file mode 100644
index 000000000..0ba652c1e
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_array.partial
@@ -0,0 +1,4 @@
+Here's a non-sense array of values
+{{#array}}
+ {{.}}
+{{/array}}
diff --git a/builder/lib/Mustache/test/_files/partial_array.txt b/builder/lib/Mustache/test/_files/partial_array.txt
new file mode 100644
index 000000000..892837cb7
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_array.txt
@@ -0,0 +1,5 @@
+Here's a non-sense array of values
+ 1
+ 2
+ 3
+ 4
diff --git a/builder/lib/Mustache/test/_files/partial_array_of_partials.js b/builder/lib/Mustache/test/_files/partial_array_of_partials.js
new file mode 100644
index 000000000..03f13c946
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_array_of_partials.js
@@ -0,0 +1,8 @@
+({
+ numbers: [
+ {i: '1'},
+ {i: '2'},
+ {i: '3'},
+ {i: '4'}
+ ]
+})
diff --git a/builder/lib/Mustache/test/_files/partial_array_of_partials.mustache b/builder/lib/Mustache/test/_files/partial_array_of_partials.mustache
new file mode 100644
index 000000000..1af6d68c6
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_array_of_partials.mustache
@@ -0,0 +1,4 @@
+Here is some stuff!
+{{#numbers}}
+{{>partial}}
+{{/numbers}}
diff --git a/builder/lib/Mustache/test/_files/partial_array_of_partials.partial b/builder/lib/Mustache/test/_files/partial_array_of_partials.partial
new file mode 100644
index 000000000..bdde77daf
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_array_of_partials.partial
@@ -0,0 +1 @@
+{{i}}
diff --git a/builder/lib/Mustache/test/_files/partial_array_of_partials.txt b/builder/lib/Mustache/test/_files/partial_array_of_partials.txt
new file mode 100644
index 000000000..f622375c8
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_array_of_partials.txt
@@ -0,0 +1,5 @@
+Here is some stuff!
+1
+2
+3
+4
diff --git a/builder/lib/Mustache/test/_files/partial_array_of_partials_implicit.js b/builder/lib/Mustache/test/_files/partial_array_of_partials_implicit.js
new file mode 100644
index 000000000..9ec0c00ff
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_array_of_partials_implicit.js
@@ -0,0 +1,3 @@
+({
+ numbers: ['1', '2', '3', '4']
+})
diff --git a/builder/lib/Mustache/test/_files/partial_array_of_partials_implicit.mustache b/builder/lib/Mustache/test/_files/partial_array_of_partials_implicit.mustache
new file mode 100644
index 000000000..1af6d68c6
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_array_of_partials_implicit.mustache
@@ -0,0 +1,4 @@
+Here is some stuff!
+{{#numbers}}
+{{>partial}}
+{{/numbers}}
diff --git a/builder/lib/Mustache/test/_files/partial_array_of_partials_implicit.partial b/builder/lib/Mustache/test/_files/partial_array_of_partials_implicit.partial
new file mode 100644
index 000000000..12f715986
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_array_of_partials_implicit.partial
@@ -0,0 +1 @@
+{{.}}
diff --git a/builder/lib/Mustache/test/_files/partial_array_of_partials_implicit.txt b/builder/lib/Mustache/test/_files/partial_array_of_partials_implicit.txt
new file mode 100644
index 000000000..f622375c8
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_array_of_partials_implicit.txt
@@ -0,0 +1,5 @@
+Here is some stuff!
+1
+2
+3
+4
diff --git a/builder/lib/Mustache/test/_files/partial_empty.js b/builder/lib/Mustache/test/_files/partial_empty.js
new file mode 100644
index 000000000..82b8c2242
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_empty.js
@@ -0,0 +1,3 @@
+({
+ foo: 1
+})
diff --git a/builder/lib/Mustache/test/_files/partial_empty.mustache b/builder/lib/Mustache/test/_files/partial_empty.mustache
new file mode 100644
index 000000000..a71004703
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_empty.mustache
@@ -0,0 +1,2 @@
+hey {{foo}}
+{{>partial}}
diff --git a/builder/lib/Mustache/test/_files/partial_empty.partial b/builder/lib/Mustache/test/_files/partial_empty.partial
new file mode 100644
index 000000000..e69de29bb
diff --git a/builder/lib/Mustache/test/_files/partial_empty.txt b/builder/lib/Mustache/test/_files/partial_empty.txt
new file mode 100644
index 000000000..1a679077b
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_empty.txt
@@ -0,0 +1 @@
+hey 1
diff --git a/builder/lib/Mustache/test/_files/partial_template.js b/builder/lib/Mustache/test/_files/partial_template.js
new file mode 100644
index 000000000..a913f8784
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_template.js
@@ -0,0 +1,6 @@
+({
+ title: function () {
+ return "Welcome";
+ },
+ again: "Goodbye"
+})
diff --git a/builder/lib/Mustache/test/_files/partial_template.mustache b/builder/lib/Mustache/test/_files/partial_template.mustache
new file mode 100644
index 000000000..6a7492e00
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_template.mustache
@@ -0,0 +1,2 @@
+
{{title}}
+{{>partial}}
diff --git a/builder/lib/Mustache/test/_files/partial_template.partial b/builder/lib/Mustache/test/_files/partial_template.partial
new file mode 100644
index 000000000..a40452924
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_template.partial
@@ -0,0 +1 @@
+Again, {{again}}!
diff --git a/builder/lib/Mustache/test/_files/partial_template.txt b/builder/lib/Mustache/test/_files/partial_template.txt
new file mode 100644
index 000000000..692698f08
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_template.txt
@@ -0,0 +1,2 @@
+
diff --git a/builder/lib/Mustache/test/_files/partial_view.partial b/builder/lib/Mustache/test/_files/partial_view.partial
new file mode 100644
index 000000000..03df20686
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_view.partial
@@ -0,0 +1,5 @@
+Hello {{name}}
+You have just won ${{value}}!
+{{#in_ca}}
+Well, ${{ taxed_value }}, after taxes.
+{{/in_ca}}
\ No newline at end of file
diff --git a/builder/lib/Mustache/test/_files/partial_view.txt b/builder/lib/Mustache/test/_files/partial_view.txt
new file mode 100644
index 000000000..c09147c7f
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/partial_view.txt
@@ -0,0 +1,5 @@
+
Welcome
+Hello Chris
+You have just won $10000!
+Well, $6000, after taxes.
+
+{{/a_object}}
diff --git a/builder/lib/Mustache/test/_files/section_as_context.txt b/builder/lib/Mustache/test/_files/section_as_context.txt
new file mode 100644
index 000000000..d834e8047
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/section_as_context.txt
@@ -0,0 +1,6 @@
+
this is an object
+
one of its attributes is a list
+
+
listitem1
+
listitem2
+
diff --git a/builder/lib/Mustache/test/_files/simple.js b/builder/lib/Mustache/test/_files/simple.js
new file mode 100644
index 000000000..1d8d6f425
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/simple.js
@@ -0,0 +1,8 @@
+({
+ name: "Chris",
+ value: 10000,
+ taxed_value: function () {
+ return this.value - (this.value * 0.4);
+ },
+ in_ca: true
+})
diff --git a/builder/lib/Mustache/test/_files/simple.mustache b/builder/lib/Mustache/test/_files/simple.mustache
new file mode 100644
index 000000000..2fea6327a
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/simple.mustache
@@ -0,0 +1,5 @@
+Hello {{name}}
+You have just won ${{value}}!
+{{#in_ca}}
+Well, ${{ taxed_value }}, after taxes.
+{{/in_ca}}
diff --git a/builder/lib/Mustache/test/_files/simple.txt b/builder/lib/Mustache/test/_files/simple.txt
new file mode 100644
index 000000000..5d75d6562
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/simple.txt
@@ -0,0 +1,3 @@
+Hello Chris
+You have just won $10000!
+Well, $6000, after taxes.
diff --git a/builder/lib/Mustache/test/_files/string_as_context.js b/builder/lib/Mustache/test/_files/string_as_context.js
new file mode 100644
index 000000000..e8bb4da00
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/string_as_context.js
@@ -0,0 +1,4 @@
+({
+ a_string: 'aa',
+ a_list: ['a','b','c']
+})
diff --git a/builder/lib/Mustache/test/_files/string_as_context.mustache b/builder/lib/Mustache/test/_files/string_as_context.mustache
new file mode 100644
index 000000000..00f7181b1
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/string_as_context.mustache
@@ -0,0 +1,5 @@
+
+{{#a_list}}
+
{{a_string}}/{{.}}
+{{/a_list}}
+
\ No newline at end of file
diff --git a/builder/lib/Mustache/test/_files/string_as_context.txt b/builder/lib/Mustache/test/_files/string_as_context.txt
new file mode 100644
index 000000000..8bd87ff82
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/string_as_context.txt
@@ -0,0 +1,5 @@
+
+
aa/a
+
aa/b
+
aa/c
+
\ No newline at end of file
diff --git a/builder/lib/Mustache/test/_files/two_in_a_row.js b/builder/lib/Mustache/test/_files/two_in_a_row.js
new file mode 100644
index 000000000..9c17c1173
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/two_in_a_row.js
@@ -0,0 +1,4 @@
+({
+ name: "Joe",
+ greeting: "Welcome"
+})
diff --git a/builder/lib/Mustache/test/_files/two_in_a_row.mustache b/builder/lib/Mustache/test/_files/two_in_a_row.mustache
new file mode 100644
index 000000000..b23f29e87
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/two_in_a_row.mustache
@@ -0,0 +1 @@
+{{greeting}}, {{name}}!
diff --git a/builder/lib/Mustache/test/_files/two_in_a_row.txt b/builder/lib/Mustache/test/_files/two_in_a_row.txt
new file mode 100644
index 000000000..c6d6a9b48
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/two_in_a_row.txt
@@ -0,0 +1 @@
+Welcome, Joe!
diff --git a/builder/lib/Mustache/test/_files/two_sections.js b/builder/lib/Mustache/test/_files/two_sections.js
new file mode 100644
index 000000000..b4100a597
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/two_sections.js
@@ -0,0 +1 @@
+({})
diff --git a/builder/lib/Mustache/test/_files/two_sections.mustache b/builder/lib/Mustache/test/_files/two_sections.mustache
new file mode 100644
index 000000000..a4b9f2a78
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/two_sections.mustache
@@ -0,0 +1,4 @@
+{{#foo}}
+{{/foo}}
+{{#bar}}
+{{/bar}}
diff --git a/builder/lib/Mustache/test/_files/two_sections.txt b/builder/lib/Mustache/test/_files/two_sections.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/builder/lib/Mustache/test/_files/unescaped.js b/builder/lib/Mustache/test/_files/unescaped.js
new file mode 100644
index 000000000..b6d064f12
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/unescaped.js
@@ -0,0 +1,5 @@
+({
+ title: function () {
+ return "Bear > Shark";
+ }
+})
diff --git a/builder/lib/Mustache/test/_files/unescaped.mustache b/builder/lib/Mustache/test/_files/unescaped.mustache
new file mode 100644
index 000000000..6b07d7b71
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/unescaped.mustache
@@ -0,0 +1 @@
+
{{{title}}}
diff --git a/builder/lib/Mustache/test/_files/unescaped.txt b/builder/lib/Mustache/test/_files/unescaped.txt
new file mode 100644
index 000000000..089ad7967
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/unescaped.txt
@@ -0,0 +1 @@
+
Bear > Shark
diff --git a/builder/lib/Mustache/test/_files/whitespace.js b/builder/lib/Mustache/test/_files/whitespace.js
new file mode 100644
index 000000000..f41cb5640
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/whitespace.js
@@ -0,0 +1,4 @@
+({
+ tag1: "Hello",
+ tag2: "World"
+})
diff --git a/builder/lib/Mustache/test/_files/whitespace.mustache b/builder/lib/Mustache/test/_files/whitespace.mustache
new file mode 100644
index 000000000..aa76e08ea
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/whitespace.mustache
@@ -0,0 +1,4 @@
+{{tag1}}
+
+
+{{tag2}}.
diff --git a/builder/lib/Mustache/test/_files/whitespace.txt b/builder/lib/Mustache/test/_files/whitespace.txt
new file mode 100644
index 000000000..851fa7448
--- /dev/null
+++ b/builder/lib/Mustache/test/_files/whitespace.txt
@@ -0,0 +1,4 @@
+Hello
+
+
+World.
diff --git a/builder/lib/Mustache/test/context-test.js b/builder/lib/Mustache/test/context-test.js
new file mode 100644
index 000000000..752f74bc7
--- /dev/null
+++ b/builder/lib/Mustache/test/context-test.js
@@ -0,0 +1,51 @@
+require('./helper');
+var Context = Mustache.Context;
+
+describe('A new Mustache.Context', function () {
+ var context;
+ beforeEach(function () {
+ context = new Context({ name: 'parent', message: 'hi', a: { b: 'b' } });
+ });
+
+ it('is able to lookup properties of its own view', function () {
+ assert.equal(context.lookup('name'), 'parent');
+ });
+
+ it('is able to lookup nested properties of its own view', function () {
+ assert.equal(context.lookup('a.b'), 'b');
+ });
+
+ describe('when pushed', function () {
+ beforeEach(function () {
+ context = context.push({ name: 'child', c: { d: 'd' } });
+ });
+
+ it('returns the child context', function () {
+ assert.equal(context.view.name, 'child');
+ assert.equal(context.parent.view.name, 'parent');
+ });
+
+ it('is able to lookup properties of its own view', function () {
+ assert.equal(context.lookup('name'), 'child');
+ });
+
+ it("is able to lookup properties of the parent context's view", function () {
+ assert.equal(context.lookup('message'), 'hi');
+ });
+
+ it('is able to lookup nested properties of its own view', function () {
+ assert.equal(context.lookup('c.d'), 'd');
+ });
+
+ it('is able to lookup nested properties of its parent view', function () {
+ assert.equal(context.lookup('a.b'), 'b');
+ });
+ });
+});
+
+describe('Mustache.Context.make', function () {
+ it('returns the same object when given a Context', function () {
+ var context = new Context;
+ assert.strictEqual(Context.make(context), context);
+ });
+});
diff --git a/builder/lib/Mustache/test/helper.js b/builder/lib/Mustache/test/helper.js
new file mode 100644
index 000000000..a91fe499f
--- /dev/null
+++ b/builder/lib/Mustache/test/helper.js
@@ -0,0 +1,2 @@
+assert = require('assert');
+Mustache = require('../mustache');
diff --git a/builder/lib/Mustache/test/mustache-spec-test.js b/builder/lib/Mustache/test/mustache-spec-test.js
new file mode 100644
index 000000000..1050ea43d
--- /dev/null
+++ b/builder/lib/Mustache/test/mustache-spec-test.js
@@ -0,0 +1,89 @@
+require('./helper');
+
+var fs = require('fs');
+var path = require('path');
+var specsDir = path.join(__dirname, 'spec/specs');
+
+var skipTests = {
+ comments: [
+ 'Standalone Without Newline'
+ ],
+ delimiters: [
+ 'Standalone Without Newline'
+ ],
+ inverted: [
+ 'Standalone Without Newline'
+ ],
+ partials: [
+ 'Standalone Without Previous Line',
+ 'Standalone Without Newline',
+ 'Standalone Indentation'
+ ],
+ sections: [
+ 'Standalone Without Newline'
+ ],
+ '~lambdas': [
+ 'Interpolation',
+ 'Interpolation - Expansion',
+ 'Interpolation - Alternate Delimiters',
+ 'Interpolation - Multiple Calls',
+ 'Escaping',
+ 'Section - Expansion',
+ 'Section - Alternate Delimiters'
+ ]
+};
+
+// You can run the skiped tests by setting the NOSKIP environment variable to
+// true (e.g. NOSKIP=true mocha test/mustache-spec-test.js)
+var noSkip = process.env.NOSKIP;
+
+// You can put the name of a specific test file to run in the TEST environment
+// variable (e.g. TEST=interpolation mocha test/mustache-spec-test.js)
+var fileToRun = process.env.TEST;
+
+// Mustache should work on node 0.6 that doesn't have fs.exisisSync
+function existsDir(path) {
+ try {
+ return fs.statSync(path).isDirectory();
+ } catch (x) {
+ return false;
+ }
+}
+
+var specFiles;
+if (fileToRun) {
+ specFiles = [fileToRun];
+} else if (existsDir(specsDir)) {
+ specFiles = fs.readdirSync(specsDir).filter(function (file) {
+ return (/\.json$/).test(file);
+ }).map(function (file) {
+ return path.basename(file).replace(/\.json$/, '');
+ }).sort();
+} else {
+ specFiles = [];
+}
+
+function getSpecs(specArea) {
+ return JSON.parse(fs.readFileSync(path.join(specsDir, specArea + '.' + 'json'), 'utf8'));
+}
+
+describe('Mustache spec compliance', function() {
+ beforeEach(function () {
+ Mustache.clearCache();
+ });
+
+ specFiles.forEach(function(specArea) {
+ describe('- ' + specArea + ':', function() {
+ var specs = getSpecs(specArea);
+ specs.tests.forEach(function(test) {
+ var it_ = (!noSkip && skipTests[specArea] && skipTests[specArea].indexOf(test.name) >= 0) ? it.skip : it;
+ it_(test.name + ' - ' + test.desc, function() {
+ if (test.data.lambda && test.data.lambda.__tag__ === 'code')
+ test.data.lambda = eval('(function() { return ' + test.data.lambda.js + '; })');
+ var output = Mustache.render(test.template, test.data, test.partials);
+ assert.equal(output, test.expected);
+ });
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/builder/lib/Mustache/test/parse-test.js b/builder/lib/Mustache/test/parse-test.js
new file mode 100644
index 000000000..40d23a431
--- /dev/null
+++ b/builder/lib/Mustache/test/parse-test.js
@@ -0,0 +1,106 @@
+require('./helper');
+
+// A map of templates to their expected token output. Tokens are in the format:
+// [type, value, startIndex, endIndex, subTokens].
+var expectations = {
+ '' : [],
+ '{{hi}}' : [ [ 'name', 'hi', 0, 6 ] ],
+ '{{hi.world}}' : [ [ 'name', 'hi.world', 0, 12 ] ],
+ '{{hi . world}}' : [ [ 'name', 'hi . world', 0, 14 ] ],
+ '{{ hi}}' : [ [ 'name', 'hi', 0, 7 ] ],
+ '{{hi }}' : [ [ 'name', 'hi', 0, 7 ] ],
+ '{{ hi }}' : [ [ 'name', 'hi', 0, 8 ] ],
+ '{{{hi}}}' : [ [ '&', 'hi', 0, 8 ] ],
+ '{{!hi}}' : [ [ '!', 'hi', 0, 7 ] ],
+ '{{! hi}}' : [ [ '!', 'hi', 0, 8 ] ],
+ '{{! hi }}' : [ [ '!', 'hi', 0, 9 ] ],
+ '{{ !hi}}' : [ [ '!', 'hi', 0, 8 ] ],
+ '{{ ! hi}}' : [ [ '!', 'hi', 0, 9 ] ],
+ '{{ ! hi }}' : [ [ '!', 'hi', 0, 10 ] ],
+ 'a\n b' : [ [ 'text', 'a\n b', 0, 4 ] ],
+ 'a{{hi}}' : [ [ 'text', 'a', 0, 1 ], [ 'name', 'hi', 1, 7 ] ],
+ 'a {{hi}}' : [ [ 'text', 'a ', 0, 2 ], [ 'name', 'hi', 2, 8 ] ],
+ ' a{{hi}}' : [ [ 'text', ' a', 0, 2 ], [ 'name', 'hi', 2, 8 ] ],
+ ' a {{hi}}' : [ [ 'text', ' a ', 0, 3 ], [ 'name', 'hi', 3, 9 ] ],
+ 'a{{hi}}b' : [ [ 'text', 'a', 0, 1 ], [ 'name', 'hi', 1, 7 ], [ 'text', 'b', 7, 8 ] ],
+ 'a{{hi}} b' : [ [ 'text', 'a', 0, 1 ], [ 'name', 'hi', 1, 7 ], [ 'text', ' b', 7, 9 ] ],
+ 'a{{hi}}b ' : [ [ 'text', 'a', 0, 1 ], [ 'name', 'hi', 1, 7 ], [ 'text', 'b ', 7, 9 ] ],
+ 'a\n{{hi}} b \n' : [ [ 'text', 'a\n', 0, 2 ], [ 'name', 'hi', 2, 8 ], [ 'text', ' b \n', 8, 12 ] ],
+ 'a\n {{hi}} \nb' : [ [ 'text', 'a\n ', 0, 3 ], [ 'name', 'hi', 3, 9 ], [ 'text', ' \nb', 9, 12 ] ],
+ 'a\n {{!hi}} \nb' : [ [ 'text', 'a\n', 0, 2 ], [ '!', 'hi', 3, 10 ], [ 'text', 'b', 12, 13 ] ],
+ 'a\n{{#a}}{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [], 8 ], [ 'text', 'b', 15, 16 ] ],
+ 'a\n {{#a}}{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [], 9 ], [ 'text', 'b', 16, 17 ] ],
+ 'a\n {{#a}}{{/a}} \nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [], 9 ], [ 'text', 'b', 17, 18 ] ],
+ 'a\n{{#a}}\n{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [], 9 ], [ 'text', 'b', 16, 17 ] ],
+ 'a\n {{#a}}\n{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [], 10 ], [ 'text', 'b', 17, 18 ] ],
+ 'a\n {{#a}}\n{{/a}} \nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [], 10 ], [ 'text', 'b', 18, 19 ] ],
+ 'a\n{{#a}}\n{{/a}}\n{{#b}}\n{{/b}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [], 9 ], [ '#', 'b', 16, 22, [], 23 ], [ 'text', 'b', 30, 31 ] ],
+ 'a\n {{#a}}\n{{/a}}\n{{#b}}\n{{/b}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [], 10 ], [ '#', 'b', 17, 23, [], 24 ], [ 'text', 'b', 31, 32 ] ],
+ 'a\n {{#a}}\n{{/a}}\n{{#b}}\n{{/b}} \nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [], 10 ], [ '#', 'b', 17, 23, [], 24 ], [ 'text', 'b', 32, 33 ] ],
+ 'a\n{{#a}}\n{{#b}}\n{{/b}}\n{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [ [ '#', 'b', 9, 15, [], 16 ] ], 23 ], [ 'text', 'b', 30, 31 ] ],
+ 'a\n {{#a}}\n{{#b}}\n{{/b}}\n{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [ [ '#', 'b', 10, 16, [], 17 ] ], 24 ], [ 'text', 'b', 31, 32 ] ],
+ 'a\n {{#a}}\n{{#b}}\n{{/b}}\n{{/a}} \nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [ [ '#', 'b', 10, 16, [], 17 ] ], 24 ], [ 'text', 'b', 32, 33 ] ],
+ '{{>abc}}' : [ [ '>', 'abc', 0, 8 ] ],
+ '{{> abc }}' : [ [ '>', 'abc', 0, 10 ] ],
+ '{{ > abc }}' : [ [ '>', 'abc', 0, 11 ] ],
+ '{{=<% %>=}}' : [ [ '=', '<% %>', 0, 11 ] ],
+ '{{= <% %> =}}' : [ [ '=', '<% %>', 0, 13 ] ],
+ '{{=<% %>=}}<%={{ }}=%>' : [ [ '=', '<% %>', 0, 11 ], [ '=', '{{ }}', 11, 22 ] ],
+ '{{=<% %>=}}<%hi%>' : [ [ '=', '<% %>', 0, 11 ], [ 'name', 'hi', 11, 17 ] ],
+ '{{#a}}{{/a}}hi{{#b}}{{/b}}\n' : [ [ '#', 'a', 0, 6, [], 6 ], [ 'text', 'hi', 12, 14 ], [ '#', 'b', 14, 20, [], 20 ], [ 'text', '\n', 26, 27 ] ],
+ '{{a}}\n{{b}}\n\n{{#c}}\n{{/c}}\n' : [ [ 'name', 'a', 0, 5 ], [ 'text', '\n', 5, 6 ], [ 'name', 'b', 6, 11 ], [ 'text', '\n\n', 11, 13 ], [ '#', 'c', 13, 19, [], 20 ] ],
+ '{{#foo}}\n {{#a}}\n {{b}}\n {{/a}}\n{{/foo}}\n'
+ : [ [ '#', 'foo', 0, 8, [ [ '#', 'a', 11, 17, [ [ 'text', ' ', 18, 22 ], [ 'name', 'b', 22, 27 ], [ 'text', '\n', 27, 28 ] ], 30 ] ], 37 ] ]
+};
+
+describe('Mustache.parse', function () {
+
+ for (var template in expectations) {
+ (function (template, tokens) {
+ it('knows how to parse ' + JSON.stringify(template), function () {
+ assert.deepEqual(Mustache.parse(template), tokens);
+ });
+ })(template, expectations[template]);
+ }
+
+ describe('when there is an unclosed tag', function () {
+ it('throws an error', function () {
+ assert.throws(function () {
+ Mustache.parse('My name is {{name');
+ }, /unclosed tag at 17/i);
+ });
+ });
+
+ describe('when there is an unclosed section', function () {
+ it('throws an error', function () {
+ assert.throws(function () {
+ Mustache.parse('A list: {{#people}}{{name}}');
+ }, /unclosed section "people" at 27/i);
+ });
+ });
+
+ describe('when there is an unopened section', function () {
+ it('throws an error', function () {
+ assert.throws(function () {
+ Mustache.parse('The end of the list! {{/people}}');
+ }, /unopened section "people" at 21/i);
+ });
+ });
+
+ describe('when invalid tags are given as an argument', function () {
+ it('throws an error', function () {
+ assert.throws(function () {
+ Mustache.parse('A template <% name %>', [ '<%' ]);
+ }, /invalid tags/i);
+ });
+ });
+
+ describe('when the template contains invalid tags', function () {
+ it('throws an error', function () {
+ assert.throws(function () {
+ Mustache.parse('A template {{=<%=}}');
+ }, /invalid tags at 11/i);
+ });
+ });
+
+});
diff --git a/builder/lib/Mustache/test/render-test.js b/builder/lib/Mustache/test/render-test.js
new file mode 100644
index 000000000..acec47ffd
--- /dev/null
+++ b/builder/lib/Mustache/test/render-test.js
@@ -0,0 +1,68 @@
+require('./helper');
+
+var fs = require('fs');
+var path = require('path');
+var _files = path.join(__dirname, '_files');
+
+function getContents(testName, ext) {
+ return fs.readFileSync(path.join(_files, testName + '.' + ext), 'utf8');
+}
+
+function getView(testName) {
+ var view = getContents(testName, 'js');
+ if (!view) throw new Error('Cannot find view for test "' + testName + '"');
+ return eval(view);
+}
+
+function getPartial(testName) {
+ try {
+ return getContents(testName, 'partial');
+ } catch (e) {
+ // No big deal. Not all tests need to test partial support.
+ }
+}
+
+function getTest(testName) {
+ var test = {};
+ test.view = getView(testName);
+ test.template = getContents(testName, 'mustache');
+ test.partial = getPartial(testName);
+ test.expect = getContents(testName, 'txt');
+ return test;
+}
+
+// You can put the name of a specific test to run in the TEST environment
+// variable (e.g. TEST=backslashes vows test/render-test.js)
+var testToRun = process.env.TEST;
+
+var testNames;
+if (testToRun) {
+ testNames = [testToRun];
+} else {
+ testNames = fs.readdirSync(_files).filter(function (file) {
+ return (/\.js$/).test(file);
+ }).map(function (file) {
+ return path.basename(file).replace(/\.js$/, '');
+ });
+}
+
+describe('Mustache.render', function () {
+ beforeEach(function () {
+ Mustache.clearCache();
+ });
+
+ testNames.forEach(function (testName) {
+ var test = getTest(testName);
+
+ it('knows how to render ' + testName, function () {
+ var output;
+ if (test.partial) {
+ output = Mustache.render(test.template, test.view, { partial: test.partial });
+ } else {
+ output = Mustache.render(test.template, test.view);
+ }
+
+ assert.equal(output, test.expect);
+ });
+ });
+});
diff --git a/builder/lib/Mustache/test/scanner-test.js b/builder/lib/Mustache/test/scanner-test.js
new file mode 100644
index 000000000..9c9766420
--- /dev/null
+++ b/builder/lib/Mustache/test/scanner-test.js
@@ -0,0 +1,78 @@
+require('./helper');
+var Scanner = Mustache.Scanner;
+
+describe('A new Mustache.Scanner', function () {
+ describe('for an empty string', function () {
+ it('is at the end', function () {
+ var scanner = new Scanner('');
+ assert(scanner.eos());
+ });
+ });
+
+ describe('for a non-empty string', function () {
+ var scanner;
+ beforeEach(function () {
+ scanner = new Scanner('a b c');
+ });
+
+ describe('scan', function () {
+ describe('when the RegExp matches the entire string', function () {
+ it('returns the entire string', function () {
+ var match = scanner.scan(/a b c/);
+ assert.equal(match, scanner.string);
+ assert(scanner.eos());
+ });
+ });
+
+ describe('when the RegExp matches at index 0', function () {
+ it('returns the portion of the string that matched', function () {
+ var match = scanner.scan(/a/);
+ assert.equal(match, 'a');
+ assert.equal(scanner.pos, 1);
+ });
+ });
+
+ describe('when the RegExp matches at some index other than 0', function () {
+ it('returns the empty string', function () {
+ var match = scanner.scan(/b/);
+ assert.equal(match, '');
+ assert.equal(scanner.pos, 0);
+ });
+ });
+
+ describe('when the RegExp does not match', function () {
+ it('returns the empty string', function () {
+ var match = scanner.scan(/z/);
+ assert.equal(match, '');
+ assert.equal(scanner.pos, 0);
+ });
+ });
+ }); // scan
+
+ describe('scanUntil', function () {
+ describe('when the RegExp matches at index 0', function () {
+ it('returns the empty string', function () {
+ var match = scanner.scanUntil(/a/);
+ assert.equal(match, '');
+ assert.equal(scanner.pos, 0);
+ });
+ });
+
+ describe('when the RegExp matches at some index other than 0', function () {
+ it('returns the string up to that index', function () {
+ var match = scanner.scanUntil(/b/);
+ assert.equal(match, 'a ');
+ assert.equal(scanner.pos, 2);
+ });
+ });
+
+ describe('when the RegExp does not match', function () {
+ it('returns the entire string', function () {
+ var match = scanner.scanUntil(/z/);
+ assert.equal(match, scanner.string);
+ assert(scanner.eos());
+ });
+ });
+ }); // scanUntil
+ }); // for a non-empty string
+});
diff --git a/builder/lib/Mustache/test/writer-test.js b/builder/lib/Mustache/test/writer-test.js
new file mode 100644
index 000000000..db2813a5f
--- /dev/null
+++ b/builder/lib/Mustache/test/writer-test.js
@@ -0,0 +1,43 @@
+require('./helper');
+var Writer = Mustache.Writer;
+
+describe('A new Mustache.Writer', function () {
+ var writer;
+ beforeEach(function () {
+ writer = new Writer;
+ });
+
+ it('loads partials correctly', function () {
+ var partial = 'The content of the partial.';
+ var result = writer.render('{{>partial}}', {}, function (name) {
+ assert.equal(name, 'partial');
+ return partial;
+ });
+
+ assert.equal(result, partial);
+ });
+
+ it('caches partials by content, not name', function () {
+ var result = writer.render('{{>partial}}', {}, {
+ partial: 'partial one'
+ });
+
+ assert.equal(result, 'partial one');
+
+ result = writer.render('{{>partial}}', {}, {
+ partial: 'partial two'
+ });
+
+ assert.equal(result, 'partial two');
+ });
+
+ it('can compile an array of tokens', function () {
+ var template = 'Hello {{name}}!';
+ var tokens = Mustache.parse(template);
+ var render = writer.compileTokens(tokens, template);
+
+ var result = render({ name: 'Michael' });
+
+ assert.equal(result, 'Hello Michael!');
+ });
+});
diff --git a/builder/lib/Mustache/wrappers/dojo/mustache.js.post b/builder/lib/Mustache/wrappers/dojo/mustache.js.post
new file mode 100644
index 000000000..eeeb4b7f0
--- /dev/null
+++ b/builder/lib/Mustache/wrappers/dojo/mustache.js.post
@@ -0,0 +1,4 @@
+
+ dojox.mustache = dojo.hitch(Mustache, "render");
+
+})();
\ No newline at end of file
diff --git a/builder/lib/Mustache/wrappers/dojo/mustache.js.pre b/builder/lib/Mustache/wrappers/dojo/mustache.js.pre
new file mode 100644
index 000000000..f87f3cd77
--- /dev/null
+++ b/builder/lib/Mustache/wrappers/dojo/mustache.js.pre
@@ -0,0 +1,9 @@
+/*
+Shameless port of a shameless port
+@defunkt => @janl => @aq => @voodootikigod
+
+See http://github.com/defunkt/mustache for more info.
+*/
+
+dojo.provide("dojox.mustache._base");
+(function(){
diff --git a/builder/lib/Mustache/wrappers/jquery/mustache.js.post b/builder/lib/Mustache/wrappers/jquery/mustache.js.post
new file mode 100644
index 000000000..3209e915f
--- /dev/null
+++ b/builder/lib/Mustache/wrappers/jquery/mustache.js.post
@@ -0,0 +1,13 @@
+ $.mustache = function (template, view, partials) {
+ return Mustache.render(template, view, partials);
+ };
+
+ $.fn.mustache = function (view, partials) {
+ return $(this).map(function (i, elm) {
+ var template = $.trim($(elm).html());
+ var output = $.mustache(template, view, partials);
+ return $(output).get();
+ });
+ };
+
+})(jQuery);
diff --git a/builder/lib/Mustache/wrappers/jquery/mustache.js.pre b/builder/lib/Mustache/wrappers/jquery/mustache.js.pre
new file mode 100644
index 000000000..b4d8af5e1
--- /dev/null
+++ b/builder/lib/Mustache/wrappers/jquery/mustache.js.pre
@@ -0,0 +1,9 @@
+/*
+Shameless port of a shameless port
+@defunkt => @janl => @aq
+
+See http://github.com/defunkt/mustache for more info.
+*/
+
+;(function($) {
+
diff --git a/builder/lib/Mustache/wrappers/mootools/mustache.js.post b/builder/lib/Mustache/wrappers/mootools/mustache.js.post
new file mode 100644
index 000000000..aa9b8fab7
--- /dev/null
+++ b/builder/lib/Mustache/wrappers/mootools/mustache.js.post
@@ -0,0 +1,5 @@
+
+ Object.implement('mustache', function(view, partials){
+ return Mustache.render(view, this, partials);
+ });
+})();
diff --git a/builder/lib/Mustache/wrappers/mootools/mustache.js.pre b/builder/lib/Mustache/wrappers/mootools/mustache.js.pre
new file mode 100644
index 000000000..9839f9931
--- /dev/null
+++ b/builder/lib/Mustache/wrappers/mootools/mustache.js.pre
@@ -0,0 +1,2 @@
+(function(){
+
diff --git a/builder/lib/Mustache/wrappers/qooxdoo/mustache.js.post b/builder/lib/Mustache/wrappers/qooxdoo/mustache.js.post
new file mode 100644
index 000000000..6488b9c9c
--- /dev/null
+++ b/builder/lib/Mustache/wrappers/qooxdoo/mustache.js.post
@@ -0,0 +1,9 @@
+/**
+ * Above is the original mustache code.
+ */
+
+// EXPOSE qooxdoo variant
+qx.bom.Template.version = this.Mustache.version;
+qx.bom.Template.render = this.Mustache.render;
+
+}).call({});
\ No newline at end of file
diff --git a/builder/lib/Mustache/wrappers/qooxdoo/mustache.js.pre b/builder/lib/Mustache/wrappers/qooxdoo/mustache.js.pre
new file mode 100644
index 000000000..22aca98c6
--- /dev/null
+++ b/builder/lib/Mustache/wrappers/qooxdoo/mustache.js.pre
@@ -0,0 +1,164 @@
+/* ************************************************************************
+
+ qooxdoo - the new era of web development
+
+ http://qooxdoo.org
+
+ Copyright:
+ 2004-2012 1&1 Internet AG, Germany, http://www.1und1.de
+
+ License:
+ LGPL: http://www.gnu.org/licenses/lgpl.html
+ EPL: http://www.eclipse.org/org/documents/epl-v10.php
+ See the LICENSE file in the project's top-level directory for details.
+
+ Authors:
+ * Martin Wittemann (martinwittemann)
+
+ ======================================================================
+
+ This class contains code based on the following work:
+
+ * Mustache.js version 0.7.2
+
+ Code:
+ https://github.com/janl/mustache.js
+
+ Copyright:
+ (c) 2009 Chris Wanstrath (Ruby)
+ (c) 2010 Jan Lehnardt (JavaScript)
+
+ License:
+ MIT: http://www.opensource.org/licenses/mit-license.php
+
+ ----------------------------------------------------------------------
+
+ Copyright (c) 2009 Chris Wanstrath (Ruby)
+ Copyright (c) 2010 Jan Lehnardt (JavaScript)
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+************************************************************************ */
+
+/**
+ * The is a template class which can be used for HTML templating. In fact,
+ * this is a wrapper for mustache.js which is a "framework-agnostic way to
+ * render logic-free views".
+ *
+ * Here is a basic example how to use it:
+ * Template:
+ *
+ * var template = "Hi, my name is {{name}}!";
+ * var view = {name: "qooxdoo"};
+ * qx.bom.Template.render(template, view);
+ * // return "Hi, my name is qooxdoo!"
+ *
+ *
+ * For further details, please visit the mustache.js documentation here:
+ * https://github.com/janl/mustache.js/blob/master/README.md
+ *
+ * @ignore(module)
+ */
+qx.Bootstrap.define("qx.bom.Template", {
+ statics : {
+ /** Contains the mustache.js version. */
+ version: null,
+
+ /**
+ * Original and only template method of mustache.js. For further
+ * documentation, please visit https://github.com/janl/mustache.js
+ *
+ * @signature function(template, view, partials)
+ * @param template {String} The String containing the template.
+ * @param view {Object} The object holding the data to render.
+ * @param partials {Object} Object holding parts of a template.
+ * @return {String} The parsed template.
+ */
+ render: null,
+
+ /**
+ * Combines {@link #render} and {@link #get}. Input is equal to {@link #render}
+ * and output is equal to {@link #get}. The advantage over {@link #get}
+ * is that you don't need a HTML template but can use a template
+ * string and still get a DOM element. Keep in mind that templates
+ * can only have one root element.
+ *
+ * @param template {String} The String containing the template.
+ * @param view {Object} The object holding the data to render.
+ * @param partials {Object} Object holding parts of a template.
+ * @return {Element} A DOM element holding the parsed template data.
+ */
+ renderToNode : function(template, view, partials) {
+ var renderedTmpl = this.render(template, view, partials);
+ return this._createNodeFromTemplate(renderedTmpl);
+ },
+
+ /**
+ * Helper method which provides you with a direct access to templates
+ * stored as HTML in the DOM. The DOM node with the given ID will be used
+ * as a template, parsed and a new DOM node will be returned containing the
+ * parsed data. Keep in mind to have only one root DOM element in the the
+ * template.
+ * Additionally, you should not put the template into a regular, hidden
+ * DOM element because the template may not be valid HTML due to the containing
+ * mustache tags. We suggest to put it into a script tag with the type
+ * text/template.
+ *
+ * @param id {String} The id of the HTML template in the DOM.
+ * @param view {Object} The object holding the data to render.
+ * @param partials {Object} Object holding parts of a template.
+ * @return {Element} A DOM element holding the parsed template data.
+ */
+ get : function(id, view, partials) {
+ // get the content stored in the DOM
+ var template = document.getElementById(id);
+ return this.renderToNode(template.innerHTML, view, partials);
+ },
+
+ /**
+ * Accepts a parsed template and returns a (potentially nested) node.
+ *
+ * @param template {String} The String containing the template.
+ * @return {Element} A DOM element holding the parsed template data.
+ */
+ _createNodeFromTemplate : function(template) {
+ // template is text only (no html elems) so use text node
+ if (template.search(/<|>/) === -1) {
+ return document.createTextNode(template);
+ }
+
+ // template has html elems so convert string into DOM nodes
+ var helper = qx.dom.Element.create("div");
+ helper.innerHTML = template;
+
+ return helper.children[0];
+ }
+ }
+});
+
+(function() {
+
+/**
+ * Below is the original mustache.js code. Snapshot date is mentioned in
+ * the head of this file.
+ * @ignore(exports)
+ * @ignore(define.*)
+ * @ignore(module.*)
+ */
diff --git a/builder/patternlab.js b/builder/patternlab.js
new file mode 100644
index 000000000..da16cf34d
--- /dev/null
+++ b/builder/patternlab.js
@@ -0,0 +1,330 @@
+var path = require('path');
+
+var oPattern = function(name, subdir, filename, data){
+ this.name = name; //this is the unique name with the subDir
+ this.subdir = subdir;
+ this.filename = filename;
+ this.data = data;
+ this.template = '';
+ this.patternOutput = '';
+ this.patternName = ''; //this is the display name for the ui
+ this.patternLink = '';
+ this.patternGroup = name.substring(name.indexOf('-') + 1, name.indexOf('-', 4) + 1 - name.indexOf('-') + 1);
+ this.patternSubGroup = subdir.substring(subdir.indexOf('/') + 4);
+ this.flatPatternPath = subdir.replace(/\//g, '-');
+};
+
+var oBucket = function(name){
+ this.bucketNameLC = name;
+ this.bucketNameUC = name.charAt(0).toUpperCase() + name.slice(1);
+ this.navItems = [];
+ this.navItemsIndex = [];
+ this.patternItems = [];
+ this.patternItemsIndex = [];
+};
+
+var oNavItem = function(name){
+ this.sectionNameLC = name;
+ this.sectionNameUC = name.charAt(0).toUpperCase() + name.slice(1);
+ this.navSubItems = [];
+ this.navSubItemsIndex = [];
+};
+
+var oNavSubItem = function(name){
+ this.patternPath = '';
+ this.patternPartial = '';
+ this.patternName = name.charAt(0).toUpperCase() + name.slice(1);;
+};
+
+var oPatternItem = function(){
+ this.patternPath = '';
+ this.patternPartial = '';
+ this.patternName = '';
+};
+
+var mustache = require('./lib/Mustache/mustache.js');
+
+module.exports = function(grunt) {
+ grunt.registerTask('patternlab', 'create design systems with atomic design', function(arg) {
+
+ var patternlab = {};
+ patternlab.data = grunt.file.readJSON('./source/_data/data.json');
+ patternlab.listitems = grunt.file.readJSON('./source/_data/listitems.json');
+ patternlab.header = grunt.file.read('./source/_patternlab-files/pattern-header-footer/header.html');
+ patternlab.footer = grunt.file.read('./source/_patternlab-files/pattern-header-footer/footer.html');
+ patternlab.patterns = [];
+ patternlab.patternIndex = [];
+ patternlab.partials = {};
+ patternlab.buckets = [];
+ patternlab.bucketIndex = [];
+ patternlab.patternPaths = {};
+ patternlab.viewAllPaths = {};
+
+ grunt.file.recurse('./source/_patterns', function(abspath, rootdir, subdir, filename){
+ //check if the pattern already exists.
+ var patternName = filename.substring(0, filename.indexOf('.'));
+ var patternIndex = patternlab.patternIndex.indexOf(subdir + '-' + patternName);
+ var currentPattern;
+ var flatPatternPath;
+
+ //ignore _underscored patterns
+ if(filename.charAt(0) === '_'){
+ return;
+ }
+
+ //two reasons could return no pattern, 1) just a bare mustache, or 2) a json found before the mustache
+ //returns -1 if patterns does not exist, otherwise returns the index
+ //add the pattern array if first time, otherwise pull it up
+ if(patternIndex === -1){
+ grunt.log.ok('pattern not found, adding to array');
+ var flatPatternName = subdir.replace(/\//g, '-') + '-' + patternName;
+ flatPatternName = flatPatternName.replace(/\//g, '-');
+ currentPattern = new oPattern(flatPatternName, subdir, filename, {});
+ currentPattern.patternName = patternName.substring(patternName.indexOf('-') + 1);
+
+ if(grunt.util._.str.include(filename, 'json')){
+ grunt.log.writeln('json file found first, so add it to the pattern and continuing');
+ currentPattern.data = grunt.file.readJSON(abspath);
+ //done
+ } else{
+ grunt.log.writeln('mustache file found, assume no data, so compile it right away');
+ currentPattern.template = grunt.file.read(abspath);
+
+ //render the pattern. pass partials object just in case.
+ currentPattern.patternOutput = mustache.render(currentPattern.template, patternlab.data, patternlab.partials);
+
+ //write the compiled template to the public patterns directory
+ flatPatternPath = currentPattern.name + '/' + currentPattern.name + '.html';
+ grunt.file.write('./public/patterns/' + flatPatternPath, patternlab.header + currentPattern.patternOutput + patternlab.footer);
+ currentPattern.patternLink = flatPatternPath;
+
+ //add as a partial in case this is referenced later. convert to syntax needed by existing patterns
+ var sub = subdir.substring(subdir.indexOf('-') + 1);
+ var folderIndex = sub.indexOf('/'); //THIS IS MOST LIKELY WINDOWS ONLY. path.sep not working yet
+ var cleanSub = sub.substring(0, folderIndex);
+
+ //add any templates found to an object of partials, so downstream templates may use them too
+ //exclude the template patterns - we don't need them as partials because pages will just swap data
+ if(cleanSub !== ''){
+ var partialname = cleanSub + '-' + patternName.substring(patternName.indexOf('-') + 1);
+
+ patternlab.partials[partialname] = currentPattern.template;
+
+ //done
+ }
+ }
+ //add to patternlab arrays so we can look these up later. this could probably just be an object.
+ patternlab.patternIndex.push(currentPattern.name);
+ patternlab.patterns.push(currentPattern);
+ } else{
+ //if we get here, we can almost ensure we found the json first, so render the template and pass in the unique json
+ currentPattern = patternlab.patterns[patternIndex];
+ grunt.log.ok('pattern found, loaded');
+ //determine if this file is data or pattern
+ if(grunt.util._.str.include(filename, 'mustache')){
+
+ currentPattern.template = grunt.file.read(abspath);
+
+ //render the pattern. pass partials object just in case.
+ currentPattern.patternOutput = mustache.render(currentPattern.template, currentPattern.data, patternlab.partials);
+ grunt.log.writeln('template compiled with data!');
+
+ //write the compiled template to the public patterns directory
+ flatPatternPath = currentPattern.name + '/' + currentPattern.name + '.html';
+ grunt.file.write('./public/patterns/' + flatPatternPath, patternlab.header + currentPattern.patternOutput + patternlab.footer);
+ currentPattern.patternLink = flatPatternPath;
+
+ //done
+ } else{
+ grunt.log.error('json encountered!? there should only be one');
+ }
+ }
+
+ });
+
+ //build the styleguide
+ var styleguideTemplate = grunt.file.read('./source/_patternlab-files/styleguide.mustache');
+ var styleguideHtml = mustache.render(styleguideTemplate, {partials: patternlab.patterns});
+ grunt.file.write('./public/styleguide/html/styleguide.html', styleguideHtml);
+
+ //build the patternlab website
+ var patternlabSiteTemplate = grunt.file.read('./source/_patternlab-files/index.mustache');
+
+ //the patternlab site requires a lot of partials to be rendered.
+ //patternNav.
+ var patternNavTemplate = grunt.file.read('./source/_patternlab-files/partials/patternNav.mustache');
+
+ //loop through all patterns. deciding to do this separate from the recursion, even at a performance hit, to attempt to separate the tasks of styleguide creation versus site menu creation
+ for(var i = 0; i < patternlab.patterns.length; i++){
+ var pattern = patternlab.patterns[i];
+ var bucketName = pattern.name.replace(/\//g, '-').split('-')[1];
+
+ //check if the bucket already exists
+ var bucketIndex = patternlab.bucketIndex.indexOf(bucketName);
+ if(bucketIndex === -1){
+ //add the bucket
+ var bucket = new oBucket(bucketName);
+
+ //add paternPath
+ patternlab.patternPaths[bucketName] = {};
+
+ //get the navItem
+ var navItemName = pattern.subdir.split('-').pop();
+
+ //get the navSubItem
+ var navSubItemName = pattern.patternName.replace(/-/g, ' ');
+
+ grunt.log.writeln('new bucket found: ' + bucketName + " " + navItemName + " " + navSubItemName);
+
+ //assume the navItem does not exist.
+ var navItem = new oNavItem(navItemName);
+
+ //assume the navSubItem does not exist.
+ var navSubItem = new oNavSubItem(navSubItemName);
+ navSubItem.patternPath = pattern.patternLink;
+ navSubItem.patternPartial = bucketName + "-" + navSubItemName;
+
+ //TODO patternItems....
+
+ //add everything
+ navItem.navSubItems.push(navSubItem);
+ navItem.navSubItemsIndex.push(navSubItemName);
+ bucket.navItems.push(navItem);
+ bucket.navItemsIndex.push(navItemName);
+ patternlab.buckets.push(bucket);
+ patternlab.bucketIndex.push(bucketName);
+
+ //add to patternPaths
+ patternlab.patternPaths[bucketName][navSubItemName] = pattern.subdir + "/" + pattern.filename.substring(0, pattern.filename.indexOf('.'));
+
+ //done
+
+ } else{
+ //find the bucket
+ var bucket = patternlab.buckets[bucketIndex];
+
+ //get the navItem
+ var navItemName = pattern.subdir.split('-').pop();
+
+ //get the navSubItem
+ var navSubItemName = pattern.patternName.replace(/-/g, ' ');;
+
+ //check to see if navItem exists
+ var navItemIndex = bucket.navItemsIndex.indexOf(navItemName);
+ if(navItemIndex === -1){
+
+ var navItem = new oNavItem(navItemName);
+
+ //assume the navSubItem does not exist.
+ var navSubItem = new oNavSubItem(navSubItemName);
+ navSubItem.patternPath = pattern.patternLink;
+ navSubItem.patternPartial = bucketName + "-" + navSubItemName;
+
+ //add the navItem and navSubItem
+ navItem.navSubItems.push(navSubItem);
+ navItem.navSubItemsIndex.push(navSubItemName);
+ bucket.navItems.push(navItem);
+ bucket.navItemsIndex.push(navItemName);
+
+ } else{
+ var navItem = bucket.navItems[navItemIndex];
+
+ //check to see if the navSubItem exists
+ var navSubItemIndex = navItem.navSubItemsIndex.indexOf(navSubItemName);
+ if(navSubItemIndex === -1){
+
+ var navSubItem = new oNavSubItem(navSubItemName);
+ navSubItem.patternPath = pattern.patternLink;
+ navSubItem.patternPartial = bucketName + "-" + navSubItemName;
+
+ //add the navSubItem
+ navItem.navSubItems.push(navSubItem);
+ navItem.navSubItemsIndex.push(navSubItemName);
+
+ } else{
+
+ var navSubItem = navItem.navSubItems[navSubItemsIndex];
+
+ navSubItem.patternPath = pattern.patternLink;
+ navSubItem.patternPartial = bucketName + "-" + navSubItemName;
+ }
+
+ }
+
+ //add to patternPaths
+ patternlab.patternPaths[bucketName][navSubItemName] = pattern.subdir + "/" + pattern.filename.substring(0, pattern.filename.indexOf('.'));
+
+ //check to see if this bucket has a View All yet. If not, add it.
+ // var navItem = bucket.navItems[navItemIndex];
+ // if(navItem){
+ // var hasViewAll = navItem.navSubItemsIndex.indexOf('View All');
+ // if(hasViewAll === -1){
+ // console.log('add a view all pattern');
+
+ // var navSubItem = new oNavSubItem('View All');
+ // navSubItem.patternPath = pattern.flatPatternPath + '/index.html'; //this is likely wrong
+ // navSubItem.patternPartial = 'viewall-' + bucketName + '-' + pattern.patternSubGroup;
+
+ // //add the navSubItem
+ // console.log(navSubItem);
+ // navItem.navSubItems.push(navSubItem);
+ // navItem.navSubItemsIndex.push('View All');
+ // }
+ // }
+ }
+
+ //VIEW ALL LOGIC CAN LOOP THROUGH PATTERNS TOO
+ //only add if it's an atom, molecule, or organism
+ // if(pattern.patternGroup === 'atoms' || pattern.patternGroup === 'molecules' || pattern.patternGroup === 'organisms'){
+ // if(patternlab.viewAllPaths[pattern.patternGroup]){
+
+ // //add the pattern sub-group
+ // patternlab.viewAllPaths[pattern.patternGroup][pattern.patternSubGroup] = pattern.flatPatternPath;
+ // }
+ // else{
+ // //add the new group then the subgroup
+ // patternlab.viewAllPaths[pattern.patternGroup] = {};
+ // patternlab.viewAllPaths[pattern.patternGroup][pattern.patternSubGroup] = pattern.flatPatternPath;
+ // }
+ // }
+
+ };
+
+ var patternNavPartialHtml = mustache.render(patternNavTemplate, patternlab);
+
+ //ishControls
+ var ishControlsTemplate = grunt.file.read('./source/_patternlab-files/partials/ishControls.mustache');
+ var ishControlsPartialHtml = mustache.render(ishControlsTemplate);
+
+ //patternPaths
+ var patternPathsTemplate = grunt.file.read('./source/_patternlab-files/partials/patternPaths.mustache');
+ var patternPathsPartialHtml = mustache.render(patternPathsTemplate, {'patternPaths': JSON.stringify(patternlab.patternPaths)});
+
+ //viewAllPaths
+ var viewAllPathsTemplate = grunt.file.read('./source/_patternlab-files/partials/viewAllPaths.mustache');
+ var viewAllPathersPartialHtml = mustache.render(viewAllPathsTemplate, {'viewallpaths': JSON.stringify(patternlab.viewAllPaths)});
+
+ //websockets
+ var websocketsTemplate = grunt.file.read('./source/_patternlab-files/partials/websockets.mustache');
+ var config = grunt.file.readJSON('./config/config.json');
+ patternlab.contentsyncport = config.contentSyncPort;
+ patternlab.navsyncport = config.navSyncPort;
+
+ var websocketsPartialHtml = mustache.render(websocketsTemplate, patternlab);
+
+ //render the patternlab template, with all partials
+ var patternlabSiteHtml = mustache.render(patternlabSiteTemplate, {}, {
+ 'ishControls': ishControlsPartialHtml,
+ 'patternNav': patternNavPartialHtml,
+ 'patternPaths': patternPathsPartialHtml,
+ 'websockets': websocketsPartialHtml,
+ 'viewAllPaths': viewAllPathersPartialHtml
+ });
+ grunt.file.write('./public/index.html', patternlabSiteHtml);
+
+ //debug
+ var outputFilename = './patternlab.json';
+ grunt.file.write(outputFilename, JSON.stringify(patternlab, null, 3));
+
+ });
+};
\ No newline at end of file
diff --git a/config/config.json b/config/config.json
new file mode 100644
index 000000000..1f97027f4
--- /dev/null
+++ b/config/config.json
@@ -0,0 +1,10 @@
+ {
+ "patterns" : {
+ "source" : "/../../source/_patterns/",
+ "public" : "/../../public/patterns/"
+ },
+ "ignored-extensions" : ["scss", "DS_Store", "less"],
+ "ignored-directories" : ["scss"],
+ "contentSyncPort" : 8002,
+ "navSyncPort" : 8003
+ }
\ No newline at end of file
diff --git a/extras/apache/README b/extras/apache/README
new file mode 100644
index 000000000..4a58f3819
--- /dev/null
+++ b/extras/apache/README
@@ -0,0 +1,37 @@
+# How to Set-up Apache on Mac OS X
+
+This document reviews how to (hopefully) easily set-up Apache on Mac OS X to support Pattern Lab. You'll need to open Terminal. Note that PHP may flake out with the default Apache install. I can modify directions in the future to account for that.
+
+## 1. Modify hosts
+
+First, you'll want to modify your hosts file so that you can use a specific hostname for the site rather than just `127.0.0.1` or `localhost`. To do so do the following:
+
+1. In Terminal type `sudo vi /etc/hosts`
+2. When prompted, enter the password you use to log-in
+3. When the file loads type `i`
+4. Using the arrow keys get to the end of the last line
+5. Hit `return` and type `127.0.0.1 patternlab.localhost`
+6. Hit the `esc` key and type `:wq`
+
+Your hosts should now be saved.
+
+## 2. Modify Apache
+
+Second, you'll want to add an Apache `VirtualHost` so that Apache will know to listen for your application at the correct hostname.
+
+1. In Terminal type `sudo vi /etc/apache2/extra/httpd-vhosts.conf`
+2. When prompted, enter the password you use to log-in
+3. When the file loads type `i`
+4. Using the arrow keys get to the end of the last line
+5. Hit `return` twice
+6. Copy and paste the info from the `vhost.txt` file in this directory.
+7. Modify `DocumentRoot` path to match the location of the your install of Pattern Lab
+7. Hit the `esc` key and type `:wq`
+
+## 3. Restart Apache
+
+Last, you'll want to restart Apache so your changes take effect. Simply open System Preferences and go to the "Sharing" panel. Untick the "Web Sharing" checkbox and tick it again to restart Apache.
+
+## 4. Test By Visiting patternlab.localhost
+
+In a browser try to visit http://patternlab.localhost. You should get the Pattern Lab styleguide by default. If you get Google Search results just make sure you enter the http://
\ No newline at end of file
diff --git a/extras/apache/vhost.txt b/extras/apache/vhost.txt
new file mode 100644
index 000000000..e4431fc02
--- /dev/null
+++ b/extras/apache/vhost.txt
@@ -0,0 +1,5 @@
+
+ DocumentRoot "/Users/dmolsen/Sites/patternlab/public"
+ ServerName patternlab.localhost
+ ServerAlias patternlab.*.xip.io
+
\ No newline at end of file
diff --git a/listeners/contentSyncBroadcasterServer.php b/listeners/contentSyncBroadcasterServer.php
new file mode 100644
index 000000000..df6081108
--- /dev/null
+++ b/listeners/contentSyncBroadcasterServer.php
@@ -0,0 +1,43 @@
+register();
+
+// parse the main config for the content sync port
+if (!($config = @parse_ini_file(__DIR__."/../config/config.ini"))) {
+ print "Missing the configuration file. Please build it using the Pattern Lab builder.\n";
+ exit;
+}
+
+$port = ($config) ? trim($config['contentSyncPort']) : '8002';
+
+// start the content sync server
+$server = new \Wrench\Server('ws://0.0.0.0:'.$port.'/', array());
+
+// register the application
+$server->registerApplication('contentsync', new \Wrench\Application\contentSyncBroadcasterApplication());
+
+print "\n";
+print "Auto-reload Server Started...\n";
+print "Use CTRL+C to stop this service...\n";
+
+// run it
+$server->run();
diff --git a/listeners/lib/LICENSE b/listeners/lib/LICENSE
new file mode 100644
index 000000000..5c93f4565
--- /dev/null
+++ b/listeners/lib/LICENSE
@@ -0,0 +1,13 @@
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar
+
+ Everyone is permitted to copy and distribute verbatim or modified
+ copies of this license document, and changing it is allowed as long
+ as the name is changed.
+
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
diff --git a/listeners/lib/SplClassLoader.php b/listeners/lib/SplClassLoader.php
new file mode 100644
index 000000000..2bc31f04b
--- /dev/null
+++ b/listeners/lib/SplClassLoader.php
@@ -0,0 +1,136 @@
+register();
+ *
+ * @author Jonathan H. Wage
+ * @author Roman S. Borschel
+ * @author Matthew Weier O'Phinney
+ * @author Kris Wallsmith
+ * @author Fabien Potencier
+ */
+class SplClassLoader
+{
+ private $_fileExtension = '.php';
+ private $_namespace;
+ private $_includePath;
+ private $_namespaceSeparator = '\\';
+
+ /**
+ * Creates a new SplClassLoader that loads classes of the
+ * specified namespace.
+ *
+ * @param string $ns The namespace to use.
+ */
+ public function __construct($ns = null, $includePath = null)
+ {
+ $this->_namespace = $ns;
+ $this->_includePath = $includePath;
+ }
+
+ /**
+ * Sets the namespace separator used by classes in the namespace of this class loader.
+ *
+ * @param string $sep The separator to use.
+ */
+ public function setNamespaceSeparator($sep)
+ {
+ $this->_namespaceSeparator = $sep;
+ }
+
+ /**
+ * Gets the namespace seperator used by classes in the namespace of this class loader.
+ *
+ * @return void
+ */
+ public function getNamespaceSeparator()
+ {
+ return $this->_namespaceSeparator;
+ }
+
+ /**
+ * Sets the base include path for all class files in the namespace of this class loader.
+ *
+ * @param string $includePath
+ */
+ public function setIncludePath($includePath)
+ {
+ $this->_includePath = $includePath;
+ }
+
+ /**
+ * Gets the base include path for all class files in the namespace of this class loader.
+ *
+ * @return string $includePath
+ */
+ public function getIncludePath()
+ {
+ return $this->_includePath;
+ }
+
+ /**
+ * Sets the file extension of class files in the namespace of this class loader.
+ *
+ * @param string $fileExtension
+ */
+ public function setFileExtension($fileExtension)
+ {
+ $this->_fileExtension = $fileExtension;
+ }
+
+ /**
+ * Gets the file extension of class files in the namespace of this class loader.
+ *
+ * @return string $fileExtension
+ */
+ public function getFileExtension()
+ {
+ return $this->_fileExtension;
+ }
+
+ /**
+ * Installs this class loader on the SPL autoload stack.
+ */
+ public function register()
+ {
+ spl_autoload_register(array($this, 'loadClass'));
+ }
+
+ /**
+ * Uninstalls this class loader from the SPL autoloader stack.
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $className The name of the class to load.
+ * @return void
+ */
+ public function loadClass($className)
+ {
+ if (null === $this->_namespace || $this->_namespace.$this->_namespaceSeparator === substr($className, 0, strlen($this->_namespace.$this->_namespaceSeparator))) {
+ $fileName = '';
+ $namespace = '';
+ if (false !== ($lastNsPos = strripos($className, $this->_namespaceSeparator))) {
+ $namespace = substr($className, 0, $lastNsPos);
+ $className = substr($className, $lastNsPos + 1);
+ $fileName = str_replace($this->_namespaceSeparator, DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
+ }
+ $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . $this->_fileExtension;
+
+ require ($this->_includePath !== null ? $this->_includePath . DIRECTORY_SEPARATOR : '') . $fileName;
+ }
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Application/Application.php b/listeners/lib/Wrench/Application/Application.php
new file mode 100644
index 000000000..2132a8907
--- /dev/null
+++ b/listeners/lib/Wrench/Application/Application.php
@@ -0,0 +1,34 @@
+savedTimestamp = file_get_contents(__DIR__."/../../../../public/latest-change.txt");
+ } else {
+ $this->savedTimestamp = time();
+ }
+
+ }
+
+ /**
+ * When a client connects add it to the list of connected clients
+ */
+ public function onConnect($client) {
+ $id = $client->getId();
+ $this->clients[$id] = $client;
+ }
+
+ /**
+ * When a client disconnects remove it from the list of connected clients
+ */
+ public function onDisconnect($client) {
+ $id = $client->getId();
+ unset($this->clients[$id]);
+ }
+
+ /**
+ * Dead function in this instance
+ */
+ public function onData($data, $client) {
+ // function not in use
+ }
+
+ /**
+ * Sends out a message once a second to all connected clients containing the contents of latest-change.txt
+ */
+ public function onUpdate() {
+
+ if (file_exists(__DIR__."/../../../../public/latest-change.txt")) {
+ $readTimestamp = file_get_contents(__DIR__."/../../../../public/latest-change.txt");
+ if ($readTimestamp != $this->savedTimestamp) {
+ print "pattern lab updated. alerting connected browsers...\n";
+ foreach ($this->clients as $sendto) {
+ $sendto->send($readTimestamp);
+ }
+ $this->savedTimestamp = $readTimestamp;
+ }
+ }
+
+ }
+
+}
diff --git a/listeners/lib/Wrench/Application/navSyncBroadcasterApplication.php b/listeners/lib/Wrench/Application/navSyncBroadcasterApplication.php
new file mode 100644
index 000000000..900343120
--- /dev/null
+++ b/listeners/lib/Wrench/Application/navSyncBroadcasterApplication.php
@@ -0,0 +1,71 @@
+getId();
+ $this->clients[$id] = $client;
+ if ($this->data != null) {
+ $client->send(json_encode($this->data));
+ }
+ }
+
+ /**
+ * When a client disconnects remove it from the list of connected clients
+ */
+ public function onDisconnect($client) {
+ $id = $client->getId();
+ unset($this->clients[$id]);
+ }
+
+ /**
+ * When receiving a message from a client, strip it to avoid cross-domain issues and send it to all clients except the one who sent it
+ * Also store the address as the current address for any new clients that attach
+ */
+ public function onData($data, $client) {
+
+ $dataDecoded = json_decode($data);
+
+ $dataDecoded->url = "/".$dataDecoded->url;
+ $dataEncoded = json_encode($dataDecoded);
+ $testId = $client->getId();
+ foreach ($this->clients as $sendto) {
+ if ($testId != $sendto->getId()) {
+ $sendto->send($dataEncoded);
+ }
+ }
+
+ $this->data = $dataDecoded;
+
+ }
+
+ /**
+ * Dead function in this instance
+ */
+ public function onUpdate() {
+ // not using for this application
+ }
+
+}
diff --git a/listeners/lib/Wrench/BasicServer.php b/listeners/lib/Wrench/BasicServer.php
new file mode 100644
index 000000000..88578a055
--- /dev/null
+++ b/listeners/lib/Wrench/BasicServer.php
@@ -0,0 +1,70 @@
+configureRateLimiter();
+ $this->configureOriginPolicy();
+ }
+
+ /**
+ * @see Wrench.Server::configure()
+ */
+ protected function configure(array $options)
+ {
+ $options = array_merge(array(
+ 'check_origin' => true,
+ 'allowed_origins' => array(),
+ 'origin_policy_class' => 'Wrench\Listener\OriginPolicy',
+ 'rate_limiter_class' => 'Wrench\Listener\RateLimiter'
+ ), $options);
+
+ parent::configure($options);
+ }
+
+ protected function configureRateLimiter()
+ {
+ $class = $this->options['rate_limiter_class'];
+ $this->rateLimiter = new $class();
+ $this->rateLimiter->listen($this);
+ }
+
+ /**
+ * Configures the origin policy
+ */
+ protected function configureOriginPolicy()
+ {
+ $class = $this->options['origin_policy_class'];
+ $this->originPolicy = new $class($this->options['allowed_origins']);
+
+ if ($this->options['check_origin']) {
+ $this->originPolicy->listen($this);
+ }
+ }
+
+ /**
+ * Adds an allowed origin
+ *
+ * @param array $origin
+ */
+ public function addAllowedOrigin($origin)
+ {
+ $this->originPolicy->addAllowedOrigin($origin);
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Client.php b/listeners/lib/Wrench/Client.php
new file mode 100644
index 000000000..87e8f11ba
--- /dev/null
+++ b/listeners/lib/Wrench/Client.php
@@ -0,0 +1,265 @@
+
+ */
+ protected $received = array();
+
+ /**
+ * Constructor
+ *
+ * @param string $uri
+ * @param string $origin The origin to include in the handshake (required
+ * in later versions of the protocol)
+ * @param array $options (optional) Array of options
+ * - socket => Socket instance (otherwise created)
+ * - protocol => Protocol
+ */
+ public function __construct($uri, $origin, array $options = array())
+ {
+ parent::__construct($options);
+
+ $uri = (string)$uri;
+ if (!$uri) {
+ throw new InvalidArgumentException('No URI specified');
+ }
+ $this->uri = $uri;
+
+ $origin = (string)$origin;
+ if (!$origin) {
+ throw new InvalidArgumentException('No origin specified');
+ }
+ $this->origin = $origin;
+
+ $this->protocol->validateUri($this->uri);
+ $this->protocol->validateOriginUri($this->origin);
+
+ $this->configureSocket();
+ $this->configurePayloadHandler();
+ }
+
+ /**
+ * Configure options
+ *
+ * @param array $options
+ * @return void
+ */
+ protected function configure(array $options)
+ {
+ $options = array_merge(array(
+ 'socket_class' => 'Wrench\\Socket\\ClientSocket',
+ 'on_data_callback' => null
+ ), $options);
+
+ parent::configure($options);
+ }
+
+ /**
+ * Configures the client socket
+ */
+ protected function configureSocket()
+ {
+ $class = $this->options['socket_class'];
+ $this->socket = new $class($this->uri);
+ }
+
+ /**
+ * Configures the payload handler
+ */
+ protected function configurePayloadHandler()
+ {
+ $this->payloadHandler = new PayloadHandler(array($this, 'onData'), $this->options);
+ }
+
+ /**
+ * Payload receiver
+ *
+ * Public because called from our PayloadHandler. Don't call us, we'll call
+ * you (via the on_data_callback option).
+ *
+ * @param Payload $payload
+ */
+ public function onData(Payload $payload)
+ {
+ $this->received[] = $payload;
+ if (($callback = $this->options['on_data_callback'])) {
+ call_user_func($callback, $payload);
+ }
+ }
+
+ /**
+ * Adds a request header to be included in the initial handshake
+ *
+ * For example, to include a Cookie header
+ *
+ * @param string $name
+ * @param string $value
+ * @return void
+ */
+ public function addRequestHeader($name, $value)
+ {
+ $this->headers[$name] = $value;
+ }
+
+ /**
+ * Sends data to the socket
+ *
+ * @param string $data
+ * @param string $type Payload type
+ * @param boolean $masked
+ * @return boolean Success
+ */
+ public function sendData($data, $type = Protocol::TYPE_TEXT, $masked = true)
+ {
+ if (is_string($type) && isset(Protocol::$frameTypes[$type])) {
+ $type = Protocol::$frameTypes[$type];
+ }
+
+ $payload = $this->protocol->getPayload();
+
+ $payload->encode(
+ $data,
+ $type,
+ $masked
+ );
+
+ return $payload->sendToSocket($this->socket);
+ }
+
+ /**
+ * Receives data sent by the server
+ *
+ * @param callable $callback
+ * @return array Payload received since the last call to receive()
+ */
+ public function receive()
+ {
+ if (!$this->isConnected()) {
+ return false;
+ }
+
+ $data = $this->socket->receive();
+
+ if (!$data) {
+ return $data;
+ }
+
+ $old = $this->received;
+ $this->payloadHandler->handle($data);
+ return array_diff($this->received, $old);
+ }
+
+ /**
+ * Connect to the Wrench server
+ *
+ * @return boolean Whether a new connection was made
+ */
+ public function connect()
+ {
+ if ($this->isConnected()) {
+ return false;
+ }
+
+ $this->socket->connect();
+
+ $key = $this->protocol->generateKey();
+ $handshake = $this->protocol->getRequestHandshake(
+ $this->uri,
+ $key,
+ $this->origin,
+ $this->headers
+ );
+
+ $this->socket->send($handshake);
+ $response = $this->socket->receive(self::MAX_HANDSHAKE_RESPONSE);
+ return ($this->connected =
+ $this->protocol->validateResponseHandshake($response, $key));
+ }
+
+ /**
+ * Whether the client is currently connected
+ *
+ * @return boolean
+ */
+ public function isConnected()
+ {
+ return $this->connected;
+ }
+
+ /**
+ * @todo Bug: what if connect has been called twice. The first socket never
+ * gets closed.
+ */
+ public function disconnect()
+ {
+ if ($this->socket) {
+ $this->socket->disconnect();
+ }
+ $this->connected = false;
+ }
+
+
+}
diff --git a/listeners/lib/Wrench/Connection.php b/listeners/lib/Wrench/Connection.php
new file mode 100644
index 000000000..2538fc739
--- /dev/null
+++ b/listeners/lib/Wrench/Connection.php
@@ -0,0 +1,538 @@
+ 'value'
+ *
+ * @var array
+ */
+ protected $queryParams = null;
+
+ /**
+ * Connection ID
+ *
+ * @var string|null
+ */
+ protected $id = null;
+
+ /**
+ * @var PayloadHandler
+ */
+ protected $payloadHandler;
+
+ /**
+ * Constructor
+ *
+ * @param Server $server
+ * @param ServerClientSocket $socket
+ * @param array $options
+ * @throws InvalidArgumentException
+ */
+ public function __construct(
+ ConnectionManager $manager,
+ ServerClientSocket $socket,
+ array $options = array()
+ ) {
+ $this->manager = $manager;
+ $this->socket = $socket;
+
+
+ parent::__construct($options);
+
+ $this->configureClientInformation();
+ $this->configurePayloadHandler();
+
+ $this->log('Connected');
+ }
+
+ /**
+ * Gets the connection manager of this connection
+ *
+ * @return \Wrench\ConnectionManager
+ */
+ public function getConnectionManager()
+ {
+ return $this->manager;
+ }
+
+ /**
+ * @see Wrench\Util.Configurable::configure()
+ */
+ protected function configure(array $options)
+ {
+ $options = array_merge(array(
+ 'connection_id_secret' => 'asu5gj656h64Da(0crt8pud%^WAYWW$u76dwb',
+ 'connection_id_algo' => 'sha512',
+ ), $options);
+
+ parent::configure($options);
+ }
+
+ protected function configurePayloadHandler()
+ {
+ $this->payloadHandler = new PayloadHandler(
+ array($this, 'handlePayload'),
+ $this->options
+ );
+ }
+
+ /**
+ * @throws RuntimeException
+ */
+ protected function configureClientInformation()
+ {
+ $this->ip = $this->socket->getIp();
+ $this->port = $this->socket->getPort();
+ $this->configureClientId();
+ }
+
+ /**
+ * Configures the client ID
+ *
+ * We hash the client ID to prevent leakage of information if another client
+ * happens to get a hold of an ID. The secret *must* be lengthy, and must
+ * be kept secret for this to work: otherwise it's trivial to search the space
+ * of possible IP addresses/ports (well, if not trivial, at least very fast).
+ */
+ protected function configureClientId()
+ {
+ $message = sprintf(
+ '%s:uri=%s&ip=%s&port=%s',
+ $this->options['connection_id_secret'],
+ rawurlencode($this->manager->getUri()),
+ rawurlencode($this->ip),
+ rawurlencode($this->port)
+ );
+
+ $algo = $this->options['connection_id_algo'];
+
+ if (extension_loaded('gmp')) {
+ $hash = hash($algo, $message);
+ $hash = gmp_strval(gmp_init('0x' . $hash, 16), 62);
+ } else {
+ // @codeCoverageIgnoreStart
+ $hash = hash($algo, $message);
+ // @codeCoverageIgnoreEnd
+ }
+
+ $this->id = $hash;
+ }
+
+ /**
+ * Data receiver
+ *
+ * Called by the connection manager when the connection has received data
+ *
+ * @param string $data
+ */
+ public function onData($data)
+ {
+ if (!$this->handshaked) {
+ return $this->handshake($data);
+ }
+ return $this->handle($data);
+ }
+
+ /**
+ * Performs a websocket handshake
+ *
+ * @param string $data
+ * @throws BadRequestException
+ * @throws HandshakeException
+ * @throws WrenchException
+ */
+ public function handshake($data)
+ {
+ try {
+ list($path, $origin, $key, $extensions, $protocol, $headers, $params)
+ = $this->protocol->validateRequestHandshake($data);
+
+ $this->headers = $headers;
+ $this->queryParams = $params;
+
+ $this->application = $this->manager->getApplicationForPath($path);
+ if (!$this->application) {
+ throw new BadRequestException('Invalid application');
+ }
+
+ $this->manager->getServer()->notify(
+ Server::EVENT_HANDSHAKE_REQUEST,
+ array($this, $path, $origin, $key, $extensions)
+ );
+
+ $response = $this->protocol->getResponseHandshake($key);
+
+ if (!$this->socket->isConnected()) {
+ throw new HandshakeException('Socket is not connected');
+ }
+
+ if ($this->socket->send($response) === false) {
+ throw new HandshakeException('Could not send handshake response');
+ }
+
+ $this->handshaked = true;
+
+ $this->log(sprintf(
+ 'Handshake successful: %s:%d (%s) connected to %s',
+ $this->getIp(),
+ $this->getPort(),
+ $this->getId(),
+ $path
+ ), 'info');
+
+ $this->manager->getServer()->notify(
+ Server::EVENT_HANDSHAKE_SUCCESSFUL,
+ array($this)
+ );
+
+ if (method_exists($this->application, 'onConnect')) {
+ $this->application->onConnect($this);
+ }
+ } catch (WrenchException $e) {
+ $this->log('Handshake failed: ' . $e, 'err');
+ $this->close($e);
+ }
+ }
+
+ /**
+ * Returns a string export of the given binary data
+ *
+ * @param string $data
+ * @return string
+ */
+ protected function export($data)
+ {
+ $export = '';
+ foreach (str_split($data) as $chr) {
+ $export .= '\\x' . ord($chr);
+ }
+ }
+
+ /**
+ * Handle data received from the client
+ *
+ * The data passed in may belong to several different frames across one or
+ * more protocols. It may not even contain a single complete frame. This method
+ * manages slotting the data into separate payload objects.
+ *
+ * @todo An endpoint MUST be capable of handling control frames in the
+ * middle of a fragmented message.
+ * @param string $data
+ * @return void
+ */
+ public function handle($data)
+ {
+ $this->payloadHandler->handle($data);
+ }
+
+ /**
+ * Handle a complete payload received from the client
+ *
+ * Public because called from our PayloadHandler
+ *
+ * @param string $payload
+ */
+ public function handlePayload(Payload $payload)
+ {
+ $app = $this->getClientApplication();
+
+ $this->log('Handling payload: ' . $payload->getPayload(), 'debug');
+
+ switch ($type = $payload->getType()) {
+ case Protocol::TYPE_TEXT:
+ if (method_exists($app, 'onData')) {
+ $app->onData($payload, $this);
+ }
+ return;
+
+ case Protocol::TYPE_BINARY:
+ if(method_exists($app, 'onBinaryData')) {
+ $app->onBinaryData($payload, $this);
+ } else {
+ $this->close(1003);
+ }
+ break;
+
+ case Protocol::TYPE_PING:
+ $this->log('Ping received', 'notice');
+ $this->send($payload->getPayload(), Protocol::TYPE_PONG);
+ $this->log('Pong!', 'debug');
+ break;
+
+ /**
+ * A Pong frame MAY be sent unsolicited. This serves as a
+ * unidirectional heartbeat. A response to an unsolicited Pong
+ * frame is not expected.
+ */
+ case Protocol::TYPE_PONG:
+ $this->log('Received unsolicited pong', 'info');
+ break;
+
+ case Protocol::TYPE_CLOSE:
+ $this->log('Close frame received', 'notice');
+ $this->close();
+ $this->log('Disconnected', 'info');
+ break;
+
+ default:
+ throw new ConnectionException('Unhandled payload type');
+ }
+ }
+
+ /**
+ * Sends the payload to the connection
+ *
+ * @param string $payload
+ * @param string $type
+ * @throws HandshakeException
+ * @throws ConnectionException
+ * @return boolean
+ */
+ public function send($data, $type = Protocol::TYPE_TEXT)
+ {
+ if (!$this->handshaked) {
+ throw new HandshakeException('Connection is not handshaked');
+ }
+
+ $payload = $this->protocol->getPayload();
+
+ // Servers don't send masked payloads
+ $payload->encode($data, $type, false);
+
+ if (!$payload->sendToSocket($this->socket)) {
+ $this->log('Could not send payload to client', 'warn');
+ throw new ConnectionException('Could not send data to connection: ' . $this->socket->getLastError());
+ }
+
+ return true;
+ }
+
+ /**
+ * Processes data on the socket
+ *
+ * @throws CloseException
+ */
+ public function process()
+ {
+ $data = $this->socket->receive();
+ $bytes = strlen($data);
+
+ if ($bytes === 0 || $data === false) {
+ throw new CloseException('Error reading data from socket: ' . $this->socket->getLastError());
+ }
+
+ $this->onData($data);
+ }
+
+ /**
+ * Closes the connection according to the WebSocket protocol
+ *
+ * If an endpoint receives a Close frame and that endpoint did not
+ * previously send a Close frame, the endpoint MUST send a Close frame
+ * in response. It SHOULD do so as soon as is practical. An endpoint
+ * MAY delay sending a close frame until its current message is sent
+ * (for instance, if the majority of a fragmented message is already
+ * sent, an endpoint MAY send the remaining fragments before sending a
+ * Close frame). However, there is no guarantee that the endpoint which
+ * has already sent a Close frame will continue to process data.
+
+ * After both sending and receiving a close message, an endpoint
+ * considers the WebSocket connection closed, and MUST close the
+ * underlying TCP connection. The server MUST close the underlying TCP
+ * connection immediately; the client SHOULD wait for the server to
+ * close the connection but MAY close the connection at any time after
+ * sending and receiving a close message, e.g. if it has not received a
+ * TCP close from the server in a reasonable time period.
+ *
+ * @param int|Exception $statusCode
+ * @return boolean
+ */
+ public function close($code = Protocol::CLOSE_NORMAL)
+ {
+ try {
+ if (!$this->handshaked) {
+ $response = $this->protocol->getResponseError($code);
+ $this->socket->send($response);
+ } else {
+ $response = $this->protocol->getCloseFrame($code);
+ $this->socket->send($response);
+ }
+ } catch (Exception $e) {
+ $this->log('Unable to send close message', 'warning');
+ }
+
+ if ($this->application && method_exists($this->application, 'onDisconnect')) {
+ $this->application->onDisconnect($this);
+ }
+
+ $this->socket->disconnect();
+ $this->manager->removeConnection($this);
+ }
+
+ /**
+ * Logs a message
+ *
+ * @param string $message
+ * @param string $priority
+ */
+ public function log($message, $priority = 'info')
+ {
+ $this->manager->log(sprintf(
+ '%s: %s:%d (%s): %s',
+ __CLASS__,
+ $this->getIp(),
+ $this->getPort(),
+ $this->getId(),
+ $message
+ ), $priority);
+ }
+
+ /**
+ * Gets the IP address of the connection
+ *
+ * @return string Usually dotted quad notation
+ */
+ public function getIp()
+ {
+ return $this->ip;
+ }
+
+ /**
+ * Gets the port of the connection
+ *
+ * @return int
+ */
+ public function getPort()
+ {
+ return $this->port;
+ }
+
+ /**
+ * Gets the non-web-sockets headers included with the original request
+ *
+ * @return array
+ */
+ public function getHeaders()
+ {
+ return $this->headers;
+ }
+
+ /**
+ * Gets the query parameters included with the original request
+ *
+ * @return array
+ */
+ public function getQueryParams()
+ {
+ return $this->queryParams;
+ }
+
+ /**
+ * Gets the connection ID
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Gets the socket object
+ *
+ * @return Socket\ServerClientSocket
+ */
+ public function getSocket()
+ {
+ return $this->socket;
+ }
+
+ /**
+ * Gets the client application
+ *
+ * @return Application
+ */
+ public function getClientApplication()
+ {
+ return (isset($this->application)) ? $this->application : false;
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/ConnectionManager.php b/listeners/lib/Wrench/ConnectionManager.php
new file mode 100644
index 000000000..8a503de03
--- /dev/null
+++ b/listeners/lib/Wrench/ConnectionManager.php
@@ -0,0 +1,332 @@
+ Connection>
+ */
+ protected $connections = array();
+
+ /**
+ * An array of raw socket resources, corresponding to connections, roughly
+ *
+ * @var array resource>
+ */
+ protected $resources = array();
+
+ /**
+ * Constructor
+ *
+ * @param Server $server
+ * @param array $options
+ */
+ public function __construct(Server $server, array $options = array())
+ {
+ $this->server = $server;
+
+ parent::__construct($options);
+ }
+
+ /**
+ * @see Countable::count()
+ */
+ public function count()
+ {
+ return count($this->connections);
+ }
+
+ /**
+ * @see Wrench\Socket.Socket::configure()
+ * Options include:
+ * - timeout_select => int, seconds, default 0
+ * - timeout_select_microsec => int, microseconds (NB: not milli), default: 200000
+ */
+ protected function configure(array $options)
+ {
+ $options = array_merge(array(
+ 'socket_master_class' => 'Wrench\Socket\ServerSocket',
+ 'socket_master_options' => array(),
+ 'socket_client_class' => 'Wrench\Socket\ServerClientSocket',
+ 'socket_client_options' => array(),
+ 'connection_class' => 'Wrench\Connection',
+ 'connection_options' => array(),
+ 'timeout_select' => self::TIMEOUT_SELECT,
+ 'timeout_select_microsec' => self::TIMEOUT_SELECT_MICROSEC
+ ), $options);
+
+ parent::configure($options);
+
+ $this->configureMasterSocket();
+ }
+
+ /**
+ * Gets the application associated with the given path
+ *
+ * @param string $path
+ */
+ public function getApplicationForPath($path)
+ {
+ $path = ltrim($path, '/');
+ return $this->server->getApplication($path);
+ }
+
+ /**
+ * Configures the main server socket
+ *
+ * @param string $uri
+ */
+ protected function configureMasterSocket()
+ {
+ $class = $this->options['socket_master_class'];
+ $options = $this->options['socket_master_options'];
+ $this->socket = new $class($this->server->getUri(), $options);
+ }
+
+ /**
+ * Listens on the main socket
+ *
+ * @return void
+ */
+ public function listen()
+ {
+ $this->socket->listen();
+ $this->resources[$this->socket->getResourceId()] = $this->socket->getResource();
+ }
+
+ /**
+ * Gets all resources
+ *
+ * @return array resource)
+ */
+ protected function getAllResources()
+ {
+ return array_merge($this->resources, array(
+ $this->socket->getResourceId() => $this->socket->getResource()
+ ));
+ }
+
+ /**
+ * Returns the Connection associated with the specified socket resource
+ *
+ * @param resource $socket
+ * @return Connection
+ */
+ protected function getConnectionForClientSocket($socket)
+ {
+ if (!isset($this->connections[$this->resourceId($socket)])) {
+ return false;
+ }
+ return $this->connections[$this->resourceId($socket)];
+ }
+
+ /**
+ * Select and process an array of resources
+ *
+ * @param array $resources
+ */
+ public function selectAndProcess()
+ {
+ $read = $this->resources;
+ $unused_write = null;
+ $unsued_exception = null;
+
+ stream_select(
+ $read,
+ $unused_write,
+ $unused_exception,
+ $this->options['timeout_select'],
+ $this->options['timeout_select_microsec']
+ );
+
+ foreach ($read as $socket) {
+ if ($socket == $this->socket->getResource()) {
+ $this->processMasterSocket();
+ } else {
+ $this->processClientSocket($socket);
+ }
+ }
+ }
+
+ /**
+ * Process events on the master socket ($this->socket)
+ *
+ * @return void
+ */
+ protected function processMasterSocket()
+ {
+ $new = null;
+
+ try {
+ $new = $this->socket->accept();
+ } catch (Exception $e) {
+ $this->server->log('Socket error: ' . $e, 'err');
+ return;
+ }
+
+ $connection = $this->createConnection($new);
+ $this->server->notify(Server::EVENT_SOCKET_CONNECT, array($new, $connection));
+ }
+
+ /**
+ * Creates a connection from a socket resource
+ *
+ * The create connection object is based on the options passed into the
+ * constructor ('connection_class', 'connection_options'). This connection
+ * instance and its associated socket resource are then stored in the
+ * manager.
+ *
+ * @param resource $resource A socket resource
+ * @return Connection
+ */
+ protected function createConnection($resource)
+ {
+ if (!$resource || !is_resource($resource)) {
+ throw new InvalidArgumentException('Invalid connection resource');
+ }
+
+ $socket_class = $this->options['socket_client_class'];
+ $socket_options = $this->options['socket_client_options'];
+
+ $connection_class = $this->options['connection_class'];
+ $connection_options = $this->options['connection_options'];
+
+ $socket = new $socket_class($resource, $socket_options);
+ $connection = new $connection_class($this, $socket, $connection_options);
+
+ $id = $this->resourceId($resource);
+ $this->resources[$id] = $resource;
+ $this->connections[$id] = $connection;
+
+ return $connection;
+ }
+
+ /**
+ * Process events on a client socket
+ *
+ * @param resource $socket
+ */
+ protected function processClientSocket($socket)
+ {
+ $connection = $this->getConnectionForClientSocket($socket);
+
+ if (!$connection) {
+ $this->log('No connection for client socket', 'warning');
+ return;
+ }
+
+ try {
+ $connection->process();
+ } catch (CloseException $e) {
+ $this->log('Client connection closed: ' . $e, 'notice');
+ $connection->close($e);
+ } catch (WrenchException $e) {
+ $this->log('Error on client socket: ' . $e, 'warning');
+ $connection->close($e);
+ }
+ }
+
+ /**
+ * This server makes an explicit assumption: PHP resource types may be cast
+ * to a integer. Furthermore, we assume this is bijective. Both seem to be
+ * true in most circumstances, but may not be guaranteed.
+ *
+ * This method (and $this->getResourceId()) exist to make this assumption
+ * explicit.
+ *
+ * This is needed on the connection manager as well as on resources
+ *
+ * @param resource $resource
+ */
+ protected function resourceId($resource)
+ {
+ return (int)$resource;
+ }
+
+ /**
+ * Gets the connection manager's listening URI
+ *
+ * @return string
+ */
+ public function getUri()
+ {
+ return $this->server->getUri();
+ }
+
+ /**
+ * Logs a message
+ *
+ * @param string $message
+ * @param string $priority
+ */
+ public function log($message, $priority = 'info')
+ {
+ $this->server->log(sprintf(
+ '%s: %s',
+ __CLASS__,
+ $message
+ ), $priority);
+ }
+
+ /**
+ * @return \Wrench\Server
+ */
+ public function getServer()
+ {
+ return $this->server;
+ }
+
+ /**
+ * Removes a connection
+ *
+ * @param Connection $connection
+ */
+ public function removeConnection(Connection $connection)
+ {
+ $socket = $connection->getSocket();
+
+ if ($socket->getResource()) {
+ $index = $socket->getResourceId();
+ } else {
+ $index = array_search($connection, $this->connections);
+ }
+
+ if (!$index) {
+ $this->log('Could not remove connection: not found', 'warning');
+ }
+
+ unset($this->connections[$index]);
+ unset($this->resources[$index]);
+
+ $this->server->notify(
+ Server::EVENT_SOCKET_DISCONNECT,
+ array($connection->getSocket(), $connection)
+ );
+ }
+}
diff --git a/listeners/lib/Wrench/Exception/BadRequestException.php b/listeners/lib/Wrench/Exception/BadRequestException.php
new file mode 100644
index 000000000..8b8a1591f
--- /dev/null
+++ b/listeners/lib/Wrench/Exception/BadRequestException.php
@@ -0,0 +1,22 @@
+buffer) {
+ return false;
+ }
+
+ try {
+ return $this->getBufferLength() >= $this->getExpectedBufferLength();
+ } catch (FrameException $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Receieves data into the frame
+ *
+ * @param string $buffer
+ */
+ public function receiveData($data)
+ {
+ $this->buffer .= $data;
+ }
+
+ /**
+ * Gets the remaining number of bytes before this frame will be complete
+ *
+ * @return number
+ */
+ public function getRemainingData()
+ {
+ try {
+ return $this->getExpectedBufferLength() - $this->getBufferLength();
+ } catch (FrameException $e) {
+ return null;
+ }
+ }
+
+ /**
+ * Whether this frame is waiting for more data
+ *
+ * @return boolean
+ */
+ public function isWaitingForData()
+ {
+ return $this->getRemainingData() > 0;
+ }
+
+ /**
+ * Gets the contents of the frame payload
+ *
+ * The frame must be complete to call this method.
+ *
+ * @return string
+ */
+ public function getFramePayload()
+ {
+ if (!$this->isComplete()) {
+ throw new FrameException('Cannot get payload: frame is not complete');
+ }
+
+ if (!$this->payload && $this->buffer) {
+ $this->decodeFramePayloadFromBuffer();
+ }
+
+ return $this->payload;
+ }
+
+ /**
+ * Gets the contents of the frame buffer
+ *
+ * This is the encoded value, receieved into the frame with receiveData().
+ *
+ * @throws FrameException
+ * @return string binary
+ */
+ public function getFrameBuffer()
+ {
+ if (!$this->buffer && $this->payload) {
+ throw new FrameException('Cannot get frame buffer');
+ }
+ return $this->buffer;
+ }
+
+ /**
+ * Gets the expected length of the frame payload
+ *
+ * @return int
+ */
+ protected function getBufferLength()
+ {
+ return strlen($this->buffer);
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Frame/HybiFrame.php b/listeners/lib/Wrench/Frame/HybiFrame.php
new file mode 100644
index 000000000..da6377d7e
--- /dev/null
+++ b/listeners/lib/Wrench/Frame/HybiFrame.php
@@ -0,0 +1,376 @@
+= 0
+ */
+ public function encode($payload, $type = Protocol::TYPE_TEXT, $masked = false)
+ {
+ if (!is_int($type) || !in_array($type, Protocol::$frameTypes)) {
+ throw new InvalidArgumentException('Invalid frame type');
+ }
+
+ $this->type = $type;
+ $this->masked = $masked;
+ $this->payload = $payload;
+ $this->length = strlen($this->payload);
+ $this->offset_mask = null;
+ $this->offset_payload = null;
+
+ $this->buffer = "\x00\x00";
+
+ $this->buffer[self::BYTE_HEADER] = chr(
+ (self::BITFIELD_TYPE & $this->type)
+ | (self::BITFIELD_FINAL & PHP_INT_MAX)
+ );
+
+ $masked_bit = (self::BITFIELD_MASKED & ($this->masked ? PHP_INT_MAX : 0));
+
+ if ($this->length <= 125) {
+ $this->buffer[self::BYTE_INITIAL_LENGTH] = chr(
+ (self::BITFIELD_INITIAL_LENGTH & $this->length) | $masked_bit
+ );
+ } elseif ($this->length <= 65536) {
+ $this->buffer[self::BYTE_INITIAL_LENGTH] = chr(
+ (self::BITFIELD_INITIAL_LENGTH & 126) | $masked_bit
+ );
+ $this->buffer .= pack('n', $this->length);
+ } else {
+ $this->buffer[self::BYTE_INITIAL_LENGTH] = chr(
+ (self::BITFIELD_INITIAL_LENGTH & 127) | $masked_bit
+ );
+
+ if (PHP_INT_MAX > 2147483647) {
+ $this->buffer .= pack('NN', $this->length >> 32, $this->length);
+ // $this->buffer .= pack('I', $this->length);
+ } else {
+ $this->buffer .= pack('NN', 0, $this->length);
+ }
+ }
+
+ if ($this->masked) {
+ $this->mask = $this->generateMask();
+ $this->buffer .= $this->mask;
+ $this->buffer .= $this->mask($this->payload);
+ } else {
+ $this->buffer .= $this->payload;
+ }
+
+ $this->offset_mask = $this->getMaskOffset();
+ $this->offset_payload = $this->getPayloadOffset();
+
+ return $this;
+ }
+
+ /**
+ * Masks/Unmasks the frame
+ *
+ * @param string $payload
+ * @return string
+ */
+ protected function mask($payload)
+ {
+ $length = strlen($payload);
+ $mask = $this->getMask();
+
+ $unmasked = '';
+ for ($i = 0; $i < $length; $i++) {
+ $unmasked .= $payload[$i] ^ $mask[$i % 4];
+ }
+
+ return $unmasked;
+ }
+
+ /**
+ * Masks a payload
+ *
+ * @param string $payload
+ * @return string
+ */
+ protected function unmask($payload)
+ {
+ return $this->mask($payload);
+ }
+
+ public function receiveData($data)
+ {
+ if ($this->getBufferLength() <= self::BYTE_INITIAL_LENGTH) {
+ $this->length = null;
+ $this->offset_payload = null;
+ }
+ parent::receiveData($data);
+ }
+
+ /**
+ * Gets the mask
+ *
+ * @throws FrameException
+ * @return string
+ */
+ protected function getMask()
+ {
+ if (!isset($this->mask)) {
+ if (!$this->isMasked()) {
+ throw new FrameException('Cannot get mask: frame is not masked');
+ }
+ $this->mask = substr($this->buffer, $this->getMaskOffset(), $this->getMaskSize());
+ }
+ return $this->mask;
+ }
+
+ /**
+ * Generates a suitable masking key
+ *
+ * @return string
+ */
+ protected function generateMask()
+ {
+ if (extension_loaded('openssl')) {
+ return openssl_random_pseudo_bytes(4);
+ } else {
+ // SHA1 is 128 bit (= 16 bytes)
+ // So we pack it into 32 bits
+ return pack('N', sha1(spl_object_hash($this) . mt_rand(0, PHP_INT_MAX) . uniqid('', true), true));
+ }
+ }
+
+ /**
+ * Whether the frame is masked
+ *
+ * @return boolean
+ */
+ public function isMasked()
+ {
+ if (!isset($this->masked)) {
+ if (!isset($this->buffer[1])) {
+ throw new FrameException('Cannot tell if frame is masked: not enough frame data received');
+ }
+ $this->masked = (boolean)(ord($this->buffer[1]) & self::BITFIELD_MASKED);
+ }
+ return $this->masked;
+ }
+
+ /**
+ * @see Wrench\Frame.Frame::getExpectedDataLength()
+ */
+ protected function getExpectedBufferLength()
+ {
+ return $this->getLength() + $this->getPayloadOffset();
+ }
+
+ /**
+ * Gets the offset of the payload in the frame
+ *
+ * @return int
+ */
+ protected function getPayloadOffset()
+ {
+ if (!isset($this->offset_payload)) {
+ $offset = $this->getMaskOffset();
+ $offset += $this->getMaskSize();
+
+ $this->offset_payload = $offset;
+ }
+ return $this->offset_payload;
+ }
+
+ /**
+ * Gets the offset in the frame to the masking bytes
+ *
+ * @return int
+ */
+ protected function getMaskOffset()
+ {
+ if (!isset($this->offset_mask)) {
+ $offset = self::BYTE_INITIAL_LENGTH + 1;
+ $offset += $this->getLengthSize();
+
+ $this->offset_mask = $offset;
+ }
+ return $this->offset_mask;
+ }
+
+ /**
+ * @see Wrench\Frame.Frame::getLength()
+ */
+ public function getLength()
+ {
+ if (!$this->length) {
+ $initial = $this->getInitialLength();
+
+ if ($initial < 126) {
+ $this->length = $initial;
+ } elseif ($initial >= 126) {
+ // Extended payload length: 2 or 8 bytes
+ $start = self::BYTE_INITIAL_LENGTH + 1;
+ $end = self::BYTE_INITIAL_LENGTH + $this->getLengthSize();
+
+ if ($end > $this->getBufferLength()) {
+ throw new FrameException('Cannot get extended length: need more data');
+ }
+
+ $length = 0;
+ for ($i = $start; $i <= $end; $i++) {
+ $length <<= 8;
+ $length += ord($this->buffer[$i]);
+ }
+
+ $this->length = $length;
+ }
+ }
+ return $this->length;
+ }
+
+ /**
+ * Gets the inital length value, stored in the first length byte
+ *
+ * This determines how the rest of the length value is parsed out of the
+ * frame.
+ *
+ * @return int
+ */
+ protected function getInitialLength()
+ {
+ if (!isset($this->buffer[self::BYTE_INITIAL_LENGTH])) {
+ throw new FrameException('Cannot yet tell expected length');
+ }
+ $a = (int)(ord($this->buffer[self::BYTE_INITIAL_LENGTH]) & self::BITFIELD_INITIAL_LENGTH);
+
+ return (int)(ord($this->buffer[self::BYTE_INITIAL_LENGTH]) & self::BITFIELD_INITIAL_LENGTH);
+ }
+
+ /**
+ * Returns the byte size of the length part of the frame
+ *
+ * Not including the initial 7 bit part
+ *
+ * @return int
+ */
+ protected function getLengthSize()
+ {
+ $initial = $this->getInitialLength();
+
+ if ($initial < 126) {
+ return 0;
+ } elseif ($initial === 126) {
+ return 2;
+ } elseif ($initial === 127) {
+ return 8;
+ }
+ }
+
+ /**
+ * Returns the byte size of the mask part of the frame
+ *
+ * @return int
+ */
+ protected function getMaskSize()
+ {
+ if ($this->isMasked()) {
+ return 4;
+ }
+ return 0;
+ }
+
+ /**
+ * @see Wrench\Frame.Frame::decodeFramePayloadFromBuffer()
+ */
+ protected function decodeFramePayloadFromBuffer()
+ {
+ $payload = substr($this->buffer, $this->getPayloadOffset());
+
+ if ($this->isMasked()) {
+ $payload = $this->unmask($payload);
+ }
+
+ $this->payload = $payload;
+ }
+
+ /**
+ * @see Wrench\Frame.Frame::isFinal()
+ */
+ public function isFinal()
+ {
+ if (!isset($this->buffer[self::BYTE_HEADER])) {
+ throw new FrameException('Cannot yet tell if frame is final');
+ }
+ return (boolean)(ord($this->buffer[self::BYTE_HEADER]) & self::BITFIELD_FINAL);
+ }
+
+ /**
+ * @throws FrameException
+ * @see Wrench\Frame.Frame::getType()
+ */
+ public function getType()
+ {
+ if (!isset($this->buffer[self::BYTE_HEADER])) {
+ throw new FrameException('Cannot yet tell type of frame');
+ }
+
+ $type = (int)(ord($this->buffer[self::BYTE_HEADER]) & self::BITFIELD_TYPE);
+
+ if (!in_array($type, Protocol::$frameTypes)) {
+ throw new FrameException('Invalid payload type');
+ }
+
+ return $type;
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Listener/HandshakeRequestListener.php b/listeners/lib/Wrench/Listener/HandshakeRequestListener.php
new file mode 100644
index 000000000..9b5b841ee
--- /dev/null
+++ b/listeners/lib/Wrench/Listener/HandshakeRequestListener.php
@@ -0,0 +1,19 @@
+allowed = $allowed;
+ }
+
+ /**
+ * Handshake request listener
+ *
+ * Closes the connection on handshake from an origin that isn't allowed
+ *
+ * @param Connection $connection
+ * @param string $path
+ * @param string $origin
+ * @param string $key
+ * @param array $extensions
+ */
+ public function onHandshakeRequest(Connection $connection, $path, $origin, $key, $extensions)
+ {
+ if (!$this->isAllowed($origin)) {
+ $connection->close(new InvalidOriginException('Origin not allowed'));
+ }
+ }
+
+ /**
+ * Whether the specified origin is allowed under this policy
+ *
+ * @param string $origin
+ * @return boolean
+ */
+ public function isAllowed($origin)
+ {
+ $scheme = parse_url($origin, PHP_URL_SCHEME);
+ $host = parse_url($origin, PHP_URL_HOST) ?: $origin;
+
+ foreach ($this->allowed as $allowed) {
+ $allowed_scheme = parse_url($allowed, PHP_URL_SCHEME);
+
+ if ($allowed_scheme && $scheme != $allowed_scheme) {
+ continue;
+ }
+
+ $allowed_host = parse_url($allowed, PHP_URL_HOST) ?: $allowed;
+
+ if ($host != $allowed_host) {
+ continue;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param Server $server
+ */
+ public function listen(Server $server)
+ {
+ $server->addListener(
+ Server::EVENT_HANDSHAKE_REQUEST,
+ array($this, 'onHandshakeRequest')
+ );
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Listener/RateLimiter.php b/listeners/lib/Wrench/Listener/RateLimiter.php
new file mode 100644
index 000000000..6ca0fe86e
--- /dev/null
+++ b/listeners/lib/Wrench/Listener/RateLimiter.php
@@ -0,0 +1,230 @@
+
+ */
+ protected $ips = array();
+
+ /**
+ * Request tokens per IP address
+ *
+ * @var array>
+ */
+ protected $requests = array();
+
+ /**
+ * Constructor
+ *
+ * @param array $options
+ */
+ public function __construct(array $options = array())
+ {
+ parent::__construct($options);
+ }
+
+ /**
+ * @param array $options
+ */
+ protected function configure(array $options)
+ {
+ $options = array_merge(array(
+ 'connections' => 200, // Total
+ 'connections_per_ip' => 5, // At once
+ 'requests_per_minute' => 200 // Per connection
+ ), $options);
+
+ parent::configure($options);
+ }
+
+ /**
+ * @see Wrench\Listener.Listener::listen()
+ */
+ public function listen(Server $server)
+ {
+ $this->server = $server;
+
+ $server->addListener(
+ Server::EVENT_SOCKET_CONNECT,
+ array($this, 'onSocketConnect')
+ );
+
+ $server->addListener(
+ Server::EVENT_SOCKET_DISCONNECT,
+ array($this, 'onSocketDisconnect')
+ );
+
+ $server->addListener(
+ Server::EVENT_CLIENT_DATA,
+ array($this, 'onClientData')
+ );
+ }
+
+ /**
+ * Event listener
+ *
+ * @param resource $socket
+ * @param Connection $connection
+ */
+ public function onSocketConnect($socket, $connection)
+ {
+ $this->checkConnections($connection);
+ $this->checkConnectionsPerIp($connection);
+ }
+
+ /**
+ * Event listener
+ *
+ * @param resource $socket
+ * @param Connection $connection
+ */
+ public function onSocketDisconnect($socket, $connection)
+ {
+ $this->releaseConnection($connection);
+ }
+
+ /**
+ * Event listener
+ *
+ * @param resource $socket
+ * @param Connection $connection
+ */
+ public function onClientData($socket, $connection)
+ {
+ $this->checkRequestsPerMinute($connection);
+ }
+
+ /**
+ * Idempotent
+ *
+ * @param Connection $connection
+ */
+ protected function checkConnections($connection)
+ {
+ $connections = $connection->getConnectionManager()->count();
+
+ if ($connections > $this->options['connections']) {
+ $this->limit($connection, 'Max connections');
+ }
+ }
+
+ /**
+ * NOT idempotent, call once per connection
+ *
+ * @param Connection $connection
+ */
+ protected function checkConnectionsPerIp($connection)
+ {
+ $ip = $connection->getIp();
+
+ if (!$ip) {
+ $this->log('Cannot check connections per IP', 'warning');
+ return;
+ }
+
+ if (!isset($this->ips[$ip])) {
+ $this->ips[$ip] = 1;
+ } else {
+ $this->ips[$ip] = min(
+ $this->options['connections_per_ip'],
+ $this->ips[$ip] + 1
+ );
+ }
+
+ if ($this->ips[$ip] > $this->options['connections_per_ip']) {
+ $this->limit($connection, 'Connections per IP');
+ }
+ }
+
+ /**
+ * NOT idempotent, call once per disconnection
+ *
+ * @param Connection $connection
+ */
+ protected function releaseConnection($connection)
+ {
+ $ip = $connection->getIp();
+
+ if (!$ip) {
+ $this->log('Cannot release connection', 'warning');
+ return;
+ }
+
+ if (!isset($this->ips[$ip])) {
+ $this->ips[$ip] = 0;
+ } else {
+ $this->ips[$ip] = max(0, $this->ips[$ip] - 1);
+ }
+
+ unset($this->requests[$connection->getId()]);
+ }
+
+ /**
+ * NOT idempotent, call once per data
+ *
+ * @param Connection $connection
+ */
+ protected function checkRequestsPerMinute($connection)
+ {
+ $id = $connection->getId();
+
+ if (!isset($this->requests[$id])) {
+ $this->requests[$id] = array();
+ }
+
+ // Add current token
+ $this->requests[$id][] = time();
+
+ // Expire old tokens
+ while (reset($this->requests[$id]) < time() - 60) {
+ array_shift($this->requests[$id]);
+ }
+
+ if (count($this->requests[$id]) > $this->options['requests_per_minute']) {
+ $this->limit($connection, 'Requests per minute');
+ }
+ }
+
+ /**
+ * Limits the given connection
+ *
+ * @param Connection $connection
+ * @param string $limit Reason
+ */
+ protected function limit($connection, $limit)
+ {
+ $this->log(sprintf(
+ 'Limiting connection %s: %s',
+ $connection->getIp(),
+ $limit
+ ), 'notice');
+
+ $connection->close(new RateLimiterException($limit));
+ }
+
+ /**
+ * Logger
+ *
+ * @param string $message
+ * @param string $priority
+ */
+ public function log($message, $priority = 'info')
+ {
+ $this->server->log('RateLimiter: ' . $message, $priority);
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Payload/HybiPayload.php b/listeners/lib/Wrench/Payload/HybiPayload.php
new file mode 100644
index 000000000..92d4821ab
--- /dev/null
+++ b/listeners/lib/Wrench/Payload/HybiPayload.php
@@ -0,0 +1,22 @@
+
+ */
+ protected $frames = array();
+
+ /**
+ * Gets the current frame for the payload
+ *
+ * @return mixed
+ */
+ protected function getCurrentFrame()
+ {
+ if (empty($this->frames)) {
+ array_push($this->frames, $this->getFrame());
+ }
+ return end($this->frames);
+ }
+
+ /**
+ * Gets the frame into which data should be receieved
+ *
+ * @throws PayloadException
+ * @return Frame
+ */
+ protected function getReceivingFrame()
+ {
+ $current = $this->getCurrentFrame();
+
+ if ($current->isComplete()) {
+ if ($current->isFinal()) {
+ throw new PayloadException('Payload cannot receieve data: it is already complete');
+ } else {
+ $current = array_push($this->frames, $this->getFrame());
+ }
+ }
+
+ return $current;
+ }
+
+ /**
+ * Get a frame object
+ *
+ * @return Frame
+ */
+ abstract protected function getFrame();
+
+ /**
+ * Whether the payload is complete
+ *
+ * @return boolean
+ */
+ public function isComplete()
+ {
+ return $this->getCurrentFrame()->isComplete() && $this->getCurrentFrame()->isFinal();
+ }
+
+ /**
+ * Encodes a payload
+ *
+ * @param string $data
+ * @param int $type
+ * @param boolean $masked
+ * @return Payload
+ * @todo No splitting into multiple frames just yet
+ */
+ public function encode($data, $type = Protocol::TYPE_TEXT, $masked = false)
+ {
+ $this->frames = array();
+
+ $frame = $this->getFrame();
+ array_push($this->frames, $frame);
+
+ $frame->encode($data, $type, $masked);
+
+ return $this;
+ }
+
+ /**
+ * Gets the number of remaining bytes before this payload will be
+ * complete
+ *
+ * May return 0 (no more bytes required) or null (unknown number of bytes
+ * required).
+ *
+ * @return number|NULL
+ */
+ public function getRemainingData()
+ {
+ if ($this->isComplete()) {
+ return 0;
+ }
+
+ try {
+ if ($this->getCurrentFrame()->isFinal()) {
+ return $this->getCurrentFrame()->getRemainingData();
+ }
+ } catch (FrameException $e) {
+ return null;
+ }
+
+ return null;
+ }
+
+ /**
+ * Whether this payload is waiting for more data
+ *
+ * @return boolean
+ */
+ public function isWaitingForData()
+ {
+ return $this->getRemainingData() > 0;
+ }
+
+ /**
+ * @param Socket $socket
+ * @return boolean
+ */
+ public function sendToSocket(Socket $socket)
+ {
+ $success = true;
+ foreach ($this->frames as $frame) {
+ $success = $success && ($socket->send($frame->getFrameBuffer()) !== false);
+ }
+ return $success;
+ }
+
+ /**
+ * Receive raw data into the payload
+ *
+ * @param string $data
+ * @return void
+ */
+ public function receiveData($data)
+ {
+ while ($data) {
+ $frame = $this->getReceivingFrame();
+
+ $size = strlen($data);
+ $remaining = $frame->getRemainingData();
+
+ if ($remaining === null) {
+ $chunk_size = 2;
+ } elseif ($remaining > 0) {
+ $chunk_size = $remaining;
+ }
+
+ $chunk_size = min(strlen($data), $chunk_size);
+ $chunk = substr($data, 0, $chunk_size);
+ $data = substr($data, $chunk_size);
+
+ $frame->receiveData($chunk);
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function getPayload()
+ {
+ $this->buffer = '';
+
+ foreach ($this->frames as $frame) {
+ $this->buffer .= $frame->getFramePayload();
+ }
+
+ return $this->buffer;
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString()
+ {
+ try {
+ return $this->getPayload();
+ } catch (\Exception $e) {
+ // __toString must not throw an exception
+ return '';
+ }
+ }
+
+ /**
+ * Gets the type of the payload
+ *
+ * The type of a payload is taken from its first frame
+ *
+ * @throws PayloadException
+ * @return int
+ */
+ public function getType()
+ {
+ if (!isset($this->frames[0])) {
+ throw new PayloadException('Cannot tell payload type yet');
+ }
+ return $this->frames[0]->getType();
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Payload/PayloadHandler.php b/listeners/lib/Wrench/Payload/PayloadHandler.php
new file mode 100644
index 000000000..2be036ff1
--- /dev/null
+++ b/listeners/lib/Wrench/Payload/PayloadHandler.php
@@ -0,0 +1,110 @@
+callback = $callback;
+ }
+
+ /**
+ * Handles the raw socket data given
+ *
+ * @param string $data
+ */
+ public function handle($data)
+ {
+ if (!$this->payload) {
+ $this->payload = $this->protocol->getPayload();
+ }
+
+ while ($data) { // Each iteration pulls off a single payload chunk
+ $size = strlen($data);
+ $remaining = $this->payload->getRemainingData();
+
+ // If we don't yet know how much data is remaining, read data into
+ // the payload in two byte chunks (the size of a WebSocket frame
+ // header to get the initial length)
+ //
+ // Then re-loop. For extended lengths, this will happen once or four
+ // times extra, as the extended length is read in.
+ if ($remaining === null) {
+ $chunk_size = 2;
+ } elseif ($remaining > 0) {
+ $chunk_size = $remaining;
+ } elseif ($remaining === 0) {
+ $chunk_size = 0;
+ }
+
+ $chunk_size = min(strlen($data), $chunk_size);
+ $chunk = substr($data, 0, $chunk_size);
+ $data = substr($data, $chunk_size);
+
+ $this->payload->receiveData($chunk);
+
+ if ($remaining !== 0 && !$this->payload->isComplete()) {
+ continue;
+ }
+
+ if ($this->payload->isComplete()) {
+ $this->emit($this->payload);
+ $this->payload = $this->protocol->getPayload();
+ } else {
+ throw new PayloadException('Payload will not complete');
+ }
+ }
+ }
+
+ /**
+ * Get the current payload
+ *
+ * @return Payload
+ */
+ public function getCurrent()
+ {
+ return $this->getPayloadHandler->getCurrent();
+ }
+
+ /**
+ * Emits a complete payload to the callback
+ *
+ * @param Payload $payload
+ */
+ protected function emit(Payload $payload)
+ {
+ call_user_func($this->callback, $payload);
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Protocol/Hybi10Protocol.php b/listeners/lib/Wrench/Protocol/Hybi10Protocol.php
new file mode 100644
index 000000000..33cfe15e3
--- /dev/null
+++ b/listeners/lib/Wrench/Protocol/Hybi10Protocol.php
@@ -0,0 +1,35 @@
+= 10) {
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Protocol/HybiProtocol.php b/listeners/lib/Wrench/Protocol/HybiProtocol.php
new file mode 100644
index 000000000..1d73c2fab
--- /dev/null
+++ b/listeners/lib/Wrench/Protocol/HybiProtocol.php
@@ -0,0 +1,24 @@
+
+ */
+ protected static $schemes = array(
+ self::SCHEME_WEBSOCKET,
+ self::SCHEME_WEBSOCKET_SECURE,
+ self::SCHEME_UNDERLYING,
+ self::SCHEME_UNDERLYING_SECURE
+ );
+
+ /**
+ * Close status codes
+ *
+ * @var array string>
+ */
+ public static $closeReasons = array(
+ self::CLOSE_NORMAL => 'normal close',
+ self::CLOSE_GOING_AWAY => 'going away',
+ self::CLOSE_PROTOCOL_ERROR => 'protocol error',
+ self::CLOSE_DATA_INVALID => 'data invalid',
+ self::CLOSE_DATA_INCONSISTENT => 'data inconsistent',
+ self::CLOSE_POLICY_VIOLATION => 'policy violation',
+ self::CLOSE_MESSAGE_TOO_BIG => 'message too big',
+ self::CLOSE_EXTENSION_NEEDED => 'extension needed',
+ self::CLOSE_UNEXPECTED => 'unexpected error',
+ self::CLOSE_RESERVED => null, // Don't use these!
+ self::CLOSE_RESERVED_NONE => null,
+ self::CLOSE_RESERVED_ABNORM => null,
+ self::CLOSE_RESERVED_TLS => null
+ );
+
+ /**
+ * Frame types
+ *
+ * @todo flip values and keys?
+ * @var array int>
+ */
+ public static $frameTypes = array(
+ 'continuation' => self::TYPE_CONTINUATION,
+ 'text' => self::TYPE_TEXT,
+ 'binary' => self::TYPE_BINARY,
+ 'close' => self::TYPE_CLOSE,
+ 'ping' => self::TYPE_PING,
+ 'pong' => self::TYPE_PONG
+ );
+
+ /**
+ * HTTP errors
+ *
+ * @var array string>
+ */
+ public static $httpResponses = array(
+ self::HTTP_SWITCHING_PROTOCOLS => 'Switching Protocols',
+ self::HTTP_BAD_REQUEST => 'Bad Request',
+ self::HTTP_UNAUTHORIZED => 'Unauthorized',
+ self::HTTP_FORBIDDEN => 'Forbidden',
+ self::HTTP_NOT_FOUND => 'Not Found',
+ self::HTTP_NOT_IMPLEMENTED => 'Not Implemented',
+ self::HTTP_RATE_LIMITED => 'Enhance Your Calm'
+ );
+
+ /**
+ * Gets a version number
+ *
+ * @return
+ */
+ abstract public function getVersion();
+
+ /**
+ * Subclasses should implement this method and return a boolean to the given
+ * version string, as to whether they would like to accept requests from
+ * user agents that specify that version.
+ *
+ * @return boolean
+ */
+ abstract public function acceptsVersion($version);
+
+ /**
+ * Gets a payload instance, suitable for use in decoding/encoding protocol
+ * frames
+ *
+ * @return Payload
+ */
+ abstract public function getPayload();
+
+ /**
+ * Generates a key suitable for use in the protocol
+ *
+ * This base implementation returns a 16-byte (128 bit) random key as a
+ * binary string.
+ *
+ * @return string
+ */
+ public function generateKey()
+ {
+ if (extension_loaded('openssl')) {
+ $key = openssl_random_pseudo_bytes(16);
+ } else {
+ // SHA1 is 128 bit (= 16 bytes)
+ $key = sha1(spl_object_hash($this) . mt_rand(0, PHP_INT_MAX) . uniqid('', true), true);
+ }
+
+ return base64_encode($key);
+ }
+
+ /**
+ * Gets request handshake string
+ *
+ * The leading line from the client follows the Request-Line format.
+ * The leading line from the server follows the Status-Line format. The
+ * Request-Line and Status-Line productions are defined in [RFC2616].
+ *
+ * An unordered set of header fields comes after the leading line in
+ * both cases. The meaning of these header fields is specified in
+ * Section 4 of this document. Additional header fields may also be
+ * present, such as cookies [RFC6265]. The format and parsing of
+ * headers is as defined in [RFC2616].
+ *
+ * @param string $uri WebSocket URI, e.g. ws://example.org:8000/chat
+ * @param string $key 16 byte binary string key
+ * @param string $origin Origin of the request
+ * @return string
+ */
+ public function getRequestHandshake(
+ $uri,
+ $key,
+ $origin,
+ array $headers = array()
+ ) {
+ if (!$uri || !$key || !$origin) {
+ throw new InvalidArgumentException('You must supply a URI, key and origin');
+ }
+
+ list($scheme, $host, $port, $path) = self::validateUri($uri);
+
+ $handshake = array(
+ sprintf(self::REQUEST_LINE_FORMAT, $path)
+ );
+
+ $headers = array_merge(
+ $this->getDefaultRequestHeaders(
+ $host . ':' . $port, $key, $origin
+ ),
+ $headers
+ );
+
+ foreach ($headers as $name => $value) {
+ $handshake[] = sprintf(self::HEADER_LINE_FORMAT, $name, $value);
+ }
+ return implode($handshake, "\r\n") . "\r\n\r\n";
+ }
+
+ /**
+ * Gets a handshake response body
+ *
+ * @param string $key
+ * @param array $headers
+ */
+ public function getResponseHandshake($key, array $headers = array())
+ {
+ $headers = array_merge(
+ $this->getSuccessResponseHeaders(
+ $key
+ ),
+ $headers
+ );
+
+ return $this->getHttpResponse(self::HTTP_SWITCHING_PROTOCOLS, $headers);
+ }
+
+ /**
+ * Gets a response to an error in the handshake
+ *
+ * @param int|Exception $e Exception or HTTP error
+ * @param array $headers
+ */
+ public function getResponseError($e, array $headers = array())
+ {
+ $code = false;
+
+ if ($e instanceof Exception) {
+ $code = $e->getCode();
+ } elseif (is_numeric($e)) {
+ $code = (int)$e;
+ }
+
+ if (!$code || $code < 400 || $code > 599) {
+ $code = self::HTTP_SERVER_ERROR;
+ }
+
+ return $this->getHttpResponse($code, $headers);
+ }
+
+ /**
+ * Gets an HTTP response
+ *
+ * @param int $status
+ * @param array $headers
+ */
+ protected function getHttpResponse($status, array $headers = array())
+ {
+ if (array_key_exists($status, self::$httpResponses)) {
+ $response = self::$httpResponses[$status];
+ } else {
+ $response = self::$httpResponses[self::HTTP_NOT_IMPLEMENTED];
+ }
+
+ $handshake = array(
+ sprintf(self::RESPONSE_LINE_FORMAT, $status, $response)
+ );
+
+ foreach ($headers as $name => $value) {
+ $handshake[] = sprintf(self::HEADER_LINE_FORMAT, $name, $value);
+ }
+
+ return implode($handshake, "\r\n") . "\r\n\r\n";
+ }
+
+ /**
+ * @todo better header handling
+ * @todo throw exception
+ * @param unknown_type $response
+ * @param unknown_type $key
+ * @return boolean
+ */
+ public function validateResponseHandshake($response, $key)
+ {
+ if (!$response) {
+ return false;
+ }
+
+ $headers = $this->getHeaders($response);
+
+ if (!isset($headers[self::HEADER_ACCEPT])) {
+ throw new HandshakeException('No accept header receieved on handshake response');
+ }
+
+ $accept = $headers[self::HEADER_ACCEPT];
+
+ if (!$accept) {
+ throw new HandshakeException('Invalid accept header');
+ }
+
+ $expected = $this->getAcceptValue($key);
+
+ preg_match('#Sec-WebSocket-Accept:\s(.*)$#mU', $response, $matches);
+ $keyAccept = trim($matches[1]);
+
+ return ($keyAccept === $this->getEncodedHash($key)) ? true : false;
+ }
+
+ /**
+ * Gets an encoded hash for a key
+ *
+ * @param string $key
+ * @return string
+ */
+ public function getEncodedHash($key)
+ {
+ return base64_encode(pack('H*', sha1($key . self::MAGIC_GUID)));
+ }
+
+ /**
+ * Validates a request handshake
+ *
+ * @param string $request
+ * @throws BadRequestException
+ */
+ public function validateRequestHandshake(
+ $request
+ ) {
+ if (!$request) {
+ return false;
+ }
+
+ list($request, $headers) = $this->getRequestHeaders($request);
+ // make a copy of the headers array to store all extra headers
+ $extraHeaders = $headers;
+
+ // parse the resulting url to separate query parameters from the path
+ $url = parse_url($this->validateRequestLine($request));
+ $path = isset($url['path']) ? $url['path'] : null;
+ $urlParams = array();
+ if (isset($url['query'])) {
+ parse_str($url['query'], $urlParams);
+ }
+
+ if (empty($headers[self::HEADER_ORIGIN])) {
+ throw new BadRequestException('No origin header');
+ } else {
+ unset($extraHeaders[self::HEADER_ORIGIN]);
+ }
+
+ $origin = $headers[self::HEADER_ORIGIN];
+
+ if (!isset($headers[self::HEADER_UPGRADE])
+ || strtolower($headers[self::HEADER_UPGRADE]) != self::UPGRADE_VALUE
+ ) {
+ throw new BadRequestException('Invalid upgrade header');
+ } else {
+ unset($extraHeaders[self::HEADER_UPGRADE]);
+ }
+
+ if (!isset($headers[self::HEADER_CONNECTION])
+ || stripos($headers[self::HEADER_CONNECTION], self::CONNECTION_VALUE) === false
+ ) {
+ throw new BadRequestException('Invalid connection header');
+ } else {
+ unset($extraHeaders[self::HEADER_CONNECTION]);
+ }
+
+ if (!isset($headers[self::HEADER_HOST])) {
+ // @todo Validate host == listening socket? Or would that break
+ // TCP proxies?
+ throw new BadRequestException('No host header');
+ } else {
+ unset($extraHeaders[self::HEADER_HOST]);
+ }
+
+ if (!isset($headers[self::HEADER_VERSION])) {
+ throw new BadRequestException('No version header received on handshake request');
+ }
+
+ if (!$this->acceptsVersion($headers[self::HEADER_VERSION])) {
+ throw new BadRequestException('Unsupported version: ' . $version);
+ } else {
+ unset($extraHeaders[self::HEADER_VERSION]);
+ }
+
+ if (!isset($headers[self::HEADER_KEY])) {
+ throw new BadRequestException('No key header received');
+ }
+
+ $key = trim($headers[self::HEADER_KEY]);
+
+ if (!$key) {
+ throw new BadRequestException('Invalid key');
+ } else {
+ unset($extraHeaders[self::HEADER_KEY]);
+ }
+
+ // Optional
+ $protocol = null;
+ if (isset($headers[self::HEADER_PROTOCOL])) {
+ $protocol = $headers[self::HEADER_PROTOCOL];
+ unset($extraHeaders[self::HEADER_PROTOCOL]);
+ }
+
+ $extensions = array();
+ if (!empty($headers[self::HEADER_EXTENSIONS])) {
+ $extensions = $headers[self::HEADER_EXTENSIONS];
+ if (is_scalar($extensions)) {
+ $extensions = array($extensions);
+ }
+ }
+ unset($extraHeaders[self::HEADER_EXTENSIONS]);
+
+ return array($path, $origin, $key, $extensions, $protocol, $extraHeaders, $urlParams);
+ }
+
+ /**
+ * Gets a suitable WebSocket close frame
+ *
+ * @param Exception|int $e
+ */
+ public function getCloseFrame($e)
+ {
+ $code = false;
+
+ if ($e instanceof Exception) {
+ $code = $e->getCode();
+ } elseif (is_numeric($e)) {
+ $code = (int)$e;
+ }
+
+ if (!$code || !key_exists($code, self::$closeReasons)) {
+ $code = self::CLOSE_UNEXPECTED;
+ }
+
+ $body = pack('n', $code) . self::$closeReasons[$code];
+
+ $payload = $this->getPayload();
+ return $payload->encode($body, self::TYPE_CLOSE);
+ }
+
+ /**
+ * Validates a WebSocket URI
+ *
+ * @param string $uri
+ * @return array(string $scheme, string $host, int $port, string $path)
+ */
+ public function validateUri($uri)
+ {
+ $uri = (string)$uri;
+ if (!$uri) {
+ throw new InvalidArgumentException('Invalid URI');
+ }
+
+ $scheme = parse_url($uri, PHP_URL_SCHEME);
+ $this->validateScheme($scheme);
+
+ $host = parse_url($uri, PHP_URL_HOST);
+ if (!$host) {
+ throw new InvalidArgumentException("Invalid host");
+ }
+
+ $port = parse_url($uri, PHP_URL_PORT);
+ if (!$port) {
+ $port = $this->getPort($scheme);
+ }
+
+ $path = parse_url($uri, PHP_URL_PATH);
+ if (!$path) {
+ throw new InvalidArgumentException('Invalid path');
+ }
+
+ return array($scheme, $host, $port, $path);
+ }
+
+ /**
+ * Validates a socket URI
+ *
+ * @param string $uri
+ * @throws InvalidArgumentException
+ * @return array(string $scheme, string $host, string $port)
+ */
+ public function validateSocketUri($uri)
+ {
+ $uri = (string)$uri;
+ if (!$uri) {
+ throw new InvalidArgumentException('Invalid URI');
+ }
+
+ $scheme = parse_url($uri, PHP_URL_SCHEME);
+ $scheme = $this->validateScheme($scheme);
+
+ $host = parse_url($uri, PHP_URL_HOST);
+ if (!$host) {
+ throw new InvalidArgumentException("Invalid host");
+ }
+
+ $port = parse_url($uri, PHP_URL_PORT);
+ if (!$port) {
+ $port = $this->getPort($scheme);
+ }
+
+ return array($scheme, $host, $port);
+ }
+
+ /**
+ * Validates an origin URI
+ *
+ * @param string $origin
+ * @throws InvalidArgumentException
+ * @return string
+ */
+ public function validateOriginUri($origin)
+ {
+ $origin = (string)$origin;
+ if (!$origin) {
+ throw new InvalidArgumentException('Invalid URI');
+ }
+
+ $scheme = parse_url($origin, PHP_URL_SCHEME);
+ if (!$scheme) {
+ throw new InvalidArgumentException('Invalid scheme');
+ }
+
+ $host = parse_url($origin, PHP_URL_HOST);
+ if (!$host) {
+ throw new InvalidArgumentException("Invalid host");
+ }
+
+ return $origin;
+ }
+
+ /**
+ * Validates a request line
+ *
+ * @param string $line
+ * @throws BadRequestException
+ */
+ protected function validateRequestLine($line)
+ {
+ $matches = array(0 => null, 1 => null);
+
+ if (!preg_match(self::REQUEST_LINE_REGEX, $line, $matches) || !$matches[1]) {
+ throw new BadRequestException('Invalid request line', 400);
+ }
+
+ return $matches[1];
+ }
+
+ /**
+ * Gets the expected accept value for a handshake response
+ *
+ * Note that the protocol calls for the base64 encoded value to be hashed,
+ * not the original 16 byte random key.
+ *
+ * @see http://tools.ietf.org/html/rfc6455#section-4.2.2
+ * @param string $key
+ */
+ protected function getAcceptValue($encoded_key)
+ {
+ return base64_encode(sha1($encoded_key . self::MAGIC_GUID, true));
+ }
+
+ /**
+ * Gets the headers from a full response
+ *
+ * @param string $response
+ * @return array()
+ * @throws InvalidArgumentException
+ */
+ protected function getHeaders($response, &$request_line = null)
+ {
+ $parts = explode("\r\n\r\n", $response, 2);
+
+ if (count($parts) != 2) {
+ $parts = array($parts, '');
+ }
+
+ list($headers, $body) = $parts;
+
+ $return = array();
+ foreach (explode("\r\n", $headers) as $header) {
+ $parts = explode(': ', $header, 2);
+ if (count($parts) == 2) {
+ list($name, $value) = $parts;
+ if (!isset($return[$name])) {
+ $return[$name] = $value;
+ } else {
+ if (is_array($return[$name])) {
+ $return[$name][] = $value;
+ } else {
+ $return[$name] = array($return[$name], $value);
+ }
+ }
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Gets request headers
+ *
+ * @param string $response
+ * @return array> The request line, and an array of
+ * headers
+ * @throws InvalidArgumentException
+ */
+ protected function getRequestHeaders($response)
+ {
+ $eol = stripos($response, "\r\n");
+
+ if ($eol === false) {
+ throw new InvalidArgumentException('Invalid request line');
+ }
+
+ $request = substr($response, 0, $eol);
+ $headers = $this->getHeaders(substr($response, $eol + 2));
+
+ return array($request, $headers);
+ }
+
+ /**
+ * Validates a scheme
+ *
+ * @param string $scheme
+ * @return string Underlying scheme
+ * @throws InvalidArgumentException
+ */
+ protected function validateScheme($scheme)
+ {
+ if (!$scheme) {
+ throw new InvalidArgumentException('No scheme specified');
+ }
+ if (!in_array($scheme, self::$schemes)) {
+ throw new InvalidArgumentException(
+ 'Unknown socket scheme: ' . $scheme
+ );
+ }
+
+ if ($scheme == self::SCHEME_WEBSOCKET_SECURE) {
+ return self::SCHEME_UNDERLYING_SECURE;
+ }
+ return self::SCHEME_UNDERLYING;
+ }
+
+ /**
+ * Gets the default request headers
+ *
+ * @param string $host
+ * @param string $key
+ * @param string $origin
+ * @param int $version
+ * @return multitype:unknown string NULL
+ */
+ protected function getDefaultRequestHeaders($host, $key, $origin)
+ {
+ return array(
+ self::HEADER_HOST => $host,
+ self::HEADER_UPGRADE => self::UPGRADE_VALUE,
+ self::HEADER_CONNECTION => self::CONNECTION_VALUE,
+ self::HEADER_KEY => $key,
+ self::HEADER_ORIGIN => $origin,
+ self::HEADER_VERSION => $this->getVersion()
+ );
+ }
+
+ /**
+ * Gets the default response headers
+ *
+ * @param string $key
+ */
+ protected function getSuccessResponseHeaders($key)
+ {
+ return array(
+ self::HEADER_UPGRADE => self::UPGRADE_VALUE,
+ self::HEADER_CONNECTION => self::CONNECTION_VALUE,
+ self::HEADER_ACCEPT => $this->getAcceptValue($key)
+ );
+ }
+
+ /**
+ * Gets the default port for a scheme
+ *
+ * By default, the WebSocket Protocol uses port 80 for regular WebSocket
+ * connections and port 443 for WebSocket connections tunneled over
+ * Transport Layer Security
+ *
+ * @param string $uri
+ * @return int
+ */
+ protected function getPort($scheme)
+ {
+ if ($scheme == self::SCHEME_WEBSOCKET) {
+ return 80;
+ } elseif ($scheme == self::SCHEME_WEBSOCKET_SECURE) {
+ return 443;
+ } elseif ($scheme == self::SCHEME_UNDERLYING) {
+ return 80;
+ } elseif ($scheme == self::SCHEME_UNDERLYING_SECURE) {
+ return 443;
+ } else {
+ throw new InvalidArgumentException('Unknown websocket scheme');
+ }
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Protocol/Rfc6455Protocol.php b/listeners/lib/Wrench/Protocol/Rfc6455Protocol.php
new file mode 100644
index 000000000..b0f5c5289
--- /dev/null
+++ b/listeners/lib/Wrench/Protocol/Rfc6455Protocol.php
@@ -0,0 +1,36 @@
+
+ * @author Simon Samtleben
+ * @author Dominic Scheirlinck
+ */
+class Server extends Configurable
+{
+ /**#@+
+ * Events
+ *
+ * @var string
+ */
+ const EVENT_SOCKET_CONNECT = 'socket_connect';
+ const EVENT_SOCKET_DISCONNECT = 'socket_disconnect';
+ const EVENT_HANDSHAKE_REQUEST = 'handshake_request';
+ const EVENT_HANDSHAKE_SUCCESSFUL = 'handshake_successful';
+ const EVENT_CLIENT_DATA = 'client_data';
+ /**#@-*/
+
+ /**
+ * The URI of the server
+ *
+ * @var string
+ */
+ protected $uri;
+
+ /**
+ * Options
+ *
+ * @var array
+ */
+ protected $options = array();
+
+ /**
+ * A logging callback
+ *
+ * The default callback simply prints to stdout. You can pass your own logger
+ * in the options array. It should take a string message and string priority
+ * as parameters.
+ *
+ * @var Closure
+ */
+ protected $logger;
+
+ /**
+ * Event listeners
+ *
+ * Add listeners using the addListener() method.
+ *
+ * @var array array>
+ */
+ protected $listeners = array();
+
+ /**
+ * Connection manager
+ *
+ * @var ConnectionManager
+ */
+ protected $connectionManager;
+
+ /**
+ * Applications
+ *
+ * @var array Application>
+ */
+ protected $applications = array();
+
+ /**
+ * Constructor
+ *
+ * @param string $uri Websocket URI, e.g. ws://localhost:8000/, path will
+ * be ignored
+ * @param array $options (optional) See configure
+ */
+ public function __construct($uri, array $options = array())
+ {
+ $this->uri = $uri;
+
+ parent::__construct($options);
+
+ $this->log('Server initialized', 'info');
+ }
+
+ /**
+ * Configure options
+ *
+ * Options include
+ * - socket_class => The socket class to use, defaults to ServerSocket
+ * - socket_options => An array of socket options
+ * - logger => Closure($message, $priority = 'info'), used
+ * for logging
+ *
+ * @param array $options
+ * @return void
+ */
+ protected function configure(array $options)
+ {
+ $options = array_merge(array(
+ 'connection_manager_class' => 'Wrench\ConnectionManager',
+ 'connection_manager_options' => array()
+ ), $options);
+
+ parent::configure($options);
+
+ $this->configureConnectionManager();
+ $this->configureLogger();
+ }
+
+ /**
+ * Configures the logger
+ *
+ * @return void
+ */
+ protected function configureLogger()
+ {
+ // Default logger
+ if (!isset($this->options['logger'])) {
+ $this->options['logger'] = function ($message, $priority = 'info') {
+ printf("%s: %s%s", $priority, $message, PHP_EOL);
+ };
+ }
+ $this->setLogger($this->options['logger']);
+ }
+
+ /**
+ * Configures the connection manager
+ *
+ * @return void
+ */
+ protected function configureConnectionManager()
+ {
+ $class = $this->options['connection_manager_class'];
+ $options = $this->options['connection_manager_options'];
+ $this->connectionManager = new $class($this, $options);
+ }
+
+ /**
+ * Gets the connection manager
+ *
+ * @return \Wrench\ConnectionManager
+ */
+ public function getConnectionManager()
+ {
+ return $this->connectionManager;
+ }
+
+ /**
+ * @return string
+ */
+ public function getUri()
+ {
+ return $this->uri;
+ }
+
+ /**
+ * Sets a logger
+ *
+ * @param Closure $logger
+ * @return void
+ */
+ public function setLogger($logger)
+ {
+ if (!is_callable($logger)) {
+ throw new \InvalidArgumentException('Logger must be callable');
+ }
+ $this->logger = $logger;
+ }
+
+ /**
+ * Main server loop
+ *
+ * @return void This method does not return!
+ */
+ public function run()
+ {
+ $this->connectionManager->listen();
+
+ while (true) {
+ /*
+ * If there's nothing changed on any of the sockets, the server
+ * will sleep and other processes will have a change to run. Control
+ * this behaviour with the timeout options.
+ */
+ $this->connectionManager->selectAndProcess();
+
+ /*
+ * If the application wants to perform periodic operations or queries and push updates to clients based on the result then that logic can be implemented in the 'onUpdate' method.
+ */
+ foreach($this->applications as $application) {
+ if(method_exists($application, 'onUpdate')) {
+ $application->onUpdate();
+ }
+ }
+ }
+ }
+
+ /**
+ * Logs a message to the server log
+ *
+ * The default logger simply prints the message to stdout. You can provide
+ * a logging closure. This is useful, for instance, if you've daemonized
+ * and closed STDOUT.
+ *
+ * @param string $message Message to display.
+ * @param string $type Type of message.
+ * @return void
+ */
+ public function log($message, $priority = 'info')
+ {
+ call_user_func($this->logger, $message, $priority);
+ }
+
+ /**
+ * Notifies listeners of an event
+ *
+ * @param string $event
+ * @param array $arguments Event arguments
+ * @return void
+ */
+ public function notify($event, array $arguments = array())
+ {
+ if (!isset($this->listeners[$event])) {
+ return;
+ }
+
+ foreach ($this->listeners[$event] as $listener) {
+ call_user_func_array($listener, $arguments);
+ }
+ }
+
+ /**
+ * Adds a listener
+ *
+ * Provide an event (see the Server::EVENT_* constants) and a callback
+ * closure. Some arguments may be provided to your callback, such as the
+ * connection the caused the event.
+ *
+ * @param string $event
+ * @param Closure $callback
+ * @return void
+ * @throws InvalidArgumentException
+ */
+ public function addListener($event, $callback)
+ {
+ if (!isset($this->listeners[$event])) {
+ $this->listeners[$event] = array();
+ }
+
+ if (!is_callable($callback)) {
+ throw new InvalidArgumentException('Invalid listener');
+ }
+
+ $this->listeners[$event][] = $callback;
+ }
+
+ /**
+ * Returns a server application.
+ *
+ * @param string $key Name of application.
+ * @return Application The application object.
+ */
+ public function getApplication($key)
+ {
+ if (empty($key)) {
+ return false;
+ }
+
+ if (array_key_exists($key, $this->applications)) {
+ return $this->applications[$key];
+ }
+
+ return false;
+ }
+
+ /**
+ * Adds a new application object to the application storage.
+ *
+ * @param string $key Name of application.
+ * @param object $application The application object
+ * @return void
+ */
+ public function registerApplication($key, $application)
+ {
+ $this->applications[$key] = $application;
+ }
+}
diff --git a/listeners/lib/Wrench/Socket/ClientSocket.php b/listeners/lib/Wrench/Socket/ClientSocket.php
new file mode 100644
index 000000000..9f5fbe293
--- /dev/null
+++ b/listeners/lib/Wrench/Socket/ClientSocket.php
@@ -0,0 +1,105 @@
+ int, seconds, default 2
+ */
+class ClientSocket extends UriSocket
+{
+ /**
+ * Default connection timeout
+ *
+ * @var int seconds
+ */
+ const TIMEOUT_CONNECT = 2;
+
+ /**
+ * @see Wrench\Socket.Socket::configure()
+ * Options include:
+ * - ssl_verify_peer => boolean, whether to perform peer verification
+ * of SSL certificate used
+ * - ssl_allow_self_signed => boolean, whether ssl_verify_peer allows
+ * self-signed certs
+ * - timeout_connect => int, seconds, default 2
+ */
+ protected function configure(array $options)
+ {
+ $options = array_merge(array(
+ 'timeout_connect' => self::TIMEOUT_CONNECT,
+ 'ssl_verify_peer' => false,
+ 'ssl_allow_self_signed' => true
+ ), $options);
+
+ parent::configure($options);
+ }
+
+ /**
+ * Connects to the given socket
+ */
+ public function connect()
+ {
+ if ($this->isConnected()) {
+ return true;
+ }
+
+ $errno = null;
+ $errstr = null;
+
+ $this->socket = stream_socket_client(
+ $this->getUri(),
+ $errno,
+ $errstr,
+ $this->options['timeout_connect'],
+ STREAM_CLIENT_CONNECT,
+ $this->getStreamContext()
+ );
+
+ if (!$this->socket) {
+ throw new \Wrench\Exception\ConnectionException(sprintf(
+ 'Could not connect to socket: %s (%d)',
+ $errstr,
+ $errno
+ ));
+ }
+
+ stream_set_timeout($this->socket, $this->options['timeout_socket']);
+
+ return ($this->connected = true);
+ }
+
+ public function reconnect()
+ {
+ $this->disconnect();
+ $this->connect();
+ }
+
+ /**
+ * @see Wrench\Socket.UriSocket::getSocketStreamContextOptions()
+ */
+ protected function getSocketStreamContextOptions()
+ {
+ $options = array();
+ return $options;
+ }
+
+ /**
+ * @see Wrench\Socket.UriSocket::getSslStreamContextOptions()
+ */
+ protected function getSslStreamContextOptions()
+ {
+ $options = array();
+
+ if ($this->options['ssl_verify_peer']) {
+ $options['verify_peer'] = true;
+ }
+
+ if ($this->options['ssl_allow_self_signed']) {
+ $options['allow_self_signed'] = true;
+ }
+
+ return $options;
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Socket/ServerClientSocket.php b/listeners/lib/Wrench/Socket/ServerClientSocket.php
new file mode 100644
index 000000000..61583f6cc
--- /dev/null
+++ b/listeners/lib/Wrench/Socket/ServerClientSocket.php
@@ -0,0 +1,25 @@
+connect() or whatnot.
+ *
+ * @param resource $accepted_socket
+ * @param array $options
+ */
+ public function __construct($accepted_socket, array $options = array())
+ {
+ parent::__construct($options);
+
+ $this->socket = $accepted_socket;
+ $this->connected = (boolean)$accepted_socket;
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Socket/ServerSocket.php b/listeners/lib/Wrench/Socket/ServerSocket.php
new file mode 100644
index 000000000..facb89f86
--- /dev/null
+++ b/listeners/lib/Wrench/Socket/ServerSocket.php
@@ -0,0 +1,126 @@
+ int, used to limit the number of outstanding
+ * connections in the socket's listen queue
+ * - ssl_cert_file => string, server SSL certificate
+ * file location. File should contain
+ * certificate and private key
+ * - ssl_passphrase => string, passphrase for the key
+ * - timeout_accept => int, seconds, default 5
+ */
+ protected function configure(array $options)
+ {
+ $options = array_merge(array(
+ 'backlog' => 50,
+ 'ssl_cert_file' => null,
+ 'ssl_passphrase' => null,
+ 'ssl_allow_self_signed' => false,
+ 'timeout_accept' => self::TIMEOUT_ACCEPT
+ ), $options);
+
+ parent::configure($options);
+ }
+
+ /**
+ * Listens
+ *
+ * @throws ConnectionException
+ */
+ public function listen()
+ {
+ $this->socket = stream_socket_server(
+ $this->getUri(),
+ $errno,
+ $errstr,
+ STREAM_SERVER_BIND|STREAM_SERVER_LISTEN.
+ $this->getStreamContext()
+ );
+
+ if (!$this->socket) {
+ throw new ConnectionException(sprintf(
+ 'Could not listen on socket: %s (%d)',
+ $errstr,
+ $errno
+ ));
+ }
+
+ $this->listening = true;
+ }
+
+ /**
+ * Accepts a new connection on the socket
+ *
+ * @throws ConnectionException
+ * @return resource
+ */
+ public function accept()
+ {
+ $new = stream_socket_accept(
+ $this->socket,
+ $this->options['timeout_accept']
+ );
+
+ if (!$new) {
+ throw new ConnectionException(socket_strerror(socket_last_error($new)));
+ }
+
+ return $new;
+ }
+
+ /**
+ * @see Wrench\Socket.UriSocket::getSocketStreamContextOptions()
+ */
+ protected function getSocketStreamContextOptions()
+ {
+ $options = array();
+
+ if ($this->options['backlog']) {
+ $options['backlog'] = $this->options['backlog'];
+ }
+
+ return $options;
+ }
+
+ /**
+ * @see Wrench\Socket.UriSocket::getSslStreamContextOptions()
+ */
+ protected function getSslStreamContextOptions()
+ {
+ $options = array();
+
+ if ($this->options['server_ssl_cert_file']) {
+ $options['local_cert'] = $this->options['server_ssl_cert_file'];
+ if ($this->options['server_ssl_passphrase']) {
+ $options['passphrase'] = $this->options['server_ssl_passphrase'];
+ }
+ }
+
+ return $options;
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Socket/Socket.php b/listeners/lib/Wrench/Socket/Socket.php
new file mode 100644
index 000000000..97f6b28cc
--- /dev/null
+++ b/listeners/lib/Wrench/Socket/Socket.php
@@ -0,0 +1,322 @@
+ int, seconds, default 2
+ * - timeout_socket => int, seconds, default 5
+ *
+ * @param array $options
+ * @return void
+ */
+ protected function configure(array $options)
+ {
+ $options = array_merge(array(
+ 'timeout_socket' => self::TIMEOUT_SOCKET,
+ ), $options);
+
+ parent::configure($options);
+ }
+
+ /**
+ * Gets the name of the socket
+ */
+ protected function getName()
+ {
+ if (!isset($this->name) || !$this->name) {
+ $this->name = @stream_socket_get_name($this->socket, true);
+ }
+ return $this->name;
+ }
+
+ /**
+ * Gets part of the name of the socket
+ *
+ * PHP seems to return IPV6 address/port combos like this:
+ * ::1:1234, where ::1 is the address and 1234 the port
+ * So, the part number here is either the last : delimited section (the port)
+ * or all the other sections (the whole initial part, the address).
+ *
+ * @param string $name (from $this->getName() usually)
+ * @param int<0, 1> $part
+ * @return string
+ * @throws SocketException
+ */
+ public static function getNamePart($name, $part)
+ {
+ if (!$name) {
+ throw new InvalidArgumentException('Invalid name');
+ }
+
+ $parts = explode(':', $name);
+
+ if (count($parts) < 2) {
+ throw new SocketException('Could not parse name parts: ' . $name);
+ }
+
+ if ($part == self::NAME_PART_PORT) {
+ return end($parts);
+ } elseif ($part == self::NAME_PART_IP) {
+ return implode(':', array_slice($parts, 0, -1));
+ } else {
+ throw new InvalidArgumentException('Invalid name part');
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the IP address of the socket
+ *
+ * @return string
+ */
+ public function getIp()
+ {
+ $name = $this->getName();
+
+ if ($name) {
+ return self::getNamePart($name, self::NAME_PART_IP);
+ } else {
+ throw new SocketException('Cannot get socket IP address');
+ }
+ }
+
+ /**
+ * Gets the port of the socket
+ *
+ * @return int
+ */
+ public function getPort()
+ {
+ $name = $this->getName();
+
+ if ($name) {
+ return self::getNamePart($name, self::NAME_PART_PORT);
+ } else {
+ throw new SocketException('Cannot get socket IP address');
+ }
+ }
+
+ /**
+ * Get the last error that occurred on the socket
+ *
+ * @return int|string
+ */
+ public function getLastError()
+ {
+ if ($this->isConnected() && $this->socket) {
+ $err = @socket_last_error($this->socket);
+ if ($err) {
+ $err = socket_strerror($err);
+ }
+ if (!$err) {
+ $err = 'Unknown error';
+ }
+ return $err;
+ } else {
+ return 'Not connected';
+ }
+ }
+
+ /**
+ * Whether the socket is currently connected
+ *
+ * @return boolean
+ */
+ public function isConnected()
+ {
+ return $this->connected;
+ }
+
+ /**
+ * Disconnect the socket
+ *
+ * @return void
+ */
+ public function disconnect()
+ {
+ if ($this->socket) {
+ stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
+ }
+ $this->socket = null;
+ $this->connected = false;
+ }
+
+ /**
+ * @see Wrench.Resource::getResource()
+ */
+ public function getResource()
+ {
+ return $this->socket;
+ }
+
+ /**
+ * @see Wrench.Resource::getResourceId()
+ */
+ public function getResourceId()
+ {
+ return (int)$this->socket;
+ }
+
+ /**
+ * @param unknown_type $data
+ * @throws SocketException
+ * @return boolean|int The number of bytes sent or false on error
+ */
+ public function send($data)
+ {
+ if (!$this->isConnected()) {
+ throw new SocketException('Socket is not connected');
+ }
+
+ $length = strlen($data);
+
+ if ($length == 0) {
+ return 0;
+ }
+
+ for ($i = $length; $i > 0; $i -= $written) {
+ $written = @fwrite($this->socket, substr($data, -1 * $i));
+
+ if ($written === false) {
+ return false;
+ } elseif ($written === 0) {
+ return false;
+ }
+ }
+
+ return $length;
+ }
+
+ /**
+ * Receive data from the socket
+ *
+ * @param int $length
+ * @return string
+ */
+ public function receive($length = self::DEFAULT_RECEIVE_LENGTH)
+ {
+ $remaining = $length;
+
+ $buffer = '';
+ $metadata['unread_bytes'] = 0;
+
+ do {
+ if (feof($this->socket)) {
+ return $buffer;
+ }
+
+ $result = fread($this->socket, $length);
+
+ if ($result === false) {
+ return $buffer;
+ }
+
+ $buffer .= $result;
+
+ if (feof($this->socket)) {
+ return $buffer;
+ }
+
+ $continue = false;
+
+ if ($this->firstRead == true && strlen($result) == 1) {
+ // Workaround Chrome behavior (still needed?)
+ $continue = true;
+ }
+ $this->firstRead = false;
+
+ if (strlen($result) == $length) {
+ $continue = true;
+ }
+
+ // Continue if more data to be read
+ $metadata = stream_get_meta_data($this->socket);
+ if ($metadata && isset($metadata['unread_bytes']) && $metadata['unread_bytes']) {
+ $continue = true;
+ $length = $metadata['unread_bytes'];
+ }
+ } while ($continue);
+
+ return $buffer;
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Socket/UriSocket.php b/listeners/lib/Wrench/Socket/UriSocket.php
new file mode 100644
index 000000000..8e1cbc047
--- /dev/null
+++ b/listeners/lib/Wrench/Socket/UriSocket.php
@@ -0,0 +1,118 @@
+ Wrench\Protocol object, latest protocol
+ * version used if not specified
+ * - timeout_socket => int, seconds, default 5
+ * - server_ssl_cert_file => string, server SSL certificate
+ * file location. File should contain
+ * certificate and private key
+ * - server_ssl_passphrase => string, passphrase for the key
+ * - server_ssl_allow_self_signed => boolean, whether to allows self-
+ * signed certs
+ */
+ public function __construct($uri, array $options = array())
+ {
+ parent::__construct($options);
+
+ list($this->scheme, $this->host, $this->port)
+ = $this->protocol->validateSocketUri($uri);
+ }
+
+ /**
+ * Gets the canonical/normalized URI for this socket
+ *
+ * @return string
+ */
+ protected function getUri()
+ {
+ return sprintf(
+ '%s://%s:%d',
+ $this->scheme,
+ $this->host,
+ $this->port
+ );
+ }
+
+ /**
+ * @todo DNS lookup? Override getIp()?
+ * @see Wrench\Socket.Socket::getName()
+ */
+ protected function getName()
+ {
+ return sprintf('%s:%s', $this->host, $this->port);
+ }
+
+ /**
+ * Gets the host name
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * @see Wrench\Socket.Socket::getPort()
+ */
+ public function getPort()
+ {
+ return $this->port;
+ }
+
+ /**
+ * Gets a stream context
+ */
+ protected function getStreamContext($listen = false)
+ {
+ $options = array();
+
+ if ($this->scheme == Protocol::SCHEME_UNDERLYING_SECURE
+ || $this->scheme == Protocol::SCHEME_UNDERLYING) {
+ $options['socket'] = $this->getSocketStreamContextOptions();
+ }
+
+ if ($this->scheme == Protocol::SCHEME_UNDERLYING_SECURE) {
+ $options['ssl'] = $this->getSslStreamContextOptions();
+ }
+
+ return stream_context_create(
+ $options,
+ array()
+ );
+ }
+
+ /**
+ * Returns an array of socket stream context options
+ *
+ * See http://php.net/manual/en/context.socket.php
+ *
+ * @return array
+ */
+ abstract protected function getSocketStreamContextOptions();
+
+ /**
+ * Returns an array of ssl stream context options
+ *
+ * See http://php.net/manual/en/context.ssl.php
+ *
+ * @return array
+ */
+ abstract protected function getSslStreamContextOptions();
+}
diff --git a/listeners/lib/Wrench/Tests/Application/EchoApplicationTest.php b/listeners/lib/Wrench/Tests/Application/EchoApplicationTest.php
new file mode 100644
index 000000000..bd32aeb33
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Application/EchoApplicationTest.php
@@ -0,0 +1,57 @@
+assertInstanceOfClass($this->getInstance());
+ }
+
+ /**
+ * @param unknown_type $payload
+ * @dataProvider getValidPayloads
+ */
+ public function testOnData($payload)
+ {
+ $connection = $this->getMockBuilder('Wrench\Connection')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $connection
+ ->expects($this->once())
+ ->method('send')
+ ->with($this->equalTo($payload), $this->equalTo(Protocol::TYPE_TEXT))
+ ->will($this->returnValue(true));
+
+ $this->getInstance()->onData($payload, $connection);
+ }
+
+ /**
+ * Data provider
+ *
+ * @return array>
+ */
+ public function getValidPayloads()
+ {
+ return array(
+ array('asdkllakdaowidoaw noaoinosdna nwodinado ndsnd aklndiownd'),
+ array(' ')
+ );
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/BasicServerTest.php b/listeners/lib/Wrench/Tests/BasicServerTest.php
new file mode 100644
index 000000000..20d004419
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/BasicServerTest.php
@@ -0,0 +1,119 @@
+getInstance('ws://localhost:8000', array(
+ 'allowed_origins' => $allowed,
+ 'logger' => array($this, 'log')
+ ));
+
+ $connection = $this->getMockBuilder('Wrench\Connection')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $connection
+ ->expects($this->never())
+ ->method('close')
+ ->will($this->returnValue(true));
+
+ $server->notify(
+ Server::EVENT_HANDSHAKE_REQUEST,
+ array($connection, '', $origin, '', array())
+ );
+ }
+
+ /**
+ * @param array $allowed
+ * @param string $origin
+ * @dataProvider getInvalidOrigins
+ */
+ public function testInvalidOriginPolicy(array $allowed, $origin)
+ {
+ $server = $this->getInstance('ws://localhost:8000', array(
+ 'allowed_origins' => $allowed,
+ 'logger' => array($this, 'log')
+ ));
+
+ $connection = $this->getMockBuilder('Wrench\Connection')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $connection
+ ->expects($this->once())
+ ->method('close')
+ ->will($this->returnValue(true));
+
+ $server->notify(
+ Server::EVENT_HANDSHAKE_REQUEST,
+ array($connection, '', $origin, '', array())
+ );
+ }
+
+ /**
+ * @see Wrench\Tests.ServerTest::getValidConstructorArguments()
+ */
+ public function getValidConstructorArguments()
+ {
+ return array_merge(parent::getValidConstructorArguments(), array(
+ array(
+ 'ws://localhost:8000',
+ array('logger' => function () {})
+ )
+ ));
+ }
+
+ /**
+ * Data provider
+ *
+ * @return array>
+ */
+ public function getValidOrigins()
+ {
+ return array(
+ array(array('localhost'), 'localhost'),
+ array(array('somewhere.com'), 'somewhere.com'),
+ );
+ }
+
+ /**
+ * Data provider
+ *
+ * @return array>
+ */
+ public function getInvalidOrigins()
+ {
+ return array(
+ array(array('localhost'), 'blah'),
+ array(array('somewhere.com'), 'somewhereelse.com'),
+ array(array('somewhere.com'), 'subdomain.somewhere.com')
+ );
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/ClientTest.php b/listeners/lib/Wrench/Tests/ClientTest.php
new file mode 100644
index 000000000..824774eef
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/ClientTest.php
@@ -0,0 +1,173 @@
+assertInstanceOfClass(
+ $client = new Client(
+ 'ws://localhost/test', 'http://example.org/'
+ ),
+ 'ws:// scheme, default socket'
+ );
+
+ $this->assertInstanceOfClass(
+ $client = new Client(
+ 'ws://localhost/test', 'http://example.org/',
+ array('socket' => $this->getMockSocket())
+ ),
+ 'ws:// scheme, socket specified'
+ );
+ }
+
+ /**
+ * Gets a mock socket
+ *
+ * @return Socket
+ */
+ protected function getMockSocket()
+ {
+ return $this->getMock('Wrench\Socket\ClientSocket', array(), array('wss://localhost:8000'));
+ }
+
+ /**
+ * @expectedException PHPUnit_Framework_Error
+ */
+ public function testConstructorSocketUnspecified()
+ {
+ $w = new Client();
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testConstructorUriInvalid()
+ {
+ $w = new Client('invalid uri', 'http://www.example.com/');
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testConstructorUriEmpty()
+ {
+ $w = new Client(null, 'http://www.example.com/');
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testConstructorUriPathUnspecified()
+ {
+ $w = new Client('ws://localhost', 'http://www.example.com/');
+ }
+
+ /**
+ * @expectedException PHPUnit_Framework_Error
+ */
+ public function testConstructorOriginUnspecified()
+ {
+ $w = new Client('ws://localhost');
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testConstructorOriginEmpty()
+ {
+ $w = new Client('wss://localhost', null);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testConstructorOriginInvalid()
+ {
+ $w = new Client('ws://localhost:8000', 'NOTAVALIDURI');
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testSendInvalidType()
+ {
+ $client = new Client('ws://localhost/test', 'http://example.org/');
+ $client->sendData('blah', 9999);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testSendInvalidTypeString()
+ {
+ $client = new Client('ws://localhost/test', 'http://example.org/');
+ $client->sendData('blah', 'fooey');
+ }
+
+ public function testSend()
+ {
+ try {
+ $helper = new ServerTestHelper();
+ $helper->setUp();
+
+ /* @var $instance Wrench\Client */
+ $instance = $this->getInstance($helper->getEchoConnectionString(), 'http://www.example.com/send');
+ $instance->addRequestHeader('X-Test', 'Custom Request Header');
+
+ $this->assertFalse($instance->receive(), 'Receive before connect');
+
+ $success = $instance->connect();
+ $this->assertTrue($success, 'Client can connect to test server');
+ $this->assertTrue($instance->isConnected());
+
+ $this->assertFalse($instance->connect(), 'Double connect');
+
+ $this->assertFalse((boolean)$instance->receive(), 'No data');
+
+ $bytes = $instance->sendData('foobar', 'text');
+ $this->assertTrue($bytes >= 6, 'sent text frame');
+ sleep(1);
+
+ $bytes = $instance->sendData('baz', Protocol::TYPE_TEXT);
+ $this->assertTrue($bytes >= 3, 'sent text frame');
+ sleep(1);
+
+ $responses = $instance->receive();
+ $this->assertTrue(is_array($responses));
+ $this->assertCount(2, $responses);
+ $this->assertInstanceOf('Wrench\\Payload\\Payload', $responses[0]);
+ $this->assertInstanceOf('Wrench\\Payload\\Payload', $responses[1]);
+
+ $instance->disconnect();
+
+ $this->assertFalse($instance->isConnected());
+ } catch (\Exception $e) {
+ $helper->tearDown();
+ throw $e;
+ }
+
+ $helper->tearDown();
+ }
+}
diff --git a/listeners/lib/Wrench/Tests/ConnectionManagerTest.php b/listeners/lib/Wrench/Tests/ConnectionManagerTest.php
new file mode 100644
index 000000000..ed3de0b03
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/ConnectionManagerTest.php
@@ -0,0 +1,101 @@
+assertInstanceOfClass(
+ $instance = $this->getInstance(
+ $server,
+ $options
+ ),
+ 'Valid constructor arguments'
+ );
+ }
+
+ /**
+ * Tests the constructor
+ */
+ public function testConstructor()
+ {
+ $this->assertInstanceOfClass(
+ $instance = $this->getInstance(
+ $this->getMockServer(),
+ array()
+ ),
+ 'Constructor'
+ );
+ return $instance;
+ }
+
+ /**
+ * @depends testConstructor
+ * @param ConnectionManager $instance
+ */
+ public function testCount($instance)
+ {
+ $this->assertTrue(is_numeric($instance->count()));
+ }
+
+ /**
+ * Data provider
+ */
+ public function getValidConstructorArguments()
+ {
+ return array(
+ array($this->getMockServer(), array())
+ );
+ }
+
+ /**
+ * Gets a mock server
+ */
+ protected function getMockServer()
+ {
+ $server = $this->getMock('Wrench\Server', array(), array(), '', false);
+
+ $server->registerApplication('/echo', $this->getMockApplication());
+
+ $server->expects($this->any())
+ ->method('getUri')
+ ->will($this->returnValue('ws://localhost:8000/'));
+
+ return $server;
+ }
+
+ /**
+ * Gets a mock application
+ *
+ * @return EchoApplication
+ */
+ protected function getMockApplication()
+ {
+ return new EchoApplication();
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/ConnectionTest.php b/listeners/lib/Wrench/Tests/ConnectionTest.php
new file mode 100644
index 000000000..25d57ffbc
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/ConnectionTest.php
@@ -0,0 +1,386 @@
+assertInstanceOfClass(
+ $instance = $this->getInstance(
+ $manager,
+ $socket,
+ $options
+ ),
+ 'Valid constructor arguments'
+ );
+
+ return $instance;
+ }
+
+ /**
+ * @dataProvider getValidCloseCodes
+ */
+ public function testClose($code)
+ {
+ $socket = $this->getMockSocket();
+
+ $socket->expects($this->any())
+ ->method('getIp')
+ ->will($this->returnValue('127.0.0.1'));
+
+ $socket->expects($this->any())
+ ->method('getPort')
+ ->will($this->returnValue(mt_rand(1025, 50000)));
+
+ $manager = $this->getMockConnectionManager();
+
+ $connection = $this->getInstance($manager, $socket);
+ $connection->close($code);
+ }
+
+ /**
+ * @dataProvider getValidHandshakeData
+ */
+ public function testHandshake($path, $request)
+ {
+ $connection = $this->getConnectionForHandshake(
+ $this->getConnectedSocket(),
+ $path,
+ $request
+ );
+ $connection->handshake($request);
+ $connection->onData('somedata');
+ $this->assertTrue($connection->send('someotherdata'));
+ return $connection;
+ }
+
+ /**
+ * @dataProvider getValidHandshakeData
+ * @expectedException Wrench\Exception\HandshakeException
+ */
+ public function testHandshakeBadSocket($path, $request)
+ {
+ $connection = $this->getConnectionForHandshake(
+ $this->getNotConnectedSocket(),
+ $path,
+ $request
+ );
+ $connection->handshake($request);
+ }
+
+ /**
+ * Because expectation is that only $path application is available
+ *
+ * @dataProvider getWrongPathHandshakeData
+ * @expectedException PHPUnit_Framework_ExpectationFailedException
+ */
+ public function testWrongPathHandshake($path, $request)
+ {
+ $connection = $this->getConnectionForHandshake(
+ $this->getConnectedSocket(),
+ $path,
+ $request
+ );
+ $connection->handshake($request);
+ }
+
+ /**
+ * @dataProvider getValidHandleData
+ */
+ public function testHandle($path, $request_handshake, array $requests, array $counts)
+ {
+ $connection = $this->getConnectionForHandle(
+ $this->getConnectedSocket(),
+ $path,
+ $request_handshake,
+ $counts
+ );
+
+ $connection->handshake($request_handshake);
+
+ foreach ($requests as $request) {
+ $connection->handle($request);
+ }
+
+ return $connection;
+ }
+
+ /**
+ * @return Socket
+ */
+ protected function getConnectedSocket()
+ {
+ $socket = $this->getMockSocket();
+
+ $socket->expects($this->any())
+ ->method('isConnected')
+ ->will($this->returnValue(true));
+
+ return $socket;
+ }
+
+ /**
+ * @return Socket
+ */
+ protected function getNotConnectedSocket()
+ {
+ $socket = $this->getMockSocket();
+
+ $socket->expects($this->any())
+ ->method('isConnected')
+ ->will($this->returnValue(false));
+
+ return $socket;
+ }
+
+ protected function getConnectionForHandshake($socket, $path, $request)
+ {
+ $manager = $this->getMockConnectionManager();
+
+ $application = $this->getMockApplication();
+
+ $server = $this->getMock('Wrench\Server', array(), array(), '', false);
+ $server->registerApplication($path, $application);
+
+ $manager->expects($this->any())
+ ->method('getApplicationForPath')
+ ->with($path)
+ ->will($this->returnValue($application));
+
+ $manager->expects($this->any())
+ ->method('getServer')
+ ->will($this->returnValue($server));
+
+ $connection = $this->getInstance($manager, $socket);
+
+ return $connection;
+ }
+
+ protected function getConnectionForHandle($socket, $path, $handshake, array $counts)
+ {
+ $connection = $this->getConnectionForHandshake($socket, $path, $handshake);
+
+ $manager = $this->getMockConnectionManager();
+
+ $application = $this->getMockApplication();
+
+ $application->expects($this->exactly(isset($counts['onData']) ? $counts['onData'] : 0))
+ ->method('onData')
+ ->will($this->returnValue(true));
+
+ $server = $this->getMock('Wrench\Server', array(), array(), '', false);
+ $server->registerApplication($path, $application);
+
+ $manager->expects($this->any())
+ ->method('getApplicationForPath')
+ ->with($path)
+ ->will($this->returnValue($application));
+
+ $manager->expects($this->exactly(isset($counts['removeConnection']) ? $counts['removeConnection'] : 0))
+ ->method('removeConnection');
+
+ $manager->expects($this->any())
+ ->method('getServer')
+ ->will($this->returnValue($server));
+
+ $connection = $this->getInstance($manager, $socket);
+
+ return $connection;
+ }
+
+ /**
+ * @return ConnectionManager
+ */
+ protected function getMockConnectionManager()
+ {
+ return $this->getMock('Wrench\ConnectionManager', array(), array(), '', false);
+ }
+
+ /**
+ * Gets a mock socket
+ *
+ * @return Socket
+ */
+ protected function getMockSocket()
+ {
+ return $this->getMock('Wrench\Socket\ServerClientSocket', array(), array(), '', false);
+ }
+
+ /**
+ * Gets a mock application
+ *
+ * @return EchoApplication
+ */
+ protected function getMockApplication()
+ {
+ return $this->getMock('Wrench\Application\EchoApplication');
+ }
+
+ /**
+ * Data provider
+ *
+ * @return array>
+ */
+ public function getValidCloseCodes()
+ {
+ $arguments = array();
+ foreach (Protocol::$closeReasons as $code => $reason) {
+ $arguments[] = array($code);
+ }
+ return $arguments;
+ }
+
+ /**
+ * Data provider
+ *
+ * @return array>
+ */
+ public function getValidConstructorArguments()
+ {
+ $socket = $this->getMockSocket();
+
+ $socket->expects($this->any())
+ ->method('getIp')
+ ->will($this->returnValue('127.0.0.1'));
+
+ $socket->expects($this->any())
+ ->method('getPort')
+ ->will($this->returnValue(mt_rand(1025, 50000)));
+
+ $manager = $this->getMockConnectionManager();
+
+ return array(
+ array(
+ $manager,
+ $socket,
+ array('logger' => function() {})
+ ),
+ array(
+ $manager,
+ $socket,
+ array('logger' => function () {},
+ 'connection_id_algo' => 'sha512')
+ )
+ );
+ }
+
+ /**
+ * Data provider
+ *
+ * Uses this awkward valid request array so that splitting of payloads
+ * across multiple calls to handle can be tested
+ *
+ * testHandle($path, $request_handshake, array $requests, array $counts)
+ */
+ public function getValidHandleData()
+ {
+ $valid_requests = array(
+ array(
+ 'data' => array(
+ "\x81\xad\x2e\xab\x82\xac\x6f\xfe\xd6\xe4\x14\x8b\xf9\x8c\x0c"
+ ."\xde\xf1\xc9\x5c\xc5\xe3\xc1\x4b\x89\xb8\x8c\x0c\xcd\xed\xc3"
+ ."\x0c\x87\xa2\x8e\x5e\xca\xf1\xdf\x59\xc4\xf0\xc8\x0c\x91\xa2"
+ ."\x8e\x4c\xca\xf0\x8e\x53\x81\xad\xd4\xfd\x81\xfe\x95\xa8\xd5"
+ ."\xb6\xee\xdd\xfa\xde\xf6\x88\xf2\x9b\xa6\x93\xe0\x93\xb1\xdf"
+ ."\xbb\xde\xf6\x9b\xee\x91\xf6\xd1\xa1\xdc\xa4\x9c\xf2\x8d\xa3"
+ ."\x92\xf3\x9a\xf6\xc7\xa1\xdc\xb6\x9c\xf3\xdc\xa9\x81\x80\x8e"
+ ."\x12\xcd\x8e\x81\x8c\xf6\x8a\xf0\xee\x9a\xeb\x83\x9a\xd6\xe7"
+ ."\x95\x9d\x85\xeb\x97\x8b" // Four text frames
+ ),
+ 'counts' => array(
+ 'onData' => 4
+ )
+ ),
+ array(
+ 'data' => array(
+ "\x88\x80\xdc\x8e\xa2\xc5" // Close frame
+ ),
+ 'counts' => array(
+ 'removeConnection' => 1
+ )
+ )
+ );
+
+ $data = array();
+
+ $handshakes = $this->getValidHandshakeData();
+
+ foreach ($handshakes as $handshake) {
+ foreach ($valid_requests as $handle_args) {
+ $arguments = $handshake;
+ $arguments[] = $handle_args['data'];
+ $arguments[] = $handle_args['counts'];
+
+ $data[] = $arguments;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Data provider
+ */
+ public function getValidHandshakeData()
+ {
+ return array(
+ array(
+ '/chat',
+"GET /chat HTTP/1.1\r
+Host: server.example.com\r
+Upgrade: websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
+Origin: http://example.com\r
+Sec-WebSocket-Version: 13\r\n\r\n"
+ )
+ );
+ }
+
+ /**
+ * Data provider
+ */
+ public function getWrongPathHandshakeData()
+ {
+ return array(
+ array(
+ '/foobar',
+"GET /chat HTTP/1.1\r
+Host: server.example.com\r
+Upgrade: websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
+Origin: http://example.com\r
+Sec-WebSocket-Version: 13\r\n\r\n"
+ ),
+ );
+ }
+}
diff --git a/listeners/lib/Wrench/Tests/Frame/BaseSubclassFrameTest.php b/listeners/lib/Wrench/Tests/Frame/BaseSubclassFrameTest.php
new file mode 100644
index 000000000..bcced6695
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Frame/BaseSubclassFrameTest.php
@@ -0,0 +1,29 @@
+getFrameBuffer();
+ }
+
+ protected function getClass()
+ {
+ return 'Wrench\Tests\Frame\BadSubclassFrame';
+ }
+}
diff --git a/listeners/lib/Wrench/Tests/Frame/FrameTest.php b/listeners/lib/Wrench/Tests/Frame/FrameTest.php
new file mode 100644
index 000000000..848c7103e
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Frame/FrameTest.php
@@ -0,0 +1,168 @@
+frame = $this->getNewFrame();
+ }
+
+ protected function getNewFrame()
+ {
+ $class = $this->getClass();
+ return new $class();
+ }
+
+ /**
+ * @see PHPUnit_Framework_TestCase::tearDown()
+ */
+ protected function tearDown()
+ {
+ parent::tearDown();
+ unset($this->frame);
+ }
+
+ /**
+ * @param string $payload
+ * @dataProvider getValidEncodePayloads
+ */
+ public function testBijection($type, $payload, $masked)
+ {
+ // Encode the payload
+ $this->frame->encode($payload, $type, $masked);
+
+ // Get the resulting buffer
+ $buffer = $this->frame->getFrameBuffer();
+ $this->assertTrue((boolean)$buffer, 'Got raw frame buffer');
+
+ // And feed it back into a new frame
+ $frame = $this->getNewFrame();
+ $frame->receiveData($buffer);
+
+ // Check the properties of the new frame against the old, all match
+ $this->assertEquals(
+ $this->frame->getType(),
+ $frame->getType(),
+ 'Types match after encode -> receiveData'
+ );
+
+ $this->assertEquals(
+ $this->frame->getFramePayload(),
+ $frame->getFramePayload(),
+ 'Payloads match after encode -> receiveData'
+ );
+
+ // Masking key should not be different, because we read the buffer in directly
+ $this->assertEquals(
+ $this->frame->getFrameBuffer(),
+ $frame->getFrameBuffer(),
+ 'Raw buffers match too'
+ );
+
+ // This time, we create a new frame and read the data in with encode
+ $frame = $this->getNewFrame();
+ $frame->encode($this->frame->getFramePayload(), $type, $masked);
+
+ // These still match
+ $this->assertEquals(
+ $this->frame->getType(),
+ $frame->getType(),
+ 'Types match after encode -> receiveData -> encode'
+ );
+
+ $this->assertEquals(
+ $this->frame->getFramePayload(),
+ $frame->getFramePayload(),
+ 'Payloads match after encode -> receiveData -> encode'
+ );
+
+ // But the masking key should be different, thus, so are the buffers
+ if ($masked) {
+ $this->assertNotEquals(
+ $this->frame->getFrameBuffer(),
+ $frame->getFrameBuffer(),
+ 'Raw buffers don\'t match because of masking'
+ );
+ } else {
+ $this->assertEquals(
+ $this->frame->getFramePayload(),
+ $frame->getFramePayload(),
+ 'Payloads match after encode -> receiveData -> encode'
+ );
+ }
+ }
+
+ /**
+ * @param string $payload
+ * @dataProvider getValidEncodePayloads
+ */
+ public function testEncodeTypeReflection($type, $payload, $masked)
+ {
+ $this->frame->encode($payload, $type);
+ $this->assertEquals(Protocol::TYPE_TEXT, $this->frame->getType(), 'Encode retains type information');
+ }
+
+ /**
+ * @param string $payload
+ * @dataProvider getValidEncodePayloads
+ */
+ public function testEncodeLengthReflection($type, $payload, $masked)
+ {
+ $this->frame->encode($payload, $type);
+ $this->assertEquals(strlen($payload), $this->frame->getLength(), 'Encode does not alter payload length');
+ }
+
+ /**
+ * @param string $payload
+ * @dataProvider getValidEncodePayloads
+ */
+ public function testEncodePayloadReflection($type, $payload, $masked)
+ {
+ $this->frame->encode($payload, $type, $masked);
+ $this->assertEquals($payload, $this->frame->getFramePayload(), 'Encode retains payload information');
+ }
+
+ /**
+ * Data provider
+ *
+ * @return array
+ */
+ public function getValidEncodePayloads()
+ {
+ return array(
+ array(
+ Protocol::TYPE_TEXT,
+ "123456\x007890!@#$%^&*()qwe\trtyuiopQWERTYUIOPasdfghjklASFGH\n
+ JKLzxcvbnmZXCVBNM,./<>?;[]{}-=_+\|'asdad0x11\aasdassasdasasdsd",
+ true
+ ),
+ array(
+ Protocol::TYPE_TEXT,
+ pack('CCCCCCC', 0x00, 0x01, 0x02, 0x03, 0x04, 0xff, 0xf0),
+ true
+ ),
+ array(Protocol::TYPE_TEXT, ' ', true)
+ );
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/Frame/HybiFrameTest.php b/listeners/lib/Wrench/Tests/Frame/HybiFrameTest.php
new file mode 100644
index 000000000..721429513
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Frame/HybiFrameTest.php
@@ -0,0 +1,14 @@
+getMock('Wrench\Server', array(), array(), '', false);
+
+ $instance->listen($server);
+ }
+
+ abstract public function testConstructor();
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/Listener/OriginPolicyTest.php b/listeners/lib/Wrench/Tests/Listener/OriginPolicyTest.php
new file mode 100644
index 000000000..103af9bfe
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Listener/OriginPolicyTest.php
@@ -0,0 +1,110 @@
+getInstance(array());
+ $this->assertInstanceOfClass($instance, 'No constructor arguments');
+ return $instance;
+ }
+
+ /**
+ * @dataProvider getValidArguments
+ * @param array $allowed
+ * @param string $domain
+ */
+ public function testValidAllowed($allowed, $domain)
+ {
+ $instance = $this->getInstance($allowed);
+ $this->assertTrue($instance->isAllowed($domain));
+ }
+
+ /**
+ * @dataProvider getValidArguments
+ * @param array $allowed
+ * @param string $domain
+ */
+ public function testValidHandshake($allowed, $domain)
+ {
+ $instance = $this->getInstance($allowed);
+
+ $connection = $this->getMock('Wrench\Connection', array(), array(), '', false);
+
+ $connection
+ ->expects($this->never())
+ ->method('close');
+
+ $instance->onHandshakeRequest($connection, '/', $domain, 'abc', array());
+ }
+
+ /**
+ * @dataProvider getInvalidArguments
+ * @param array $allowed
+ * @param string $bad_domain
+ */
+ public function testInvalidAllowed($allowed, $bad_domain)
+ {
+ $instance = $this->getInstance($allowed);
+ $this->assertFalse($instance->isAllowed($bad_domain));
+ }
+
+ /**
+ * @dataProvider getInvalidArguments
+ * @param array $allowed
+ * @param string $domain
+ */
+ public function testInvalidHandshake($allowed, $bad_domain)
+ {
+ $instance = $this->getInstance($allowed);
+
+ $connection = $this->getMock('Wrench\Connection', array(), array(), '', false);
+
+ $connection
+ ->expects($this->once())
+ ->method('close');
+
+ $instance->onHandshakeRequest($connection, '/', $bad_domain, 'abc', array());
+ }
+
+ /**
+ * Data provider
+ */
+ public function getValidArguments()
+ {
+ return array(
+ array(array('localhost'), 'http://localhost'),
+ array(array('foobar.com'), 'https://foobar.com'),
+ array(array('https://foobar.com'), 'https://foobar.com')
+ );
+ }
+
+ /**
+ * Data provider
+ */
+ public function getInvalidArguments()
+ {
+ return array(
+ array(array('localhost'), 'localdomain'),
+ array(array('foobar.com'), 'foobar.org'),
+ array(array('https://foobar.com'), 'http://foobar.com'),
+ array(array('http://foobar.com'), 'foobar.com')
+ );
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/Listener/RateLimiterTest.php b/listeners/lib/Wrench/Tests/Listener/RateLimiterTest.php
new file mode 100644
index 000000000..602442308
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Listener/RateLimiterTest.php
@@ -0,0 +1,67 @@
+getInstance();
+ $this->assertInstanceOfClass($instance, 'No constructor arguments');
+ return $instance;
+ }
+
+ public function testOnSocketConnect()
+ {
+ $this->getInstance()->onSocketConnect(null, $this->getConnection());
+ }
+
+ public function testOnSocketDisconnect()
+ {
+ $this->getInstance()->onSocketDisconnect(null, $this->getConnection());
+ }
+
+ public function testOnClientData()
+ {
+ $this->getInstance()->onClientData(null, $this->getConnection());
+ }
+
+ protected function getConnection()
+ {
+ $connection = $this->getMock('Wrench\Connection', array(), array(), '', false);
+
+ $connection
+ ->expects($this->any())
+ ->method('getIp')
+ ->will($this->returnValue('127.0.0.1'));
+
+ $connection
+ ->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue('abcdef01234567890'));
+
+ $manager = $this->getMock('Wrench\ConnectionManager', array(), array(), '', false);
+ $manager->expects($this->any())->method('count')->will($this->returnValue(5));
+
+ $connection
+ ->expects($this->any())
+ ->method('getConnectionManager')
+ ->will($this->returnValue($manager));
+
+ return $connection;
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/Payload/HybiPayloadTest.php b/listeners/lib/Wrench/Tests/Payload/HybiPayloadTest.php
new file mode 100644
index 000000000..5dd2b2497
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Payload/HybiPayloadTest.php
@@ -0,0 +1,14 @@
+payload = $this->getInstance();
+ }
+
+ /**
+ * Tests the constructor
+ */
+ public function testConstructor()
+ {
+ $this->assertInstanceOfClass($this->getInstance());
+ }
+
+ /**
+ * @param string $payload
+ * @dataProvider getValidEncodePayloads
+ */
+ public function testBijection($type, $payload)
+ {
+ // Encode the payload
+ $this->payload->encode($payload, $type);
+
+ // Create a new payload and read the data in with encode
+ $payload = $this->getInstance();
+ $payload->encode($this->payload->getPayload(), $type);
+
+ // These still match
+ $this->assertEquals(
+ $this->payload->getType(),
+ $payload->getType(),
+ 'Types match after encode -> receiveData'
+ );
+
+ $this->assertEquals(
+ $this->payload->getPayload(),
+ $payload->getPayload(),
+ 'Payloads match after encode -> receiveData'
+ );
+ }
+
+ /**
+ * @param string $payload
+ * @dataProvider getValidEncodePayloads
+ */
+ public function testEncodeTypeReflection($type, $payload)
+ {
+ $this->payload->encode($payload, Protocol::TYPE_TEXT);
+ $this->assertEquals(Protocol::TYPE_TEXT, $this->payload->getType(), 'Encode retains type information');
+ }
+
+ /**
+ * @param string $payload
+ * @dataProvider getValidEncodePayloads
+ */
+ public function testEncodePayloadReflection($type, $payload)
+ {
+ $this->payload->encode($payload, Protocol::TYPE_TEXT);
+ $this->assertEquals($payload, $this->payload->getPayload(), 'Encode retains payload information');
+ }
+
+ /**
+ * Tests sending to a socket
+ * @dataProvider getValidEncodePayloads
+ */
+ public function testSendToSocket($type, $payload)
+ {
+ $successfulSocket = $this->getMock('Wrench\Socket\ClientSocket', array(), array('wss://localhost:8000'));
+ $failedSocket = clone $successfulSocket;
+
+ $successfulSocket->expects($this->any())
+ ->method('send')
+ ->will($this->returnValue(true));
+
+ $failedSocket->expects($this->any())
+ ->method('send')
+ ->will($this->returnValue(false));
+
+ $this->payload->encode($payload, $type);
+
+ $this->assertTrue($this->payload->sendToSocket($successfulSocket));
+ $this->assertFalse($this->payload->sendToSocket($failedSocket));
+ }
+
+ /**
+ * Tests receiving data
+ * @dataProvider getValidEncodePayloads
+ */
+ public function testReceieveData($type, $payload)
+ {
+ $payload = $this->getInstance();
+ $payload->receiveData($payload);
+ }
+
+ /**
+ * Data provider
+ *
+ * @return array
+ */
+ public function getValidEncodePayloads()
+ {
+ return array(
+ array(
+ Protocol::TYPE_TEXT,
+ "123456\x007890!@#$%^&*()qwe\trtyuiopQWERTYUIOPasdfghjklASFGH\n
+ JKLzxcvbnmZXCVBNM,./<>?;[]{}-=_+\|'asdad0x11\aasdassasdasasdsd"
+ ),
+ array(
+ Protocol::TYPE_TEXT,
+ pack('CCCCCCC', 0x00, 0x01, 0x02, 0x03, 0x04, 0xff, 0xf0)
+ ),
+ array(Protocol::TYPE_TEXT, ' ')
+ );
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/Protocol/ProtocolTest.php b/listeners/lib/Wrench/Tests/Protocol/ProtocolTest.php
new file mode 100644
index 000000000..afba179da
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Protocol/ProtocolTest.php
@@ -0,0 +1,180 @@
+getInstance()->validateRequestHandshake($request);
+
+ $this->assertEquals('/chat', $path);
+ $this->assertEquals('http://example.com', $origin);
+ $this->assertEquals('dGhlIHNhbXBsZSBub25jZQ==', $key);
+ $this->assertTrue(is_array($extensions), 'Extensions returned as array');
+ $this->assertEquals(array('x-test', 'x-test2'), $extensions, 'Extensions match');
+ $this->assertEquals('chat, superchat', $protocol);
+ } catch (Exception $e) {
+ $this->fail($e);
+ }
+ }
+
+ /**
+ * @dataProvider getValidHandshakeResponses
+ */
+ public function testValidateHandshakeResponseValid($response, $key)
+ {
+ try {
+ $valid = $this->getInstance()->validateResponseHandshake($response, $key);
+ $this->assertTrue(is_bool($valid), 'Validation return value is boolean');
+ $this->assertTrue($valid, 'Handshake response validates');
+ } catch (Exception $e) {
+ $this->fail('Validated valid response handshake as invalid');
+ }
+ }
+
+ /**
+ * @dataProvider getValidHandshakeResponses
+ */
+ public function testGetResponseHandsake($unused, $key)
+ {
+ try {
+ $response = $this->getInstance()->getResponseHandshake($key);
+ $this->assertHttpResponse($response);
+ } catch (Exception $e) {
+ $this->fail('Unable to get handshake response: ' . $e);
+ }
+ }
+
+ /**
+ * Asserts the string response is an HTTP response
+ *
+ * @param string $response
+ */
+ protected function assertHttpResponse($response, $message = '')
+ {
+ $this->assertStringStartsWith('HTTP', $response, $message . ' - response starts well');
+ $this->assertStringEndsWith("\r\n", $response, $message . ' - response ends well');
+ }
+
+ public function testGetVersion()
+ {
+ $version = $this->getInstance()->getVersion();
+ $this->assertTrue(is_int($version));
+ }
+
+ public function testGetResponseError()
+ {
+ $response = $this->getInstance()->getResponseError(400);
+ $this->assertHttpResponse($response, 'Code as int');
+
+ $response = $this->getInstance()->getResponseError(new Exception('Some message', 500));
+ $this->assertHttpResponse($response, 'Code in Exception');
+
+ $response = $this->getInstance()->getResponseError(888);
+ $this->assertHttpResponse($response, 'Invalid code produces unimplemented response');
+ }
+
+ /**
+ * @dataProvider getValidOriginUris
+ */
+ public function testValidateOriginUriValid($uri)
+ {
+ try {
+ $this->getInstance()->validateOriginUri($uri);
+ } catch (\Exception $e) {
+ $this->fail('Valid URI validated as invalid: ' . $e);
+ }
+ }
+
+ /**
+ * @dataProvider getInvalidOriginUris
+ * @expectedException InvalidArgumentException
+ */
+ public function testValidateOriginUriInvalid($uri)
+ {
+ $this->getInstance()->validateOriginUri($uri);
+ }
+
+ public function getValidOriginUris()
+ {
+ return array(
+ array('http://www.example.org'),
+ array('http://www.example.com/some/page'),
+ array('https://localhost/')
+ );
+ }
+
+ public function getInvalidOriginUris()
+ {
+ return array(
+ array(false),
+ array(true),
+ array(''),
+ array('blah')
+ );
+ }
+
+ public function getValidHandshakeRequests()
+ {
+ $cases = array();
+
+
+ $cases[] = array("GET /chat HTTP/1.1\r
+Host: server.example.com\r
+Upgrade: websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
+Origin: http://example.com\r
+Sec-WebSocket-Extensions: x-test\r
+Sec-WebSocket-Extensions: x-test2\r
+Sec-WebSocket-Protocol: chat, superchat\r
+Sec-WebSocket-Version: 13\r
+\r\n");
+
+ $cases[] = array("GET /chat HTTP/1.1\r
+Host: server.example.com\r
+Upgrade: Websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
+Origin: http://example.com\r
+Sec-WebSocket-Extensions: x-test\r
+Sec-WebSocket-Extensions: x-test2\r
+Sec-WebSocket-Protocol: chat, superchat\r
+Sec-WebSocket-Version: 13\r
+\r\n");
+
+ return $cases;
+ }
+
+ public function getValidHandshakeResponses()
+ {
+ $cases = array();
+
+ for ($i = 10; $i > 0; $i--) {
+ $key = sha1(time() . uniqid('', true));
+ $response = "Sec-WebSocket-Accept: "
+ . base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true))
+ . "\r\n\r\n";
+
+ $cases[] = array($response, $key);
+ }
+
+ return $cases;
+ }
+}
diff --git a/listeners/lib/Wrench/Tests/Protocol/Rfc6455ProtocolTest.php b/listeners/lib/Wrench/Tests/Protocol/Rfc6455ProtocolTest.php
new file mode 100644
index 000000000..ee329f31e
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Protocol/Rfc6455ProtocolTest.php
@@ -0,0 +1,14 @@
+assertInstanceOfClass(
+ $this->getInstance($url, $options),
+ 'Valid constructor arguments'
+ );
+ }
+
+ /**
+ * Tests logging
+ */
+ public function testLogging()
+ {
+ $test = $this;
+ $logged = false;
+
+ $server = $this->getInstance('ws://localhost:8000', array(
+ 'logger' => function ($message, $priority) use ($test, &$logged) {
+ $test->assertTrue(is_string($message), 'Log had a string message');
+ $test->assertTrue(is_string($priority), 'Log had a string priority');
+ $logged = true;
+ }
+ ));
+
+ $this->assertTrue($logged, 'The log callback was hit');
+ }
+
+ /**
+ * Data provider
+ *
+ * @return array>
+ */
+ public function getValidConstructorArguments()
+ {
+ return array(
+ array(
+ 'ws://localhost:8000',
+ array('logger' => array($this, 'log'))
+ ),
+ array(
+ 'ws://localhost',
+ array('logger' => array($this, 'log'))
+ )
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/ServerTestHelper.php b/listeners/lib/Wrench/Tests/ServerTestHelper.php
new file mode 100644
index 000000000..57dd1ec05
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/ServerTestHelper.php
@@ -0,0 +1,144 @@
+tearDown();
+ }
+
+ /**
+ * @return string
+ */
+ public function getConnectionString()
+ {
+ return 'ws://localhost:' . $this->port;
+ }
+
+ /**
+ * @return string
+ */
+ public function getEchoConnectionString()
+ {
+ return $this->getConnectionString() . '/echo';
+ }
+
+ /**
+ * Sets up the server process and sleeps for a few seconds while
+ * it wakes up
+ */
+ public function setUp()
+ {
+ $this->port = self::getNextPort();
+
+ $this->process = proc_open(
+ $this->getCommand(),
+ array(
+ 0 => array('file', '/dev/null', 'r'),
+ 1 => array('file', __DIR__ . '/../../../build/server.log', 'a+'),
+ 2 => array('file', __DIR__ . '/../../../build/server.err.log', 'a+')
+ ),
+ $this->pipes,
+ __DIR__ . '../'
+ );
+
+ sleep(3);
+ }
+
+ /**
+ * Tears down the server process
+ *
+ * This method *must* be called
+ */
+ public function tearDown()
+ {
+ if ($this->process) {
+ foreach ($this->pipes as &$pipe) {
+ fclose($pipe);
+ }
+ $this->pipes = null;
+
+ // Sigh
+ $status = proc_get_status($this->process);
+
+ if ($status && isset($status['pid']) && $status['pid']) {
+ // More sigh, this is the pid of the parent sh process, we want
+ // to terminate the server directly
+ $this->log('Command: /bin/ps -ao pid,ppid | /usr/bin/col | /usr/bin/tail -n +2 | /bin/grep \' ' . $status['pid'] . "'", 'info');
+ exec('/bin/ps -ao pid,ppid | /usr/bin/col | /usr/bin/tail -n +2 | /bin/grep \' ' . $status['pid'] . "'", $processes, $return);
+
+ if ($return === 0) {
+ foreach ($processes as $process) {
+ list($pid, $ppid) = explode(' ', str_replace(' ', ' ', $process));
+ if ($pid) {
+ $this->log('Killing ' . $pid, 'info');
+ exec('/bin/kill ' . $pid . ' > /dev/null 2>&1');
+ }
+ }
+ } else {
+ $this->log('Unable to find child processes', 'warning');
+ }
+
+ sleep(1);
+
+ $this->log('Killing ' . $status['pid'], 'info');
+ exec('/bin/kill ' . $status['pid'] . ' > /dev/null 2>&1');
+
+ sleep(1);
+ }
+
+ proc_close($this->process);
+ $this->process = null;
+ }
+ }
+
+ /**
+ * Gets the server command
+ *
+ * @return string
+ */
+ protected function getCommand()
+ {
+ return sprintf('/usr/bin/env php %s/server.php %d', __DIR__, $this->port);
+ }
+
+ /**
+ * Logs a message
+ *
+ * @param string $message
+ * @param string $priority
+ */
+ public function log($message, $priority = 'info')
+ {
+ //echo $message . "\n";
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/Socket/ClientSocketTest.php b/listeners/lib/Wrench/Tests/Socket/ClientSocketTest.php
new file mode 100644
index 000000000..d64ea5df2
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Socket/ClientSocketTest.php
@@ -0,0 +1,167 @@
+assertInstanceOfClass(
+ new ClientSocket('ws://localhost/'),
+ 'ws:// scheme, default port'
+ );
+
+ $this->assertInstanceOfClass(
+ new ClientSocket('ws://localhost/some-arbitrary-path'),
+ 'with path'
+ );
+
+ $this->assertInstanceOfClass(
+ new ClientSocket('wss://localhost/test', array()),
+ 'empty options'
+ );
+
+ $this->assertInstanceOfClass(
+ new ClientSocket('ws://localhost:8000/foo'),
+ 'specified port'
+ );
+
+ return $instance;
+ }
+
+ public function testOptions()
+ {
+ $socket = null;
+
+ $this->assertInstanceOfClass(
+ $socket = new ClientSocket(
+ 'ws://localhost:8000/foo', array(
+ 'timeout_connect' => 10
+ )
+ ),
+ 'connect timeout'
+ );
+
+ $this->assertInstanceOfClass(
+ $socket = new ClientSocket(
+ 'ws://localhost:8000/foo', array(
+ 'timeout_socket' => 10
+ )
+ ),
+ 'socket timeout'
+ );
+
+ $this->assertInstanceOfClass(
+ $socket = new ClientSocket(
+ 'ws://localhost:8000/foo', array(
+ 'protocol' => new Rfc6455Protocol()
+ )
+ ),
+ 'protocol'
+ );
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testProtocolTypeError()
+ {
+ $socket = new ClientSocket(
+ 'ws://localhost:8000/foo', array(
+ 'protocol' => new stdClass()
+ )
+ );
+ }
+
+ /**
+ * @expectedException PHPUnit_Framework_Error
+ */
+ public function testConstructorUriUnspecified()
+ {
+ $w = new ClientSocket();
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testConstructorUriEmpty()
+ {
+ $w = new ClientSocket(null);
+ }
+
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testConstructorUriInvalid()
+ {
+ $w = new ClientSocket('Bad argument');
+ }
+
+
+ /**
+ * @depends testConstructor
+ * @expectedException Wrench\Exception\SocketException
+ */
+ public function testSendTooEarly($instance)
+ {
+ $instance->send('foo');
+ }
+
+ /**
+ * Test the connect, send, receive method
+ */
+ public function testConnect()
+ {
+ try {
+ $helper = new ServerTestHelper();
+ $helper->setUp();
+
+ $instance = $this->getInstance($helper->getConnectionString());
+ $success = $instance->connect();
+
+ $this->assertTrue($success, 'Client socket can connect to test server');
+
+ $sent = $instance->send("GET /echo HTTP/1.1\r
+Host: localhost\r
+Upgrade: websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
+Origin: http://localhost\r
+Sec-WebSocket-Version: 13\r\n\r\n");
+ $this->assertNotEquals(false, $sent, 'Client socket can send to test server');
+
+ $response = $instance->receive();
+ $this->assertStringStartsWith('HTTP', $response, 'Response looks like HTTP handshake response');
+
+ } catch (\Exception $e) {
+ $helper->tearDown();
+ throw $e;
+ }
+
+ $helper->tearDown();
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/Socket/ServerClientSocketTest.php b/listeners/lib/Wrench/Tests/Socket/ServerClientSocketTest.php
new file mode 100644
index 000000000..ca71983b5
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Socket/ServerClientSocketTest.php
@@ -0,0 +1,42 @@
+getInstance($resource);
+ $this->assertInstanceOfClass($instance);
+ return $instance;
+ }
+
+ /**
+ * @expectedException Wrench\Exception\SocketException
+ * @depends testConstructor
+ */
+ public function testGetIpTooSoon($instance)
+ {
+ $instance->getIp();
+ }
+
+ /**
+ * @expectedException Wrench\Exception\SocketException
+ * @depends testConstructor
+ */
+ public function testGetPortTooSoon($instance)
+ {
+ $instance->getPort();
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/Socket/ServerSocketTest.php b/listeners/lib/Wrench/Tests/Socket/ServerSocketTest.php
new file mode 100644
index 000000000..a4d4251c5
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Socket/ServerSocketTest.php
@@ -0,0 +1,13 @@
+isConnected();
+ $this->assertTrue(is_bool($connected), 'isConnected returns boolean');
+ $this->assertFalse($connected);
+ }
+
+ /**
+ * @dataProvider getValidNames
+ * @param string $name
+ */
+ public function testGetNamePart($name, $ip, $port)
+ {
+ $this->assertEquals($ip, Socket::getNamePart($name, Socket::NAME_PART_IP), 'splits ip correctly');
+ $this->assertEquals($port, Socket::getNamePart($name, Socket::NAME_PART_PORT), 'splits port correctly');
+ }
+
+ /**
+ * Data provider
+ */
+ public function getValidNames()
+ {
+ return array(
+ array('127.0.0.1:52339', '127.0.0.1', '52339'),
+ array('255.255.255.255:1025', '255.255.255.255', '1025'),
+ array('::1:56670', '::1', '56670')
+ );
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/Socket/UriSocketTest.php b/listeners/lib/Wrench/Tests/Socket/UriSocketTest.php
new file mode 100644
index 000000000..d0b7f8bdc
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Socket/UriSocketTest.php
@@ -0,0 +1,56 @@
+getInstance('ws://localhost:8000');
+ $this->assertInstanceOfClass($instance);
+ return $instance;
+ }
+
+ /**
+ * @dataProvider getInvalidConstructorArguments
+ * @expectedException InvalidArgumentException
+ */
+ public function testInvalidConstructor($uri)
+ {
+ $this->getInstance($uri);
+ }
+
+ /**
+ * @depends testConstructor
+ */
+ public function testGetIp($instance)
+ {
+ $this->assertStringStartsWith('localhost', $instance->getIp(), 'Correct host');
+ }
+
+ /**
+ * @depends testConstructor
+ */
+ public function testGetPort($instance)
+ {
+ $this->assertEquals(8000, $instance->getPort(), 'Correct port');
+ }
+
+ /**
+ * Data provider
+ */
+ public function getInvalidConstructorArguments()
+ {
+ return array(
+ array(false),
+ array('http://www.google.com/'),
+ array('ws:///'),
+ array(':::::'),
+ );
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/Test.php b/listeners/lib/Wrench/Tests/Test.php
new file mode 100644
index 000000000..f9b421fd9
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/Test.php
@@ -0,0 +1,61 @@
+assertInstanceOf(
+ $this->getClass(),
+ $instance,
+ $message
+ );
+ }
+
+ /**
+ * Gets an instance of the class under test
+ *
+ * @param mixed Normal constructor arguments
+ * @magic This method accepts a variable number of arguments
+ * @return object Of type given by getClass()
+ */
+ public function getInstance(/* ... */)
+ {
+ $reflection = new ReflectionClass($this->getClass());
+ return $reflection->newInstanceArgs(func_get_args());
+ }
+
+ /**
+ * Logging function
+ *
+ * Passed into some classes under test as a callable
+ *
+ * @param string $message
+ * @param string $priority
+ * @return void
+ */
+ public function log($message, $priority = 'info')
+ {
+ // nothing
+ }
+}
diff --git a/listeners/lib/Wrench/Tests/bootstrap.php b/listeners/lib/Wrench/Tests/bootstrap.php
new file mode 100644
index 000000000..84bd83943
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/bootstrap.php
@@ -0,0 +1,11 @@
+register();
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Tests/server.php b/listeners/lib/Wrench/Tests/server.php
new file mode 100644
index 000000000..34d680622
--- /dev/null
+++ b/listeners/lib/Wrench/Tests/server.php
@@ -0,0 +1,16 @@
+register();
+
+$server = new Wrench\Server('ws://localhost:' . $port);
+$server->registerApplication('echo', new Wrench\Application\EchoApplication());
+$server->run();
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Util/Configurable.php b/listeners/lib/Wrench/Util/Configurable.php
new file mode 100644
index 000000000..f1983d5e6
--- /dev/null
+++ b/listeners/lib/Wrench/Util/Configurable.php
@@ -0,0 +1,67 @@
+ Wrench\Protocol object, latest protocol
+ * version used if not specified
+ */
+ public function __construct(
+ array $options = array()
+ ) {
+ $this->configure($options);
+ $this->configureProtocol();
+ }
+
+ /**
+ * Configures the options
+ *
+ * @param array $options
+ */
+ protected function configure(array $options)
+ {
+ $this->options = array_merge(array(
+ 'protocol' => new Rfc6455Protocol()
+ ), $options);
+ }
+
+ /**
+ * Configures the protocol option
+ *
+ * @throws InvalidArgumentException
+ */
+ protected function configureProtocol()
+ {
+ $protocol = $this->options['protocol'];
+
+ if (!$protocol || !($protocol instanceof Protocol)) {
+ throw new InvalidArgumentException('Invalid protocol option');
+ }
+
+ $this->protocol = $protocol;
+ }
+}
\ No newline at end of file
diff --git a/listeners/lib/Wrench/Util/Ssl.php b/listeners/lib/Wrench/Util/Ssl.php
new file mode 100644
index 000000000..e8cc8bd7e
--- /dev/null
+++ b/listeners/lib/Wrench/Util/Ssl.php
@@ -0,0 +1,51 @@
+ $country_name,
+ 'stateOrProvinceName' => $state_or_province_name,
+ 'localityName' => $locality_name,
+ 'organizationName' => $organization_name,
+ 'organizationalUnitName' => $organizational_unit_name,
+ 'commonName' => $common_name,
+ 'emailAddress' => $email_address
+ );
+
+ $privkey = openssl_pkey_new();
+ $cert = openssl_csr_new($dn, $privkey);
+ $cert = openssl_csr_sign($cert, null, $privkey, 365);
+
+ $pem = array();
+
+ openssl_x509_export($cert, $pem[0]);
+
+ if ($pem_passphrase !== null) {
+ openssl_pkey_export($privkey, $pem[1], $pem_passphrase);
+ }
+
+ $pem = implode($pem);
+ file_put_contents($pem_file, $pem);
+ }
+}
\ No newline at end of file
diff --git a/listeners/navSyncBroadcasterServer.php b/listeners/navSyncBroadcasterServer.php
new file mode 100644
index 000000000..1bb19be43
--- /dev/null
+++ b/listeners/navSyncBroadcasterServer.php
@@ -0,0 +1,42 @@
+register();
+
+// parse the main config for the content sync port
+if (!($config = @parse_ini_file(__DIR__."/../config/config.ini"))) {
+ print "Missing the configuration file. Please build it using the Pattern Lab builder.\n";
+ exit;
+}
+
+$port = ($config) ? trim($config['navSyncPort']) : '8000';
+
+// start the content sync server
+$server = new \Wrench\Server('ws://0.0.0.0:'.$port.'/', array());
+
+// register the application & run it
+$server->registerApplication('navsync', new \Wrench\Application\navSyncBroadcasterApplication());
+
+print "\n";
+print "Page Follow Server Started...\n";
+print "Use CTRL+C to stop this service...\n";
+
+$server->run();
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..2767cb8e6
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "patternlab-node",
+ "version": "0.0.1",
+ "devDependencies": {
+ "grunt": "~0.4.0",
+ "grunt-contrib-nodeunit": "~0.1.2",
+ "grunt-contrib-watch": "~0.2.0",
+ "grunt-contrib-sass": "~0.2.2",
+ "grunt-contrib-copy": "~0.4.0",
+ "grunt-contrib-jshint": "~0.4.0"
+ }
+}
diff --git a/public/WHAR_INDEX b/public/WHAR_INDEX
new file mode 100644
index 000000000..bf59a26ad
--- /dev/null
+++ b/public/WHAR_INDEX
@@ -0,0 +1,3 @@
+If you're seeing a listing of files rather than the main home page for Pattern Lab
+then you probably forgot to generate the site first. Please go into scripts/ and
+double-click generateSite.command.
\ No newline at end of file
diff --git a/public/listeners/synclisteners.js b/public/listeners/synclisteners.js
new file mode 100644
index 000000000..a76ee4921
--- /dev/null
+++ b/public/listeners/synclisteners.js
@@ -0,0 +1,162 @@
+
+/*!
+ * Sync Listeners, v0.1
+ *
+ * Copyright (c) 2013 Dave Olsen, http://dmolsen.com
+ * Licensed under the MIT license
+ *
+ * The JavaScript component of the WebSocket set-up that supports syncing
+ * navigation between browsers and content updates with the server.
+ *
+ * The WebSocket test is from Modernizr. It might be a little too strict for our purposes.
+ * https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets.js
+ *
+ */
+
+var wsn;
+var wsnConnected = false;
+var wsc;
+var wscConnected = false;
+var dataPrevious = 0;
+var host = (window.location.host != "") ? window.location.host : "127.0.0.1";
+
+// handle page updates from one browser to another
+function connectNavSync() {
+
+ if ('WebSocket' in window && window.WebSocket.CLOSING === 2) {
+
+ var navSyncCopy = "Page Follow";
+ wsn = new WebSocket("ws://"+host+":"+navSyncPort+"/navsync");
+
+ // when trying to open a connection to WebSocket update the pattern lab nav bar
+ wsn.onopen = function (event) {
+ wsnConnected = true;
+ $('#navSyncButton').attr("data-state","on");
+ $('#navSyncButton').addClass("connected");
+ $('#navSyncButton').html(navSyncCopy+' On');
+ }
+
+ // when closing a connection (or failing to make a connection) to WebSocket update the pattern lab nav bar
+ wsn.onclose = function (event) {
+ wsnConnected = false;
+ $('#navSyncButton').attr("data-state","off");
+ if ($('#navSyncButton').hasClass("connected")) {
+ $('#navSyncButton').removeClass("connected");
+ }
+ $('#navSyncButton').html(navSyncCopy+' Disabled');
+ }
+
+ // when receiving a message from WebSocket update the iframe source
+ wsn.onmessage = function (event) {
+
+ var data = JSON.parse(event.data);
+ var vpLocation = document.getElementById('sg-viewport').contentWindow.location.href;
+ var mLocation = "http://"+host+data.url;
+
+ if (vpLocation != mLocation) {
+
+ document.getElementById('sg-viewport').contentWindow.location.replace(mLocation);
+
+ // make sure the pop doesn't fire and push the pattern
+ urlHandler.doPop = false;
+ urlHandler.pushPattern(data.patternpartial);
+
+ // reset the defaults
+ urlHandler.doPop = true;
+ urlHandler.skipBack = false;
+
+ }
+ }
+
+ // when there's an error update the pattern lab nav bar
+ wsn.onerror = function (event) {
+ wsnConnected = false;
+ $('#navSyncButton').attr("data-state","off");
+ if ($('#navSyncButton').hasClass("connected")) {
+ $('#navSyncButton').removeClass("connected");
+ }
+ $('#navSyncButton').html(navSyncCopy+' Disabled');
+ }
+
+ }
+
+}
+connectNavSync();
+
+// handle content updates generated by the watch
+function connectContentSync() {
+
+ if ('WebSocket' in window && window.WebSocket.CLOSING === 2) {
+
+ var dc = true;
+ var contentSyncCopy = "Auto-reload";
+
+ wsc = new WebSocket("ws://"+host+":"+contentSyncPort+"/contentsync");
+
+ // when trying to open a connection to WebSocket update the pattern lab nav bar
+ wsc.onopen = function (event) {
+ wscConnected = true;
+ $('#contentSyncButton').attr("data-state","on");
+ $('#contentSyncButton').addClass("connected");
+ $('#contentSyncButton').html(contentSyncCopy+' On');
+ }
+
+ // when closing a connection (or failing to make a connection) to WebSocket update the pattern lab nav bar
+ wsc.onclose = function (event) {
+ wscConnected = false;
+ $('#contentSyncButton').attr("data-state","off");
+ if ($('#contentSyncButton').hasClass("connected")) {
+ $('#contentSyncButton').removeClass("connected");
+ }
+ $('#contentSyncButton').html(contentSyncCopy+' Disabled');
+ }
+
+ // when receiving a message from WebSocket reload the current frame adding the received timestamp
+ // as a request var to, hopefully, bust caches... cachi(?)
+ wsc.onmessage = function (event) {
+ document.getElementById('sg-viewport').contentWindow.location.reload();
+ }
+
+ // when there's an error update the pattern lab nav bar
+ wsc.onerror = function (event) {
+ wscConnected = false;
+ $('#contentSyncButton').attr("data-state","off");
+ if ($('#contentSyncButton').hasClass("connected")) {
+ $('#contentSyncButton').removeClass("connected");
+ }
+ $('#contentSyncButton').html(contentSyncCopy+' Disabled');
+ }
+
+ }
+
+}
+connectContentSync();
+
+// handle when a user manually turns navSync and contentSync on & off
+$('#navSyncButton').click(function() {
+ if ($(this).attr("data-state") == "on") {
+ wsn.close();
+ $(this).attr("data-state","off");
+ $(this).removeClass("connected");
+ $(this).html('Nav Sync Off');
+ } else {
+ connectNavSync();
+ $(this).attr("data-state","on");
+ $(this).addClass("connected");
+ $(this).html('Nav Sync On');
+ }
+});
+
+$('#contentSyncButton').click(function() {
+ if ($(this).attr("data-state") == "on") {
+ wsc.close();
+ $(this).attr("data-state","off");
+ $(this).removeClass("connected");
+ $(this).html('Content Sync Off');
+ } else {
+ connectContentSync();
+ $(this).attr("data-state","on");
+ $(this).addClass("connected");
+ $(this).html('Content Sync On');
+ }
+});
diff --git a/public/styleguide/assets/icons.dev.svg b/public/styleguide/assets/icons.dev.svg
new file mode 100644
index 000000000..1fff93785
--- /dev/null
+++ b/public/styleguide/assets/icons.dev.svg
@@ -0,0 +1,37 @@
+
+
+
\ No newline at end of file
diff --git a/public/styleguide/assets/icons.eot b/public/styleguide/assets/icons.eot
new file mode 100644
index 000000000..ba04dfea4
Binary files /dev/null and b/public/styleguide/assets/icons.eot differ
diff --git a/public/styleguide/assets/icons.svg b/public/styleguide/assets/icons.svg
new file mode 100644
index 000000000..4902a1bb0
--- /dev/null
+++ b/public/styleguide/assets/icons.svg
@@ -0,0 +1,37 @@
+
+
+
\ No newline at end of file
diff --git a/public/styleguide/assets/icons.ttf b/public/styleguide/assets/icons.ttf
new file mode 100644
index 000000000..f5d06ee2e
Binary files /dev/null and b/public/styleguide/assets/icons.ttf differ
diff --git a/public/styleguide/assets/icons.woff b/public/styleguide/assets/icons.woff
new file mode 100644
index 000000000..a2423c261
Binary files /dev/null and b/public/styleguide/assets/icons.woff differ
diff --git a/public/styleguide/html/README b/public/styleguide/html/README
new file mode 100644
index 000000000..5b0c53819
--- /dev/null
+++ b/public/styleguide/html/README
@@ -0,0 +1 @@
+will hold the auto-generated styleguide.html
\ No newline at end of file
diff --git a/scripts/README b/scripts/README
new file mode 100644
index 000000000..95dca5ed7
--- /dev/null
+++ b/scripts/README
@@ -0,0 +1,6 @@
+These are Mac OS X-compatible files to open up select command line scripts. Simply double-click on a file and it should run
+the appropriate shell command. If you receive a permissions error you will need to use the command line to fix it.
+Simply go to the command line, cd to the scripts/php directory and type chmod +x [filename]. Replace [filename] with the
+appropriate filenames.
+
+That's confusing.
\ No newline at end of file
diff --git a/scripts/generateSite.command b/scripts/generateSite.command
new file mode 100644
index 000000000..acd0220e4
--- /dev/null
+++ b/scripts/generateSite.command
@@ -0,0 +1,3 @@
+#!/bin/sh
+DIR="$( cd "$( dirname "$0" )" && pwd )"
+php $DIR/../builder/builder.php -g
\ No newline at end of file
diff --git a/scripts/startAutoReloadServer.command b/scripts/startAutoReloadServer.command
new file mode 100644
index 000000000..8dfc8676e
--- /dev/null
+++ b/scripts/startAutoReloadServer.command
@@ -0,0 +1,3 @@
+#!/bin/sh
+DIR="$( cd "$( dirname "$0" )" && pwd )"
+php $DIR/../listeners/contentSyncBroadcasterServer.php
\ No newline at end of file
diff --git a/scripts/startPageFollowServer.command b/scripts/startPageFollowServer.command
new file mode 100644
index 000000000..8bfa8a9db
--- /dev/null
+++ b/scripts/startPageFollowServer.command
@@ -0,0 +1,3 @@
+#!/bin/sh
+DIR="$( cd "$( dirname "$0" )" && pwd )"
+php $DIR/../listeners/navSyncBroadcasterServer.php
\ No newline at end of file
diff --git a/scripts/startWatcher.command b/scripts/startWatcher.command
new file mode 100644
index 000000000..c1a8b937e
--- /dev/null
+++ b/scripts/startWatcher.command
@@ -0,0 +1,3 @@
+#!/bin/sh
+DIR="$( cd "$( dirname "$0" )" && pwd )"
+php $DIR/../builder/builder.php -w
\ No newline at end of file
diff --git a/source/_data/data.json b/source/_data/data.json
new file mode 100644
index 000000000..20d5d30f9
--- /dev/null
+++ b/source/_data/data.json
@@ -0,0 +1,72 @@
+{
+ "img": {
+ "landscape-4x3": {
+ "src": "../../images/fpo_4x3.png",
+ "alt": "4x3 Image"
+ },
+ "landscape-16x9": {
+ "src": "../../images/fpo_16x9.png",
+ "alt": "16x9 Image"
+ },
+ "square": {
+ "src": "../../images/fpo_square.png",
+ "alt": "Square Thumbnail"
+ },
+ "avatar" : {
+ "src" : "../../images/fpo_avatar.png",
+ "alt" : "Person Name"
+ }
+ },
+ "headline" : {
+ "short" : "Lorem ipsum dolor sit (37 characters)",
+ "medium" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit. (72 characters)"
+ },
+ "excerpt" : {
+ "short" : "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam",
+ "medium" : "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
+ "long" : "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
+ },
+ "description" : "So, setting about it as methodically as men might smoke out a wasps' nest, the Martians spread this strange stifling vapour over the Londonward country. The horns of the crescent slowly moved apart, until at last they formed a line from Hanwell to Coombe and Malden. All night through their destructive tubes advanced.",
+ "url" : "http://www.fillerati.com",
+ "name" : {
+ "first": "Lacy",
+ "firsti": "L",
+ "middle": "Tommie",
+ "middlei": "T",
+ "last": "Way",
+ "lasti": "W"
+ },
+ "year" : {
+ "long": "2013",
+ "short": "13"
+ },
+ "month" : {
+ "long": "February",
+ "short": "Feb",
+ "digit": "02"
+ },
+ "dayofweek" : {
+ "long": "Monday",
+ "short": "Mon"
+ },
+ "day" : {
+ "long": "10",
+ "short": "10",
+ "ordinal": "th"
+ },
+ "hour" : {
+ "long": "01",
+ "short": "1",
+ "military": "13",
+ "ampm": "pm"
+ },
+ "minute" : {
+ "long": "20",
+ "short": "20"
+ },
+ "seconds" : "31",
+ "author" : {
+ "first-name": "Author",
+ "last-name": "Name"
+ }
+}
\ No newline at end of file
diff --git a/source/_data/listitems.json b/source/_data/listitems.json
new file mode 100644
index 000000000..a5ee012eb
--- /dev/null
+++ b/source/_data/listitems.json
@@ -0,0 +1,782 @@
+{
+ "1": {
+ "title": "Nullizzle shizznit velizzle, hizzle, suscipit own yo', gravida vizzle, arcu.",
+ "img": {
+ "avatar": {
+ "src": "http://placeimg.com/100/100/people",
+ "alt": "Avatar"
+ },
+ "square": {
+ "src": "http://placeimg.com/300/300/nature",
+ "alt": "Square"
+ },
+ "rectangle": {
+ "src": "http://placeimg.com/400/300/tech",
+ "alt": "Rectangle"
+ }
+ },
+ "headline": {
+ "short": "Lorizzle pimpin' dolizzle sit amet I",
+ "medium": "Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my"
+ },
+ "excerpt": {
+ "short": "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.",
+ "medium": "Izzle crazy tempizzle sizzle. We gonna chung gangsta get down get down fo shizzle turpizzle. Away break it down black. Pellentesque bling bling rhoncus fo shizzle. In hac the bizzle platea dictumst. Black dapibizzle. Crackalackin.",
+ "long": "Curabitizzle fo shizzle diam quizzle nisi nizzle mollizzle. Suspendisse boofron. Morbi odio. Sure pizzle. Crazy orci. Shut the shizzle up maurizzle get down get down, check out this a, go to hizzle sit amizzle, malesuada izzle, pede. Pellentesque gravida. Vestibulizzle check it out mi, volutpat izzle, shiz sed, shiznit sempizzle, da bomb. Funky fresh in ipsum. Da bomb volutpat felis vizzle daahng dawg. Crizzle quis dope izzle fo shizzle my ni."
+ },
+ "description": "Fizzle crazy tortor. Sed rizzle. Ass pimpin' dolor dapibizzle turpis tempizzle fo shizzle my nizzle. Maurizzle pellentesque its fo rizzle izzle turpis. Get down get down we gonna chung nizzle. Shizzlin dizzle eleifend rhoncizzle break it down. In yo ghetto platea dictumst. Bling bling dapibizzle. Curabitur break yo neck, yall fo, pretizzle eu, go to hizzle dope, own yo' vitae, nunc. Bizzle suscipizzle. Ass semper velit sizzle fo.",
+ "url": "http://lorizzle.nl/",
+ "name": {
+ "first": "Junius",
+ "firsti": "J",
+ "middle": "Marius",
+ "middlei": "M",
+ "last": "Koolen",
+ "lasti": "K"
+ },
+ "year": {
+ "long": "2013",
+ "short": "13"
+ },
+ "month": {
+ "long": "January",
+ "short": "Jan",
+ "digit": "01"
+ },
+ "dayofweek": {
+ "long": "Sunday",
+ "short": "Sun"
+ },
+ "day": {
+ "long": "01",
+ "short": "1",
+ "ordinal": "st"
+ },
+ "hour": {
+ "long": "06",
+ "short": "6",
+ "military": "06",
+ "ampm": "am"
+ },
+ "minute": {
+ "long": "20",
+ "short": "20"
+ },
+ "seconds": "31"
+ },
+ "2": {
+ "title": "Veggies sunt bona vobis, proinde vos postulo",
+ "img": {
+ "avatar": {
+ "src": "http://placeimg.com/100/100/nature",
+ "alt": "Avatar"
+ },
+ "square": {
+ "src": "http://placeimg.com/300/300/tech",
+ "alt": "Square"
+ },
+ "rectangle": {
+ "src": "http://placeimg.com/400/300/people",
+ "alt": "Rectangle"
+ }
+ },
+ "headline": {
+ "short": "Veggies sunt bona vobis, proinde vos",
+ "medium": "Postulo esse magis azuki bean burdock brussels sprout quandong komatsun"
+ },
+ "excerpt": {
+ "short": "A fava bean collard greens endive tomatillo lotus root okra winter purslane zucchini parsley spinach artichoke. Brussels sprout pea turnip catsear.",
+ "medium": "Bush tomato gumbo potato garbanzo ricebean burdock daikon coriander kale quandong. Bok choy celery leek avocado shallot horseradish aubergine parsley. Bok choy bell pepper kale celery desert raisin kakadu plum bok choy bunya nuts.",
+ "long": "Spinach tigernut. Corn cucumber grape black-eyed pea asparagus spinach avocado dulse bunya nuts epazote celery desert raisin celtuce burdock plantain yarrow napa cabbage. Plantain okra seakale endive tigernut pea sprouts asparagus corn chard peanut beet greens groundnut radicchio carrot coriander gumbo gram celtuce. Jícama nori bamboo shoot collard greens okra radicchio tomato. Catsear mustard corn tigernut celery kale water spinach bok choy."
+ },
+ "description": "Mung bean squash sorrel taro coriander collard greens gumbo bitterleaf tomato. Taro water chestnut celtuce turnip yarrow celery endive scallion black-eyed pea onion. Aubergine dulse turnip greens mustard salsify garlic soybean parsley bitterleaf desert raisin courgette.",
+ "url": "http://veggieipsum.com",
+ "name": {
+ "first": "Siguror",
+ "firsti": "S",
+ "middle": "Aron",
+ "middlei": "A",
+ "last": "Hanigan",
+ "lasti": "H"
+ },
+ "year": {
+ "long": "2013",
+ "short": "13"
+ },
+ "month": {
+ "long": "February",
+ "short": "Feb",
+ "digit": "02"
+ },
+ "dayofweek": {
+ "long": "Monday",
+ "short": "Mon"
+ },
+ "day": {
+ "long": "10",
+ "short": "10",
+ "ordinal": "th"
+ },
+ "hour": {
+ "long": "01",
+ "short": "1",
+ "military": "13",
+ "ampm": "pm"
+ },
+ "minute": {
+ "long": "20",
+ "short": "20"
+ },
+ "seconds": "31"
+ },
+ "3": {
+ "title": "Bacon ipsum dolor sit amet turducken strip steak beef ribs shank",
+ "img": {
+ "avatar": {
+ "src": "http://placeimg.com/100/100/tech",
+ "alt": "Avatar"
+ },
+ "square": {
+ "src": "http://placeimg.com/300/300/people",
+ "alt": "Square"
+ },
+ "rectangle": {
+ "src": "http://placeimg.com/400/300/nature",
+ "alt": "Rectangle"
+ }
+ },
+ "headline": {
+ "short": "Bacon ipsum dolor sit amet spare rib",
+ "medium": "Tongue pancetta short ribs bacon. Kielbasa ball tip cow bresaola, capic"
+ },
+ "excerpt": {
+ "short": "Tail jerky rump shoulder t-bone meatball meatloaf salami. Filet mignon shank t-bone venison, ham hock ribeye drumstick bresaola kielbasa. Frankfurter.",
+ "medium": "Doner biltong turducken leberkas. Rump swine pork loin ribeye ball tip meatloaf, pork chop ground round pig pancetta cow biltong brisket. Beef corned beef beef ribs, bacon pork belly sausage meatball boudin doner ham hock. Swine gro.",
+ "long": "Und round meatball, bacon pig leberkas corned beef tongue shoulder. Drumstick pork loin prosciutto ball tip shank pancetta spare ribs jowl pastrami. Frankfurter boudin filet mignon ribeye. Pig hamburger strip steak ham turducken prosciutto bresaola ground round pancetta frankfurter jowl. Frankfurter tongue brisket tenderloin, beef ribs pastrami biltong tail bresaola flank. Biltong pork chop beef boudin hamburger bacon. Capicola bresaola sausage."
+ },
+ "description": "Boudin sausage jerky pastrami ground round salami biltong. Sausage fatback strip steak doner pork loin, pork belly drumstick ham short loin hamburger shankle. Short ribs sirloin rump tri-tip beef biltong. Meatball pig salami, jowl pork loin fatback short loin drumstick andouille.",
+ "url": "http://baconipsum.com/",
+ "name": {
+ "first": "Teun",
+ "firsti": "T",
+ "middle": "Jodocus",
+ "middlei": "J",
+ "last": "Richard",
+ "lasti": "R"
+ },
+ "year": {
+ "long": "2013",
+ "short": "13"
+ },
+ "month": {
+ "long": "March",
+ "short": "Mar",
+ "digit": "03"
+ },
+ "dayofweek": {
+ "long": "Tuesday",
+ "short": "Tue"
+ },
+ "day": {
+ "long": "22",
+ "short": "22",
+ "ordinal": "nd"
+ },
+ "hour": {
+ "long": "04",
+ "short": "4",
+ "military": "16",
+ "ampm": "pm"
+ },
+ "minute": {
+ "long": "45",
+ "short": "45"
+ },
+ "seconds": "11"
+ },
+ "4": {
+ "title": "Whatever swag accusamus occupy, gentrify butcher tote bag",
+ "img": {
+ "avatar": {
+ "src": "http://placeimg.com/100/100/animals",
+ "alt": "Avatar"
+ },
+ "square": {
+ "src": "http://placeimg.com/300/300/arch",
+ "alt": "Square"
+ },
+ "rectangle": {
+ "src": "http://placeimg.com/400/300/people/grayscale",
+ "alt": "Rectangle"
+ }
+ },
+ "headline": {
+ "short": "Nesciunt sunt cillum keytar Pitchfork",
+ "medium": "Tote bag mixtape PBR Helvetica scenester forage four loko. Irure Tonx"
+ },
+ "excerpt": {
+ "short": "Golf quis +1, Wes Anderson church-key lo-fi keffiyeh selvage culpa authentic Brooklyn fap chambray. Id synth yr, 3 wolf moon locavore +1 mixtape do.",
+ "medium": "Sed single-origin coffee anim eu. Bicycle rights Neutra Truffaut pop-up. Paleo hella irure meh Banksy, Wes Anderson typewriter VHS jean shorts yr. Eiusmod officia banjo Thundercats, odio laborum magna deep v cornhole nostrud kitsch.",
+ "long": "Tattooed Williamsburg. Jean shorts proident kogi laboris. Non tote bag pariatur elit slow-carb, Vice irure eu Echo Park ea aliqua chillwave. Cornhole Etsy quinoa Pinterest cardigan. Excepteur quis forage, Blue Bottle keffiyeh velit hoodie direct trade typewriter Etsy. Fingerstache squid non, sriracha drinking vinegar Shoreditch pork belly. Paleo sartorial mollit 3 wolf moon chambray whatever, sed tote bag small batch freegan. Master cleanse."
+ },
+ "description": "Fanny pack ullamco et veniam semiotics. Shoreditch PBR reprehenderit cliche, magna Tonx aesthetic. Narwhal photo booth DIY aute post-ironic anim. Vice cliche brunch est before they sold out fap, street art Odd Future fashion axe messenger bag nihil Tonx tattooed. Nihil hashtag incididunt, do eu art party Banksy jean shorts four loko typewriter.",
+ "url": "http://hipsteripsum.me/",
+ "name": {
+ "first": "Duane",
+ "firsti": "D",
+ "middle": "Edvin",
+ "middlei": "E",
+ "last": "Wilms",
+ "lasti": "W"
+ },
+ "year": {
+ "long": "2013",
+ "short": "13"
+ },
+ "month": {
+ "long": "April",
+ "short": "Apr",
+ "digit": "04"
+ },
+ "dayofweek": {
+ "long": "Wednesday",
+ "short": "Wed"
+ },
+ "day": {
+ "long": "13",
+ "short": "13",
+ "ordinal": "th"
+ },
+ "hour": {
+ "long": "10",
+ "short": "10",
+ "military": "10",
+ "ampm": "am"
+ },
+ "minute": {
+ "long": "14",
+ "short": "14"
+ },
+ "seconds": "52"
+ },
+ "5": {
+ "title": "Marshall McLuhan Colbert bump backpack journalist vast wasteland Romenesko CPM",
+ "img": {
+ "avatar": {
+ "src": "http://placeimg.com/100/100/people/grayscale",
+ "alt": "Avatar"
+ },
+ "square": {
+ "src": "http://placeimg.com/300/300/animals",
+ "alt": "Square"
+ },
+ "rectangle": {
+ "src": "http://placeimg.com/400/300/arch",
+ "alt": "Rectangle"
+ }
+ },
+ "headline": {
+ "short": "Blog meme masthead DocumentCloud Fou",
+ "medium": "Square tabloid Andy Carvin stupid commenters, Nick Denton mathewi semip"
+ },
+ "excerpt": {
+ "short": "I love the Weather & Opera section Groupon copyright in the slot, Journal Register open newsroom analytics future totally blowing up on Twitter AOL.",
+ "medium": "CTR mthomps Flipboard do what you do best and link to the rest Buttry media bias Journal Register RT, newspaper strike do what you do best and link to the rest semipermeable learnings cognitive surplus mathewi, Encyclo Google News.",
+ "long": "Pulse mathewi Project Thunderdome digital first. HuffPo social media optimization try PR dying the notion of the public monetization data visualization audience atomization overcome community, libel lawyer twitterati should isn't a business model fair use innovation Facebook AOL, Walter Cronkite died for your sins horse-race coverage crowdfunding Patch but what's the business model rubber cement horse-race coverage. Lucius Nieman content farm."
+ },
+ "description": "Like button audience atomization overcome Colbert bump Free Darko inverted pyramid we will make them pay, digital circulation strategy Like button totally blowing up on Twitter church of the savvy. Pictures of Goats section open source discuss Frontline analog thinking filters paidContent.",
+ "url": "http://www.niemanlab.org/journo-ipsum/",
+ "name": {
+ "first": "Frans",
+ "firsti": "F",
+ "middle": "Fabius",
+ "middlei": "F",
+ "last": "Keegan",
+ "lasti": "K"
+ },
+ "year": {
+ "long": "2013",
+ "short": "13"
+ },
+ "month": {
+ "long": "May",
+ "short": "May",
+ "digit": "05"
+ },
+ "dayofweek": {
+ "long": "Thursday",
+ "short": "Thu"
+ },
+ "day": {
+ "long": "26",
+ "short": "26",
+ "ordinal": "th"
+ },
+ "hour": {
+ "long": "06",
+ "short": "6",
+ "military": "18",
+ "ampm": "pm"
+ },
+ "minute": {
+ "long": "37",
+ "short": "37"
+ },
+ "seconds": "24"
+ },
+ "6": {
+ "title": "Thunder, thunder, thundercats, Ho!",
+ "img": {
+ "avatar": {
+ "src": "http://placeimg.com/100/100/arch",
+ "alt": "Avatar"
+ },
+ "square": {
+ "src": "http://placeimg.com/300/300/animals",
+ "alt": "Square"
+ },
+ "rectangle": {
+ "src": "http://placeimg.com/400/300/people/grayscale",
+ "alt": "Rectangle"
+ }
+ },
+ "headline": {
+ "short": "Hong Kong Phooey, number one super g",
+ "medium": "Hong Kong Phooey, quicker than the human eye. He's got style, a groovy"
+ },
+ "excerpt": {
+ "short": "Style, and a car that just won't stop. When the going gets tough, he's really rough, with a Hong Kong Phooey chop (Hi-Ya!). Hong Kong Phooey, number.",
+ "medium": "One super guy. Hong Kong Phooey, quicker than the human eye. Hong Kong Phooey, he's fan-riffic! One for all and all for one, Muskehounds are always ready. One for all and all for one, helping everybody. One for all and all for one.",
+ "long": "It's a pretty story. Sharing everything with fun, that's the way to be. One for all and all for one, Muskehounds are always ready. One for all and all for one, helping everybody. One for all and all for one, can sound pretty corny. If you've got a problem chum, think how it could be. This is my boss, Jonathan Hart, a self-made millionaire, he's quite a guy. This is Mrs H., she's gorgeous, she's one lady who knows how to take care of herself."
+ },
+ "description": "Beats all you've ever saw, been in trouble with the law since the day they was born. Straight'nin' the curve, flat'nin' the hills. Someday the mountain might get 'em, but the law never will. Makin' their way, the only way they know how, that's just a little bit more than the law will allow. Just good ol' boys, wouldn't change if they could, fightin' the system like a true modern day Robin Hood.",
+ "url": "http://www.malevole.com/mv/misc/text/",
+ "name": {
+ "first": "Fergus",
+ "firsti": "F",
+ "middle": "Jon",
+ "middlei": "J",
+ "last": "Althuis",
+ "lasti": "A"
+ },
+ "year": {
+ "long": "2013",
+ "short": "13"
+ },
+ "month": {
+ "long": "June",
+ "short": "Jun",
+ "digit": "06"
+ },
+ "dayofweek": {
+ "long": "Friday",
+ "short": "Fri"
+ },
+ "day": {
+ "long": "08",
+ "short": "8",
+ "ordinal": "th"
+ },
+ "hour": {
+ "long": "11",
+ "short": "11",
+ "military": "23",
+ "ampm": "pm"
+ },
+ "minute": {
+ "long": "37",
+ "short": "37"
+ },
+ "seconds": "33"
+ },
+ "7": {
+ "title": "Yeah, I like animals better than people sometimes",
+ "img": {
+ "avatar": {
+ "src": "http://placeimg.com/100/100/any",
+ "alt": "Avatar"
+ },
+ "square": {
+ "src": "http://placeimg.com/300/300/any",
+ "alt": "Square"
+ },
+ "rectangle": {
+ "src": "http://placeimg.com/400/300/any",
+ "alt": "Rectangle"
+ }
+ },
+ "headline": {
+ "short": "Now that we know who you are, I know",
+ "medium": "Who I am. I'm not a mistake! It all makes sense! In a comic, you know"
+ },
+ "excerpt": {
+ "short": "How you can tell who the arch-villain's going to be? He's the exact opposite of the hero. And most times they're friends, like you and me! I should've known way back when... You know why, David? Because of the kids. They called me.",
+ "medium": "The lysine contingency - it's intended to prevent the spread of the animals is case they ever got off the island. Dr. Wu inserted a gene that makes a single faulty enzyme in protein metabolism. The animals can't manufacture the amin.",
+ "long": "Do you see any Teletubbies in here? Do you see a slender plastic tag clipped to my shirt with my name printed on it? Do you see a little Asian child with a blank expression on his face sitting outside on a mechanical helicopter that shakes when you put quarters in it? No? Well, that's what you see at a toy store. And you must think you're in a toy store, because you're here shopping for an infant named Jeb. Acid lysine. Unless they're."
+ },
+ "description": "Especially dogs. Dogs are the best. Every time you come home, they act like they haven't seen you in a year. And the good thing about dogs... is they got different dogs for different people. Like pit bulls. The dog of dogs. Pit bull can be the right man's best friend... or the wrong man's worst enemy. You going to give me a dog for a pet, give me a pit bull.",
+ "url": "http://slipsum.com/lite/",
+ "name": {
+ "first": "Bertil",
+ "firsti": "B",
+ "middle": "Pier",
+ "middlei": "P",
+ "last": "Aaij",
+ "lasti": "A"
+ },
+ "year": {
+ "long": "2013",
+ "short": "13"
+ },
+ "month": {
+ "long": "July",
+ "short": "Jul",
+ "digit": "07"
+ },
+ "dayofweek": {
+ "long": "Saturday",
+ "short": "Sat"
+ },
+ "day": {
+ "long": "22",
+ "short": "22",
+ "ordinal": "nd"
+ },
+ "hour": {
+ "long": "11",
+ "short": "11",
+ "military": "11",
+ "ampm": "am"
+ },
+ "minute": {
+ "long": "12",
+ "short": "12"
+ },
+ "seconds": "47"
+ },
+ "8": {
+ "title": "Webtwo ipsum dolor sit amet, eskobo chumby doostang bebo",
+ "img": {
+ "avatar": {
+ "src": "http://placeimg.com/100/100/any/grayscale",
+ "alt": "Avatar"
+ },
+ "square": {
+ "src": "http://placeimg.com/300/300/any/grayscale",
+ "alt": "Square"
+ },
+ "rectangle": {
+ "src": "http://placeimg.com/400/300/any/grayscale",
+ "alt": "Rectangle"
+ }
+ },
+ "headline": {
+ "short": "Webtwo ipsum dolor sit amet, eskobo",
+ "medium": "Chumby doostang bebo. Wakoopa oooj geni zoho loopt eskobo sifteo chart"
+ },
+ "excerpt": {
+ "short": "Dropio, chumby waze dopplr plugg oooj yammer jibjab imvu yuntaa knewton, mobly trulia airbnb bitly chegg tivo empressr knewton. Plickers spock voxy.",
+ "medium": "Zooomr kippt voxy zinch appjet napster trulia, zappos wufoo zapier spotify mzinga jaiku fleck, disqus lijit voxy voki yoono. Dogster elgg jibjab xobni kazaa bebo udemy sifteo kiko, elgg knewton skype mog octopart zoodles kazaa udem.",
+ "long": "Appjet spock handango empressr lijit palantir weebly dropio jibjab revver kaboodle spotify orkut mobly chegg akismet, handango ebay woopra revver joukuu kosmix unigo oooooc wufoo zanga kno zinch spock knewton. Balihoo greplin bebo squidoo skype kaboodle meebo disqus joost gooru, zlio tumblr edmodo palantir eskobo shopify kiko gsnap. Greplin balihoo chartly plugg imeem diigo trulia plickers qeyno wikia akismet, palantir grockit prezi jabber zo."
+ },
+ "description": "Wufoo diigo grockit sifteo divvyshot, unigo zooomr revver. Edmodo appjet joyent skype bubbli jajah zoodles joukuu xobni hojoki edmodo appjet, mozy mzinga akismet yuntaa joost yuntaa geni tivo insala yoono chumby, grockit sococo loopt zanga etsy cloudera koofers empressr jiglu blippy. Omgpop lanyrd joukuu sococo zimbra airbnb movity jibjab, foodzie.",
+ "url": "http://web20ipsum.com",
+ "name": {
+ "first": "Freyr",
+ "firsti": "F",
+ "middle": "Ninian",
+ "middlei": "N",
+ "last": "Hines",
+ "lasti": "H"
+ },
+ "year": {
+ "long": "2013",
+ "short": "13"
+ },
+ "month": {
+ "long": "August",
+ "short": "Aug",
+ "digit": "08"
+ },
+ "dayofweek": {
+ "long": "Sunday",
+ "short": "Sun"
+ },
+ "day": {
+ "long": "31",
+ "short": "31",
+ "ordinal": "st"
+ },
+ "hour": {
+ "long": "03",
+ "short": "3",
+ "military": "15",
+ "ampm": "pm"
+ },
+ "minute": {
+ "long": "42",
+ "short": "42"
+ },
+ "seconds": "21"
+ },
+ "9": {
+ "title": "Rebel Mission to Ord Mantell",
+ "img": {
+ "avatar": {
+ "src": "http://placeimg.com/100/100/any/sepia",
+ "alt": "Avatar"
+ },
+ "square": {
+ "src": "http://placeimg.com/300/300/any/sepia",
+ "alt": "Square"
+ },
+ "rectangle": {
+ "src": "http://placeimg.com/400/300/any/sepia",
+ "alt": "Rectangle"
+ }
+ },
+ "headline": {
+ "short": "All right. Well, take care of yourself, Han",
+ "medium": "You don't believe in the Force, do you? The Force is strong with this one"
+ },
+ "excerpt": {
+ "short": "I'm trying not to, kid. I find your lack of faith disturbing. You are a part of the Rebel Alliance and a traitor! Take her away! I want to come with.",
+ "medium": "I'm surprised you had the courage to take the responsibility yourself. Don't be too proud of this technological terror you've constructed. The ability to destroy a planet is insignificant next to the power of the Force. You don't be.",
+ "long": "A tremor in the Force. The last time I felt it was in the presence of my old master. You don't believe in the Force, do you? I have traced the Rebel spies to her. Now she is my only link to finding their secret base. A tremor in the Force. The last time I felt it was in the presence of my old master. I'm trying not to, kid. The more you tighten your grip, Tarkin, the more star systems will slip through your fingers. There's nothing for me here."
+ },
+ "description": "I find your lack of faith disturbing. A tremor in the Force. The last time I felt it was in the presence of my old master. Don't act so surprised, Your Highness. You weren't on any mercy mission this time. Several transmissions were beamed to this ship by Rebel spies. I want to know what happened to the plans they sent you. The plans you refer to will soon be back in our hands.",
+ "url": "http://chrisvalleskey.com/fillerama/",
+ "name": {
+ "first": "Jacobus",
+ "firsti": "J",
+ "middle": "Domitianus",
+ "middlei": "D",
+ "last": "Sneiders",
+ "lasti": "S"
+ },
+ "year": {
+ "long": "2013",
+ "short": "13"
+ },
+ "month": {
+ "long": "September",
+ "short": "Sep",
+ "digit": "09"
+ },
+ "dayofweek": {
+ "long": "Monday",
+ "short": "Mon"
+ },
+ "day": {
+ "long": "04",
+ "short": "4",
+ "ordinal": "th"
+ },
+ "hour": {
+ "long": "09",
+ "short": "9",
+ "military": "09",
+ "ampm": "am"
+ },
+ "minute": {
+ "long": "04",
+ "short": "4"
+ },
+ "seconds": "37"
+ },
+ "10": {
+ "title": "Help, help, I'm being repressed!",
+ "img": {
+ "avatar": {
+ "src": "http://placeimg.com/100/100/tech/grayscale",
+ "alt": "Avatar"
+ },
+ "square": {
+ "src": "http://placeimg.com/300/300/nature/grayscale",
+ "alt": "Square"
+ },
+ "rectangle": {
+ "src": "http://placeimg.com/400/300/arch/grayscale",
+ "alt": "Rectangle"
+ }
+ },
+ "headline": {
+ "short": "The swallow may fly south with the sun",
+ "medium": "On second thoughts, let's not go there. It is a silly place. You don't"
+ },
+ "excerpt": {
+ "short": "The swallow may fly south with the sun, and the house martin or the plover may seek warmer climes in winter, yet these are not strangers to our land.",
+ "medium": "The Knights Who Say Ni demand a sacrifice! Found them? In Mercia?! The coconut's tropical! Where'd you get the coconuts? Why do you think that she is a witch? I am your king. You don't vote for kings. But you are dressed as one. Oh, ow!",
+ "long": "Well, I didn't vote for you. Burn her! Be quiet! He hasn't got shit all over him. Where'd you get the coconuts? The swallow may fly south with the sun, and the house martin or the plover may seek warmer climes in winter, yet these are not strangers to our land. No! Yes, yes. A bit. But she's got a wart. Shut up! Will you shut up?! I have to push the pram a lot. Now, look here, my good man. Frighten us, English pig-dogs! Go and boil your bottoms."
+ },
+ "description": "The Knights Who Say Ni demand a sacrifice! …Are you suggesting that coconuts migrate? Knights of Ni, we are but simple travelers who seek the enchanter who lives beyond these woods. You don't frighten us, English pig-dogs! Go and boil your bottoms, sons of a silly person! I blow my nose at you, so-called Ah-thoor Keeng, you and all your silly English K-n-n-n-n-n-n-n-niggits!",
+ "url": "http://chrisvalleskey.com/fillerama/",
+ "name": {
+ "first": "Plinius",
+ "firsti": "P",
+ "middle": "Varinius",
+ "middlei": "V",
+ "last": "Sloane",
+ "lasti": "S"
+ },
+ "year": {
+ "long": "2013",
+ "short": "13"
+ },
+ "month": {
+ "long": "October",
+ "short": "Oct",
+ "digit": "10"
+ },
+ "dayofweek": {
+ "long": "Tuesday",
+ "short": "Tue"
+ },
+ "day": {
+ "long": "25",
+ "short": "25",
+ "ordinal": "th"
+ },
+ "hour": {
+ "long": "03",
+ "short": "3",
+ "military": "03",
+ "ampm": "am"
+ },
+ "minute": {
+ "long": "51",
+ "short": "51"
+ },
+ "second": "19"
+ },
+ "11": {
+ "title": "Danish danish candy canes bonbon cheesecake danish marzipan",
+ "img": {
+ "avatar": {
+ "src": "http://placeimg.com/100/100/tech/sepia",
+ "alt": "Avatar"
+ },
+ "square": {
+ "src": "http://placeimg.com/300/300/animals/sepia",
+ "alt": "Square"
+ },
+ "rectangle": {
+ "src": "http://placeimg.com/400/300/arch/sepia",
+ "alt": "Rectangle"
+ }
+ },
+ "headline": {
+ "short": "Carrot cake fruitcake dessert apple",
+ "medium": "Pie powder lemon drops sesame snaps cake brownie. Biscuit ice cream gin"
+ },
+ "excerpt": {
+ "short": "Bread cotton candy marzipan. Baker too go gingerbread topping cupcake donut. Fruitcake marzipan bear claw tart toffee candy cheesecake. Lemon drops.",
+ "medium": "Cupcake chupa chups pudding gummies. Unerdwear.com cupcake candy soufflé sesame snaps macaroon sesame snaps. Tart dragée muffin. Sweet roll gummi bears caramels fruitcake candy cake. Cotton candy carrot cake tart cotton candy. Jelly.",
+ "long": "Gingerbread candy icing pastry cake bonbon fruitcake donut. Powder liquorice dessert tart croissant cake. Dessert chocolate cake sweet roll candy candy sesame snaps tiramisu ice cream. Candy candy canes marzipan biscuit cupcake pie pudding. Donut cotton candy muffin. Pastry bear claw icing halvah. Gingerbread cotton candy sweet roll toffee chocolate jujubes. Wafer jujubes danish ice cream lemon drops wafer. Sesame snaps cupcake gummies browni."
+ },
+ "description": "Sugar plum wafer soufflé ice cream. Wafer topping biscuit pie gummi bears topping. Gummies toffee powder applicake oat cake cookie. Bear claw candy tootsie roll fruitcake danish applicake candy canes macaroon. Liquorice tiramisu danish cotton candy gummies. Tiramisu dessert gummi bears macaroon sweet roll jelly-o gummi bears marzipan.",
+ "url": "http://cupcakeipsum.com/",
+ "name": {
+ "first": "Matthias",
+ "firsti": "M",
+ "middle": "Brady",
+ "middlei": "B",
+ "last": "Macguinness",
+ "lasti": "M"
+ },
+ "year": {
+ "long": "2013",
+ "short": "13"
+ },
+ "month": {
+ "long": "November",
+ "short": "Nov",
+ "digit": "11"
+ },
+ "dayofweek": {
+ "long": "Wednesday",
+ "short": "Wed"
+ },
+ "day": {
+ "long": "19",
+ "short": "19",
+ "ordinal": "th"
+ },
+ "hour": {
+ "long": "11",
+ "short": "11",
+ "military": "23",
+ "ampm": "pm"
+ },
+ "minute": {
+ "long": "55",
+ "short": "55"
+ },
+ "seconds": "12"
+ },
+ "12": {
+ "title": "Cottage cheese brie lancashire. Boursin when the cheese comes out.",
+ "img": {
+ "avatar": {
+ "src": "http://placeimg.com/100/100/people/sepia",
+ "alt": "Avatar"
+ },
+ "square": {
+ "src": "http://placeimg.com/300/300/people/sepia",
+ "alt": "Square"
+ },
+ "rectangle": {
+ "src": "http://placeimg.com/400/300/people/sepia",
+ "alt": "Rectangle"
+ }
+ },
+ "headline": {
+ "short": "Cauliflower cheese cream cheese baby",
+ "medium": "Lancashire cheesy feet rubber cheese cheese and wine gouda the big chee"
+ },
+ "excerpt": {
+ "short": "Queso fromage. Taleggio boursin bavarian bergkase cream cheese when the cheese comes out everybody's happy port-salut halloumi pecorino. Caerphilly cut the cheese manchego camembert de normandie goat melted cheese cheese and biscuit.",
+ "medium": "Pecorino queso lancashire. Manchego lancashire cheesy feet emmental babybel cheese strings dolcelatte bavarian bergkase. Ricotta cheese slices cheesy grin cow cheesecake smelly cheese mascarpone lancashire. Cow say cheese babybel do.",
+ "long": "Cheesy grin macaroni cheese airedale. Fromage frais airedale cheese and wine brie cow swiss swiss mozzarella. Emmental cheese triangles edam rubber cheese pepper jack ricotta airedale airedale. Brie parmesan smelly cheese cheese strings stinking bishop cheese strings taleggio. Bocconcini blue castello gouda. Everyone loves caerphilly rubber cheese halloumi smelly cheese melted cheese melted cheese bavarian bergkase. Rubber cheese ricotta emm."
+ },
+ "description": "Queso caerphilly cheesecake. Parmesan chalk and cheese port-salut port-salut babybel cottage cheese cheesy grin pepper jack. Croque monsieur paneer st. agur blue cheese emmental airedale monterey jack bavarian bergkase cheese triangles. Halloumi parmesan.",
+ "url": "http://www.cheeseipsum.co.uk/",
+ "name": {
+ "first": "Aquila",
+ "firsti": "A",
+ "middle": "Gaius",
+ "middlei": "G",
+ "last": "Achterkamp",
+ "lasti": "A"
+ },
+ "year": {
+ "long": "2013",
+ "short": "13"
+ },
+ "month": {
+ "long": "December",
+ "short": "Dec",
+ "digit": "12"
+ },
+ "dayofweek": {
+ "long": "Thursday",
+ "short": "Thu"
+ },
+ "day": {
+ "long": "28",
+ "short": "28",
+ "ordinal": "th"
+ },
+ "hour": {
+ "long": "08",
+ "short": "8",
+ "military": "08",
+ "ampm": "am"
+ },
+ "minute": {
+ "long": "34",
+ "short": "34"
+ },
+ "seconds": "56"
+ }
+}
\ No newline at end of file
diff --git a/source/_patternlab-files/README b/source/_patternlab-files/README
new file mode 100644
index 000000000..14208f4c9
--- /dev/null
+++ b/source/_patternlab-files/README
@@ -0,0 +1 @@
+There should be no reason to touch these files in day-to-day use.
\ No newline at end of file
diff --git a/source/_patternlab-files/index.mustache b/source/_patternlab-files/index.mustache
new file mode 100644
index 000000000..fb9ad5c2e
--- /dev/null
+++ b/source/_patternlab-files/index.mustache
@@ -0,0 +1,42 @@
+
+
+
+ Pattern Lab
+
+
+
+
+
+
+
+
+
+ Menu
+
+ {{> patternNav }}
+ {{> ishControls }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{> patternPaths }}
+ {{> viewAllPaths }}
+
+ {{> websockets }}
+
+
+
+
\ No newline at end of file
diff --git a/source/_patternlab-files/partials/ishControls.mustache b/source/_patternlab-files/partials/ishControls.mustache
new file mode 100644
index 000000000..1527c1750
--- /dev/null
+++ b/source/_patternlab-files/partials/ishControls.mustache
@@ -0,0 +1,44 @@
+
+
+
diff --git a/source/_patternlab-files/partials/patternPaths.mustache b/source/_patternlab-files/partials/patternPaths.mustache
new file mode 100644
index 000000000..798744030
--- /dev/null
+++ b/source/_patternlab-files/partials/patternPaths.mustache
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/source/_patternlab-files/partials/viewAllPaths.mustache b/source/_patternlab-files/partials/viewAllPaths.mustache
new file mode 100644
index 000000000..b0e044d97
--- /dev/null
+++ b/source/_patternlab-files/partials/viewAllPaths.mustache
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/source/_patternlab-files/partials/websockets.mustache b/source/_patternlab-files/partials/websockets.mustache
new file mode 100644
index 000000000..139a9b254
--- /dev/null
+++ b/source/_patternlab-files/partials/websockets.mustache
@@ -0,0 +1,6 @@
+
diff --git a/source/_patternlab-files/pattern-header-footer/README b/source/_patternlab-files/pattern-header-footer/README
new file mode 100644
index 000000000..6c5a981ab
--- /dev/null
+++ b/source/_patternlab-files/pattern-header-footer/README
@@ -0,0 +1 @@
+This is not a real pattern. It's simply the header and footer that patterns get sandwiched between when they're processed by the builder.
\ No newline at end of file
diff --git a/source/_patternlab-files/pattern-header-footer/footer.html b/source/_patternlab-files/pattern-header-footer/footer.html
new file mode 100644
index 000000000..0e6bb20f4
--- /dev/null
+++ b/source/_patternlab-files/pattern-header-footer/footer.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+