UNPKG

26.5 kBMarkdownView Raw
1<img src="https://github.com/flatiron/director/raw/master/img/director.png" />
2
3# Synopsis
4Director is a router. Routing is the process of determining what code to run when a URL is requested.
5
6# Motivation
7A routing library that works in both the browser and node.js environments with as few differences as possible. Simplifies the development of Single Page Apps and Node.js applications. Dependency free (doesn't require jQuery or Express, etc).
8
9# Status
10[![Build Status](https://secure.travis-ci.org/flatiron/director.png)](http://travis-ci.org/flatiron/director)
11
12# Features
13* [Client-Side Routing](#client-side)
14* [Server-Side HTTP Routing](#http-routing)
15* [Server-Side CLI Routing](#cli-routing)
16
17# Usage
18* [API Documentation](#api-documentation)
19* [Frequently Asked Questions](#faq)
20
21<a name="client-side"></a>
22## Client-side Routing
23It simply watches the hash of the URL to determine what to do, for example:
24
25```
26http://foo.com/#/bar
27```
28
29Client-side routing (aka hash-routing) allows you to specify some information about the state of the application using the URL. So that when the user visits a specific URL, the application can be transformed accordingly.
30
31<img src="https://github.com/flatiron/director/raw/master/img/hashRoute.png" />
32
33Here is a simple example:
34
35```html
36<!DOCTYPE html>
37<html>
38 <head>
39 <meta charset="utf-8">
40 <title>A Gentle Introduction</title>
41 <script src="https://raw.github.com/flatiron/director/master/build/director-1.0.7.min.js"></script>
42 <script>
43
44 var author = function () { console.log("author"); },
45 books = function () { console.log("books"); },
46 viewBook = function(bookId) { console.log("viewBook: bookId is populated: " + bookId); };
47
48 var routes = {
49 '/author': author,
50 '/books': [books, function() { console.log("An inline route handler."); }],
51 '/books/view/:bookId': viewBook
52 };
53
54 var router = Router(routes);
55 router.init();
56
57 </script>
58 </head>
59 <body>
60 <ul>
61 <li><a href="#/author">#/author</a></li>
62 <li><a href="#/books">#/books</a></li>
63 <li><a href="#/books/view/1">#/books/view/1</a></li>
64 </ul>
65 </body>
66</html>
67```
68
69Director works great with your favorite DOM library, such as jQuery.
70
71```html
72<!DOCTYPE html>
73<html>
74 <head>
75 <meta charset="utf-8">
76 <title>A Gentle Introduction 2</title>
77 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
78 <script src="https://raw.github.com/flatiron/director/master/build/director-1.0.7.min.js"></script>
79 <script>
80 $('document').ready(function(){
81 //
82 // create some functions to be executed when
83 // the correct route is issued by the user.
84 //
85 var showAuthorInfo = function () { console.log("showAuthorInfo"); },
86 listBooks = function () { console.log("listBooks"); },
87 allroutes = function() {
88 var route = window.location.hash.slice(2),
89 sections = $('section'),
90 section;
91 if ((section = sections.filter('[data-route=' + route + ']')).length) {
92 sections.hide(250);
93 section.show(250);
94 }
95 };
96
97 //
98 // define the routing table.
99 //
100 var routes = {
101 '/author': showAuthorInfo,
102 '/books': listBooks
103 };
104
105 //
106 // instantiate the router.
107 //
108 var router = Router(routes);
109
110 //
111 // a global configuration setting.
112 //
113 router.configure({
114 on: allroutes
115 });
116 router.init();
117 });
118 </script>
119 </head>
120 <body>
121 <section data-route="author">Author Name</section>
122 <section data-route="books">Book1, Book2, Book3</section>
123 <ul>
124 <li><a href="#/author">#/author</a></li>
125 <li><a href="#/books">#/books</a></li>
126 </ul>
127 </body>
128</html>
129```
130
131You can find a browser-specific build of `director` [here][1] which has all of the server code stripped away.
132
133<a name="http-routing"></a>
134## Server-Side HTTP Routing
135
136Director handles routing for HTTP requests similar to `journey` or `express`:
137
138```js
139 //
140 // require the native http module, as well as director.
141 //
142 var http = require('http'),
143 director = require('director');
144
145 //
146 // create some logic to be routed to.
147 //
148 function helloWorld(route) {
149 this.res.writeHead(200, { 'Content-Type': 'text/plain' })
150 this.res.end('hello world from (' + route + ')');
151 }
152
153 //
154 // define a routing table.
155 //
156 var router = new director.http.Router({
157 '/hello': {
158 get: helloWorld
159 }
160 });
161
162 //
163 // setup a server and when there is a request, dispatch the
164 // route that was requested in the request object.
165 //
166 var server = http.createServer(function (req, res) {
167 router.dispatch(req, res, function (err) {
168 if (err) {
169 res.writeHead(404);
170 res.end();
171 }
172 });
173 });
174
175 //
176 // You can also do ad-hoc routing, similar to `journey` or `express`.
177 // This can be done with a string or a regexp.
178 //
179 router.get('/bonjour', helloWorld);
180 router.get(/hola/, helloWorld);
181
182 //
183 // set the server to listen on port `8080`.
184 //
185 server.listen(8080);
186```
187
188<a name="cli-routing"></a>
189## CLI Routing
190
191Director supports Command Line Interface routing. Routes for cli options are based on command line input (i.e. `process.argv`) instead of a URL.
192
193``` js
194 var director = require('director');
195
196 var router = new director.cli.Router();
197
198 router.on('create', function () {
199 console.log('create something');
200 });
201
202 router.on(/destroy/, function () {
203 console.log('destroy something');
204 });
205
206 // You will need to dispatch the cli arguments yourself
207 router.dispatch('on', process.argv.slice(2).join(' '));
208```
209
210Using the cli router, you can dispatch commands by passing them as a string. For example, if this example is in a file called `foo.js`:
211
212``` bash
213$ node foo.js create
214create something
215$ node foo.js destroy
216destroy something
217```
218
219<a name="api-documentation"></a>
220# API Documentation
221
222* [Constructor](#constructor)
223* [Routing Table](#routing-table)
224* [Adhoc Routing](#adhoc-routing)
225* [Scoped Routing](#scoped-routing)
226* [Routing Events](#routing-events)
227* [Configuration](#configuration)
228* [URL Matching](#url-matching)
229* [URL Params](#url-params)
230* [Route Recursion](#route-recursion)
231* [Async Routing](#async-routing)
232* [Resources](#resources)
233* [History API](#history-api)
234* [Instance Methods](#instance-methods)
235* [Attach Properties to `this`](#attach-to-this)
236* [HTTP Streaming and Body Parsing](#http-streaming-body-parsing)
237
238<a name="constructor"></a>
239## Constructor
240
241``` js
242 var router = Router(routes);
243```
244
245<a name="routing-table"></a>
246## Routing Table
247
248An object literal that contains nested route definitions. A potentially nested set of key/value pairs. The keys in the object literal represent each potential part of the URL. The values in the object literal contain references to the functions that should be associated with them. *bark* and *meow* are two functions that you have defined in your code.
249
250``` js
251 //
252 // Assign routes to an object literal.
253 //
254 var routes = {
255 //
256 // a route which assigns the function `bark`.
257 //
258 '/dog': bark,
259 //
260 // a route which assigns the functions `meow` and `scratch`.
261 //
262 '/cat': [meow, scratch]
263 };
264
265 //
266 // Instantiate the router.
267 //
268 var router = Router(routes);
269```
270
271<a name="adhoc-routing"></a>
272## Adhoc Routing
273
274When developing large client-side or server-side applications it is not always possible to define routes in one location. Usually individual decoupled components register their own routes with the application router. We refer to this as _Adhoc Routing._ Lets take a look at the API `director` exposes for adhoc routing:
275
276**Client-side Routing**
277
278``` js
279 var router = new Router().init();
280
281 router.on('/some/resource', function () {
282 //
283 // Do something on `/#/some/resource`
284 //
285 });
286```
287
288**HTTP Routing**
289
290``` js
291 var router = new director.http.Router();
292
293 router.get(/\/some\/resource/, function () {
294 //
295 // Do something on an GET to `/some/resource`
296 //
297 });
298```
299
300<a name="scoped-routing"></a>
301## Scoped Routing
302
303In large web appliations, both [Client-side](#client-side) and [Server-side](#server-side), routes are often scoped within a few individual resources. Director exposes a simple way to do this for [Adhoc Routing](#adhoc-routing) scenarios:
304
305``` js
306 var router = new director.http.Router();
307
308 //
309 // Create routes inside the `/users` scope.
310 //
311 router.path(/\/users\/(\w+)/, function () {
312 //
313 // The `this` context of the function passed to `.path()`
314 // is the Router itself.
315 //
316
317 this.post(function (id) {
318 //
319 // Create the user with the specified `id`.
320 //
321 });
322
323 this.get(function (id) {
324 //
325 // Retreive the user with the specified `id`.
326 //
327 });
328
329 this.get(/\/friends/, function (id) {
330 //
331 // Get the friends for the user with the specified `id`.
332 //
333 });
334 });
335```
336
337<a name="routing-events"></a>
338## Routing Events
339
340In `director`, a "routing event" is a named property in the [Routing Table](#routing-table) which can be assigned to a function or an Array of functions to be called when a route is matched in a call to `router.dispatch()`.
341
342* **on:** A function or Array of functions to execute when the route is matched.
343* **before:** A function or Array of functions to execute before calling the `on` method(s).
344
345**Client-side only**
346
347* **after:** A function or Array of functions to execute when leaving a particular route.
348* **once:** A function or Array of functions to execute only once for a particular route.
349
350<a name="configuration"></a>
351## Configuration
352
353Given the flexible nature of `director` there are several options available for both the [Client-side](#client-side) and [Server-side](#server-side). These options can be set using the `.configure()` method:
354
355``` js
356 var router = new director.Router(routes).configure(options);
357```
358
359The `options` are:
360
361* **recurse:** Controls [route recursion](#route-recursion). Use `forward`, `backward`, or `false`. Default is `false` Client-side, and `backward` Server-side.
362* **strict:** If set to `false`, then trailing slashes (or other delimiters) are allowed in routes. Default is `true`.
363* **async:** Controls [async routing](#async-routing). Use `true` or `false`. Default is `false`.
364* **delimiter:** Character separator between route fragments. Default is `/`.
365* **notfound:** A function to call if no route is found on a call to `router.dispatch()`.
366* **on:** A function (or list of functions) to call on every call to `router.dispatch()` when a route is found.
367* **before:** A function (or list of functions) to call before every call to `router.dispatch()` when a route is found.
368
369**Client-side only**
370
371* **resource:** An object to which string-based routes will be bound. This can be especially useful for late-binding to route functions (such as async client-side requires).
372* **after:** A function (or list of functions) to call when a given route is no longer the active route.
373* **html5history:** If set to `true` and client supports `pushState()`, then uses HTML5 History API instead of hash fragments. See [History API](#history-api) for more information.
374* **run_handler_in_init:** If `html5history` is enabled, the route handler by default is executed upon `Router.init()` since with real URIs the router can not know if it should call a route handler or not. Setting this to `false` disables the route handler initial execution.
375
376<a name="url-matching"></a>
377## URL Matching
378
379``` js
380 var router = Router({
381 //
382 // given the route '/dog/yella'.
383 //
384 '/dog': {
385 '/:color': {
386 //
387 // this function will return the value 'yella'.
388 //
389 on: function (color) { console.log(color) }
390 }
391 }
392 });
393```
394
395Routes can sometimes become very complex, `simple/:tokens` don't always suffice. Director supports regular expressions inside the route names. The values captured from the regular expressions are passed to your listener function.
396
397``` js
398 var router = Router({
399 //
400 // given the route '/hello/world'.
401 //
402 '/hello': {
403 '/(\\w+)': {
404 //
405 // this function will return the value 'world'.
406 //
407 on: function (who) { console.log(who) }
408 }
409 }
410 });
411```
412
413``` js
414 var router = Router({
415 //
416 // given the route '/hello/world/johny/appleseed'.
417 //
418 '/hello': {
419 '/world/?([^\/]*)\/([^\/]*)/?': function (a, b) {
420 console.log(a, b);
421 }
422 }
423 });
424```
425
426<a name="url-params"></a>
427## URL Parameters
428
429When you are using the same route fragments it is more descriptive to define these fragments by name and then use them in your [Routing Table](#routing-table) or [Adhoc Routes](#adhoc-routing). Consider a simple example where a `userId` is used repeatedly.
430
431``` js
432 //
433 // Create a router. This could also be director.cli.Router() or
434 // director.http.Router().
435 //
436 var router = new director.Router();
437
438 //
439 // A route could be defined using the `userId` explicitly.
440 //
441 router.on(/([\w-_]+)/, function (userId) { });
442
443 //
444 // Define a shorthand for this fragment called `userId`.
445 //
446 router.param('userId', /([\\w\\-]+)/);
447
448 //
449 // Now multiple routes can be defined with the same
450 // regular expression.
451 //
452 router.on('/anything/:userId', function (userId) { });
453 router.on('/something-else/:userId', function (userId) { });
454```
455
456<a name="route-recursion"></a>
457## Route Recursion
458
459Can be assigned the value of `forward` or `backward`. The recurse option will determine the order in which to fire the listeners that are associated with your routes. If this option is NOT specified or set to null, then only the listeners associated with an exact match will be fired.
460
461### No recursion, with the URL /dog/angry
462
463``` js
464 var routes = {
465 '/dog': {
466 '/angry': {
467 //
468 // Only this method will be fired.
469 //
470 on: growl
471 },
472 on: bark
473 }
474 };
475
476 var router = Router(routes);
477```
478
479### Recursion set to `backward`, with the URL /dog/angry
480
481``` js
482 var routes = {
483 '/dog': {
484 '/angry': {
485 //
486 // This method will be fired first.
487 //
488 on: growl
489 },
490 //
491 // This method will be fired second.
492 //
493 on: bark
494 }
495 };
496
497 var router = Router(routes).configure({ recurse: 'backward' });
498```
499
500### Recursion set to `forward`, with the URL /dog/angry
501
502``` js
503 var routes = {
504 '/dog': {
505 '/angry': {
506 //
507 // This method will be fired second.
508 //
509 on: growl
510 },
511 //
512 // This method will be fired first.
513 //
514 on: bark
515 }
516 };
517
518 var router = Router(routes).configure({ recurse: 'forward' });
519```
520
521### Breaking out of recursion, with the URL /dog/angry
522
523``` js
524 var routes = {
525 '/dog': {
526 '/angry': {
527 //
528 // This method will be fired first.
529 //
530 on: function() { return false; }
531 },
532 //
533 // This method will not be fired.
534 //
535 on: bark
536 }
537 };
538
539 //
540 // This feature works in reverse with recursion set to true.
541 //
542 var router = Router(routes).configure({ recurse: 'backward' });
543```
544
545<a name="async-routing"></a>
546## Async Routing
547
548Before diving into how Director exposes async routing, you should understand [Route Recursion](#route-recursion). At it's core route recursion is about evaluating a series of functions gathered when traversing the [Routing Table](#routing-table).
549
550Normally this series of functions is evaluated synchronously. In async routing, these functions are evaluated asynchronously. Async routing can be extremely useful both on the client-side and the server-side:
551
552* **Client-side:** To ensure an animation or other async operations (such as HTTP requests for authentication) have completed before continuing evaluation of a route.
553* **Server-side:** To ensure arbitrary async operations (such as performing authentication) have completed before continuing the evaluation of a route.
554
555The method signatures for route functions in synchronous and asynchronous evaluation are different: async route functions take an additional `next()` callback.
556
557### Synchronous route functions
558
559``` js
560 var router = new director.Router();
561
562 router.on('/:foo/:bar/:bazz', function (foo, bar, bazz) {
563 //
564 // Do something asynchronous with `foo`, `bar`, and `bazz`.
565 //
566 });
567```
568
569### Asynchronous route functions
570
571``` js
572 var router = new director.http.Router().configure({ async: true });
573
574 router.on('/:foo/:bar/:bazz', function (foo, bar, bazz, next) {
575 //
576 // Go do something async, and determine that routing should stop
577 //
578 next(false);
579 });
580```
581
582<a name="resources"></a>
583## Resources
584
585**Available on the Client-side only.** An object literal containing functions. If a host object is specified, your route definitions can provide string literals that represent the function names inside the host object. A host object can provide the means for better encapsulation and design.
586
587``` js
588
589 var router = Router({
590
591 '/hello': {
592 '/usa': 'americas',
593 '/china': 'asia'
594 }
595
596 }).configure({ resource: container }).init();
597
598 var container = {
599 americas: function() { return true; },
600 china: function() { return true; }
601 };
602
603```
604
605<a name="history-api"></a>
606## History API
607
608**Available on the Client-side only.** Director supports using HTML5 History API instead of hash fragments for navigation. To use the API, pass `{html5history: true}` to `configure()`. Use of the API is enabled only if the client supports `pushState()`.
609
610Using the API gives you cleaner URIs but they come with a cost. Unlike with hash fragments your route URIs must exist. When the client enters a page, say http://foo.com/bar/baz, the web server must respond with something meaningful. Usually this means that your web server checks the URI points to something that, in a sense, exists, and then serves the client the JavaScript application.
611
612If you're after a single-page application you can not use plain old `<a href="/bar/baz">` tags for navigation anymore. When such link is clicked, web browsers try to ask for the resource from server which is not of course desired for a single-page application. Instead you need to use e.g. click handlers and call the `setRoute()` method yourself.
613
614<a name="attach-to-this"></a>
615## Attach Properties To `this`
616
617Generally, the `this` object bound to route handlers, will contain the request in `this.req` and the response in `this.res`. One may attach additional properties to `this` with the `router.attach` method:
618
619```js
620 var director = require('director');
621
622 var router = new director.http.Router().configure(options);
623
624 //
625 // Attach properties to `this`
626 //
627 router.attach(function () {
628 this.data = [1,2,3];
629 });
630
631 //
632 // Access properties attached to `this` in your routes!
633 //
634 router.get('/hello', function () {
635 this.res.writeHead(200, { 'content-type': 'text/plain' });
636
637 //
638 // Response will be `[1,2,3]`!
639 //
640 this.res.end(this.data);
641 });
642```
643
644This API may be used to attach convenience methods to the `this` context of route handlers.
645
646<a name="http-streaming-body-parsing">
647## HTTP Streaming and Body Parsing
648
649When you are performing HTTP routing there are two common scenarios:
650
651* Buffer the request body and parse it according to the `Content-Type` header (usually `application/json` or `application/x-www-form-urlencoded`).
652* Stream the request body by manually calling `.pipe` or listening to the `data` and `end` events.
653
654By default `director.http.Router()` will attempt to parse either the `.chunks` or `.body` properties set on the request parameter passed to `router.dispatch(request, response, callback)`. The router instance will also wait for the `end` event before firing any routes.
655
656**Default Behavior**
657
658``` js
659 var director = require('director');
660
661 var router = new director.http.Router();
662
663 router.get('/', function () {
664 //
665 // This will not work, because all of the data
666 // events and the end event have already fired.
667 //
668 this.req.on('data', function (chunk) {
669 console.log(chunk)
670 });
671 });
672```
673
674In [flatiron][2], `director` is used in conjunction with [union][3] which uses a `BufferedStream` proxy to the raw `http.Request` instance. [union][3] will set the `req.chunks` property for you and director will automatically parse the body. If you wish to perform this buffering yourself directly with `director` you can use a simple request handler in your http server:
675
676``` js
677 var http = require('http'),
678 director = require('director');
679
680 var router = new director.http.Router();
681
682 var server = http.createServer(function (req, res) {
683 req.chunks = [];
684 req.on('data', function (chunk) {
685 req.chunks.push(chunk.toString());
686 });
687
688 router.dispatch(req, res, function (err) {
689 if (err) {
690 res.writeHead(404);
691 res.end();
692 }
693
694 console.log('Served ' + req.url);
695 });
696 });
697
698 router.post('/', function () {
699 this.res.writeHead(200, { 'Content-Type': 'application/json' })
700 this.res.end(JSON.stringify(this.req.body));
701 });
702```
703
704**Streaming Support**
705
706If you wish to get access to the request stream before the `end` event is fired, you can pass the `{ stream: true }` options to the route.
707
708``` js
709 var director = require('director');
710
711 var router = new director.http.Router();
712
713 router.get('/', { stream: true }, function () {
714 //
715 // This will work because the route handler is invoked
716 // immediately without waiting for the `end` event.
717 //
718 this.req.on('data', function (chunk) {
719 console.log(chunk);
720 });
721 });
722```
723
724<a name="instance-methods"></a>
725## Instance methods
726
727### configure(options)
728* `options` {Object}: Options to configure this instance with.
729
730Configures the Router instance with the specified `options`. See [Configuration](#configuration) for more documentation.
731
732### param(token, matcher)
733* token {string}: Named parameter token to set to the specified `matcher`
734* matcher {string|Regexp}: Matcher for the specified `token`.
735
736Adds a route fragment for the given string `token` to the specified regex `matcher` to this Router instance. See [URL Parameters](#url-parameters) for more documentation.
737
738### on(method, path, route)
739* `method` {string}: Method to insert within the Routing Table (e.g. `on`, `get`, etc.).
740* `path` {string}: Path within the Routing Table to set the `route` to.
741* `route` {function|Array}: Route handler to invoke for the `method` and `path`.
742
743Adds the `route` handler for the specified `method` and `path` within the [Routing Table](#routing-table).
744
745### path(path, routesFn)
746* `path` {string|Regexp}: Scope within the Routing Table to invoke the `routesFn` within.
747* `routesFn` {function}: Adhoc Routing function with calls to `this.on()`, `this.get()` etc.
748
749Invokes the `routesFn` within the scope of the specified `path` for this Router instance.
750
751### dispatch(method, path[, callback])
752* method {string}: Method to invoke handlers for within the Routing Table
753* path {string}: Path within the Routing Table to match
754* callback {function}: Invoked once all route handlers have been called.
755
756Dispatches the route handlers matched within the [Routing Table](#routing-table) for this instance for the specified `method` and `path`.
757
758### mount(routes, path)
759* routes {object}: Partial routing table to insert into this instance.
760* path {string|Regexp}: Path within the Routing Table to insert the `routes` into.
761
762Inserts the partial [Routing Table](#routing-table), `routes`, into the Routing Table for this Router instance at the specified `path`.
763
764## Instance methods (Client-side only)
765
766### init([redirect])
767* `redirect` {String}: This value will be used if '/#/' is not found in the URL. (e.g., init('/') will resolve to '/#/', init('foo') will resolve to '/#foo').
768
769Initialize the router, start listening for changes to the URL.
770
771### getRoute([index])
772* `index` {Number}: The hash value is divided by forward slashes, each section then has an index, if this is provided, only that section of the route will be returned.
773
774Returns the entire route or just a section of it.
775
776### setRoute(route)
777* `route` {String}: Supply a route value, such as `home/stats`.
778
779Set the current route.
780
781### setRoute(start, length)
782* `start` {Number} - The position at which to start removing items.
783* `length` {Number} - The number of items to remove from the route.
784
785Remove a segment from the current route.
786
787### setRoute(index, value)
788* `index` {Number} - The hash value is divided by forward slashes, each section then has an index.
789* `value` {String} - The new value to assign the the position indicated by the first parameter.
790
791Set a segment of the current route.
792
793<a name="faq"></a>
794# Frequently Asked Questions
795
796## What About SEO?
797
798Is using a Client-side router a problem for SEO? Yes. If advertising is a requirement, you are probably building a "Web Page" and not a "Web Application". Director on the client is meant for script-heavy Web Applications.
799
800# Licence
801
802(The MIT License)
803
804Copyright (c) 2010 Nodejitsu Inc. <http://www.twitter.com/nodejitsu>
805
806Permission 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:
807
808The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
809
810THE 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.
811
812[0]: http://github.com/flatiron/director
813[1]: https://github.com/flatiron/director/blob/master/build/director-1.0.7.min.js
814[2]: http://github.com/flatiron/flatiron
815[3]: http://github.com/flatiron/union