├── .gitignore ├── .npmignore ├── .travis.yml ├── History.md ├── LICENSE ├── README.md ├── bin ├── template │ ├── History.md │ ├── LICENSE │ ├── README.md │ ├── server.js │ ├── server_coffee.js │ ├── test │ │ ├── timbits-test.coffee │ │ └── timbits-test.js │ ├── timbit.coffee │ └── timbit.js └── timbits ├── examples ├── helpers │ └── hello.js ├── server-redis.coffee ├── server.js ├── timbits │ ├── cherry.js │ ├── chocolate.js │ ├── dutchie.js │ ├── plain.js │ └── post.js └── views │ ├── cherry │ └── default.hjs │ ├── chocolate │ ├── alternate-view.hjs │ └── default.hjs │ ├── plain │ └── default.hjs │ └── post │ └── default.hjs ├── lib ├── templates │ ├── help.hjs │ ├── test.hjs │ └── timbit-help.hjs └── timbits.js ├── package.json ├── resources ├── images │ ├── accept.png │ ├── brick_add.png │ ├── cancel.png │ ├── error.png │ └── eye.png ├── javascript │ ├── timbits.csi.js │ └── timbits.csi.min.js └── stylesheets │ └── timbits.css └── test └── timbits-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | *.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .git* 3 | *.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | notifications: 5 | email: 6 | recipients: 7 | - smrt@postmedia.com 8 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.7.3 / 2014-12-08 2 | ================== 3 | * Removed use of twitter API v1.0 in templates generated by 'timbits g' and README or examples 4 | * Added support for all methods on timbit endpoints, default support for GET enabled for backward compatibility 5 | * Example timbit added to demonstrate http POST method use 6 | * Testing updated to check and use GET/POST depending on which the timbit supports. 7 | 8 | 0.7.2 / 2014-05-12 9 | ================== 10 | * Dependency update to allow pantry v0.7.x 11 | * Dependency update to allow request v2.36.x 12 | * Dependency update to allow express v3.10.x 13 | * Dependency update to allow hjs v0.0.6 14 | * Dependency update to allow hogan.js v3.0.x 15 | * Dependency update to allow run v1.2.x 16 | * Dependency update to allow mocha v1.20.x 17 | * Dependency update to allow should v4.0.x 18 | * Examples - added parameter and example help for dutchie timbit 19 | * Added dependency for should-http v0.0.2 augments should v4.x 20 | * Updated node engine support from 0.8.x to up to 0.10.x 21 | * Updated npm to 1.x 22 | * Changed test to require '-c' or '--coffee' to use coffee-script 23 | 24 | 0.7.1 / 2013-07-16 25 | ================== 26 | * Dependency update to allow pantry v0.5.x 27 | * Now uses Wordpress API instead of Twitter for examples 28 | 29 | 0.7.0 / 2013-06-05 30 | ================== 31 | * 0.7.x series should be considered unstable. stable version will be released as 0.8.x 32 | * major rewrite of Timbits in JavaScript (formally CoffeeScript) 33 | * generates JavaScript files by default (CoffeeScript is optional) 34 | * default view engine is now Hogan (formally CoffeeKup) 35 | * dynamic helpers are no longer supported 36 | * sessions are not longer enabled by default 37 | * now uses Winston for logging (formally coloured-log) 38 | * updated documentation to reflect changes 39 | 40 | 0.6.7 / 2012-11-28 41 | ================== 42 | * added trim function when processing environment key/value pairs, fixes issue on Windows when multiple environment variables are concatenated in same string. 43 | 44 | 0.6.6 / 2012-11-08 45 | ================== 46 | * renamed csi libraries to match best practices (dot instead of dash) 47 | 48 | 0.6.5 / 2012-10-23 49 | ================== 50 | * client rendering library revised. no longer uses namespaced includes 51 | 52 | 0.6.4 / 2012-10-23 53 | ================== 54 | 55 | * Add support for responsive client side includes via media attribute 56 | * Modified production flags to return 404 when feature has been disabled 57 | 58 | 0.6.3 / 2012-10-22 59 | ================== 60 | 61 | * Supports new configuration options which allow you to disable automated discovery, help, tests, and json views. 62 | * no longer uses our own custom jsonp-filter package 63 | 64 | 0.6.2 / 2012-10-22 65 | ================== 66 | 67 | * Rewrite of timbits command line in js 68 | * No longer requires coffee-script, mocha or runjs packages to be installed globally 69 | * for projects created via "timbits new", generated package.json will lock down the timbits dependency to the version used to create the project. To upgrade to a newer version of timbits, you'll need to adjust the dependency first 70 | * Added support for numerous mocha options to "timbits test" command 71 | 72 | 0.6.1 / 2012-10-16 73 | ================== 74 | 75 | * Updated read me to reflect global dependencies for command line use of timbits 76 | * Modified spawn of child process to use the node-which lib for proper PATHEXTS of executables on Windows and Linux 77 | 78 | 0.6.0 / 2012-10-05 79 | ================== 80 | 81 | * Updated dependencies to latest libraries 82 | * Now requires node v0.8.x 83 | * timbits are now loaded before server is started (instead of async) 84 | * Switch to mocha for testing (was using vows) 85 | * Completely revised automated testing with option for more extensive tests 86 | * Added test option to timbits command line 87 | * timbits s[erver] and t[est] will now load environment variables from a .env file if present 88 | 89 | 0.5.14 / 2012-10-05 90 | =================== 91 | * Removed connect -esi, -assets, -less as requirements (but you can still add them to your project manually if needed) 92 | 93 | 0.5.13 / 2012-10-05 94 | =================== 95 | 96 | * Lock down dependency versions prior to v0.6.x 97 | 98 | 0.5.12 / 2012-06-28 99 | =================== 100 | 101 | * Added alias name support to timbit parameters 102 | 103 | 0.5.10 / 2012-06-07 104 | =================== 105 | 106 | * Added error handling to fetch helper method 107 | 108 | 0.5.9 / 2012-05-28 109 | ================== 110 | 111 | * Better handling of errors when eating or rendering 112 | * Removed auto-capitalization css from help views 113 | 114 | 0.5.8 / 2012-05-24 115 | ================== 116 | 117 | * Corrected method of returning remove render JSON 118 | 119 | 0.5.7 / 2012-05-23 120 | ================== 121 | 122 | * Added support for onload event to client side esi/csi processing 123 | * Allow for $(QUERY\_STRING{'name'}) syntax in client side esi/csi processing 124 | 125 | 0.5.6 / 2012-05-22 126 | ================== 127 | 128 | * Added ability to access to pantry directly from within a Timbit 129 | 130 | 0.5.5 / 2012-05-18 131 | ================== 132 | 133 | * Added the ability render timbits client side via JSONP and callback parameter 134 | * Removed the automatic insert of pantry URIs into context due to security concerns 135 | 136 | 0.5.4 / 2012-04-25 137 | ================== 138 | 139 | * Added the ability to specify the name of the default view via .defaultView property 140 | 141 | 0.5.3 / 2012-04-16 142 | ================== 143 | 144 | * Fixed bug with minimum node version for previous changes that require node 0.6.1 or higher 145 | * Added limitation to express version installed < 3.0 as this is in alpha and breaks timbits 146 | 147 | 0.5.2 / 2012-04-12 148 | ================== 149 | 150 | * Fixed bug with making directories in bin/timbits 151 | 152 | 0.5.1 / 2012-04-12 153 | ================== 154 | 155 | * Changes to bin/timbits for CoffeScript v.1.3.x support 156 | 157 | 0.5.0 / 2012-03-21 158 | ================== 159 | 160 | * Unstable experimental release (uses unstable pantry v0.3.x) 161 | * Exposes pantry for additional configuration and use of optional storage engines 162 | 163 | 0.4.2 / 2012-03-21 164 | ================== 165 | 166 | * Updated package to prevent use of pantry > 0.3.0 167 | 168 | 0.4.1 / 2012-02-21 169 | ================== 170 | 171 | * Added support for new config.base parameter to allow nested timbit servers 172 | * timbits command line updated to use OS agnostic copy 173 | * npm init and install removed from command line (doesn't work on windows) 174 | 175 | 0.4.0 / 2012-02-02 176 | ================== 177 | 178 | * Removed client side rendering (for now) 179 | * Switched out kitkat for vows 180 | * Supports alternate view engines 181 | * No error if helpers aren't defined (missing folder) 182 | 183 | 0.3.3 / 2011-11-23 184 | ================== 185 | 186 | * ignore empty parameter values 187 | 188 | 0.3.2 / 2011-11-18 189 | ================== 190 | 191 | * added support for server-side less compilation 192 | 193 | 0.3.1 / 2011-11-10 194 | ================== 195 | 196 | * added support for json directory of available timbits 197 | * Timbit fetch method will store pantry result in array if context contains existing entry 198 | * Timbit fetch method will store the requested uri in context\[name_uri\] (or array if context contains existing entry) 199 | * added initial support for express sessions 200 | * routing now works for both get and post methods 201 | 202 | 0.3.0 / 2011-10-17 203 | ================== 204 | 205 | * initial support for dynamic helpers 206 | 207 | 0.2.0 / 2011-10-17 208 | ================== 209 | 210 | * official stable release of v0.2 211 | 212 | 0.2.0beta5 / 2011-10-04 213 | ======================= 214 | 215 | * complete rewrite of client side rendering 216 | 217 | 0.2.0beta4 / 2011-09-15 218 | ======================= 219 | 220 | * upgraded pantry to v0.2.0beta2 221 | * changed view_base to viewBase 222 | * added parameter data type validation 223 | * added downstream caching headers via maxAge 224 | * npm init is now run after new project has been generated 225 | * parameters are converted to lower case to ensure they are not case sensitive 226 | * fixed bug with conflicting path variable 227 | * added append to body if no timbit_id is provided when rendering client side 228 | 229 | 0.2.0beta3 / 2011-09-14 230 | ======================= 231 | 232 | * upgraded pantry to v0.2.0beta 233 | * fixed issue with test page host name. Closes #8 234 | 235 | 0.2.0beta2 / 2011-09-13 236 | ======================= 237 | 238 | * new projects now depend on installed version of timbits 239 | * package.json for new projects now include project name 240 | * support for timbits -v parameter 241 | * added Timbit.log for logging/debug support 242 | * new projects now support continuos testing via kitkat 243 | 244 | 0.2.0beta / 2011-09-09 245 | ====================== 246 | 247 | * working towards a stable production 0.2.0 release 248 | 249 | 0.1.3 / 2011-09-09 250 | ================== 251 | 252 | * command line for new projects and code generation 253 | * dynamic test pages 254 | * upgraded view engine to CoffeeKup 0.3.0 255 | * easier sharing of views between timbits via Timbit.view_base 256 | * revised fetch method by removing 'key' parameter 257 | * support for Timbits created in JavaScript 258 | 259 | 0.1.2 / 2011-08-25 260 | ================== 261 | 262 | * dynamic help has been styled 263 | * support for client side rendering 264 | * initial support for automated testing 265 | * better logging 266 | 267 | 0.1.1 / 2011-08-23 268 | ================== 269 | 270 | * request and response separated from context (to support optional client side rendering in a later release) 271 | * parameter validation 272 | * dynamic help pages 273 | * customized routes are no longer supported 274 | * examples updated to reflect changes 275 | 276 | 0.1.0 / 2011-08-22 277 | ================== 278 | 279 | * Official 0.1 release 280 | 281 | 0.0.7 / 2011-08-18 282 | ================== 283 | 284 | * Needed to revert to CoffeeKup 0.2.3 in order to support deploying to node < 0.4.7 285 | 286 | 0.0.6 / 2011-08-15 287 | ================== 288 | 289 | * Extracted Story, List, and Syndication examples to separate project (timbits-example) 290 | * Utilizes kitkat for testing 291 | * Added test cases 292 | * Utilizes env for port number if available 293 | * Application options object replaces parameters 294 | * Feature rich HTML5 story template in examples 295 | * Added List and Syndication widgets to examples 296 | * Incorporated connect-esi packaged 297 | 298 | 0.0.5 / 2011-08-04 299 | ================== 300 | 301 | * Now uses Pantry for JSON/XML data retrieval 302 | * Upgraded to Express 2.4.3 and CoffeeKup 0.3.0beta 303 | * Examples updated to account for the above changes 304 | 305 | 0.0.4 / 2011-07-14 306 | ================== 307 | 308 | * Updated package.json and published the package 309 | 310 | 0.0.3 / 2011-07-14 311 | ================== 312 | 313 | * Fix some typos 314 | * Started documentation 315 | * Implement default help.coffee file 316 | 317 | 0.0.2 / 2011-07-13 318 | ================== 319 | 320 | * Created sample timbits of varying complexity 321 | * Major refactoring as we develop our examples 322 | * Reworked areas towards convention over configuration 323 | * Uses the CoffeeKup view engine by default 324 | * Created Story timbit to be used as a real world prototype 325 | 326 | 0.0.1 / 2011-07-05 327 | ================== 328 | 329 | * Initial release 330 | 331 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 Postmedia Network Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Timbits 2 | Widget framework based on Express 3 | 4 | ## Introduction 5 | 6 | Timbits is an attempt to build an easy, and reusable widget framework on top of Express. These widgets are meant to render independent HTML snippets based on REST based JSON/XML data sources, and brought together on a page via ESI (Edge Side Includes), or our own proprietary CSI (Client Side Includes). 7 | 8 | It's primarily meant to serve internal purposes as Postmedia Network Inc, however, it is being open sourced under the MIT License. Others may find some use for what we are doing, and still others may be able to help turn this into a more generic, and useful solution via contributions. 9 | 10 | Constructive criticism is encouraged. 11 | 12 | ## Installing 13 | 14 | Just grab [node.js](http://nodejs.org/#download) and [npm](http://github.com/isaacs/npm) and you're set: 15 | 16 | npm install timbits -g 17 | 18 | Timbits is simplistic, and finds most of it's power by running on top of some very cool node libraries such as [Express](http://expressjs.com/) and [Hogan](http://twitter.github.com/hogan.js/). It also uses our [Pantry](https://github.com/Postmedia/pantry) package for efficient utilization of JSON/XML data sources. 19 | 20 | ## Changes 21 | 22 | We completed a significant rewrite of Timbits, these changes occur between version 0.6.7 and 0.7 release of timbits. For those using previous versions of Timbits here is a quick rundown of the changes. 23 | 24 | * major rewrite of Timbits source to JavaScript (formerly CoffeeScript) 25 | * generates JavaScript files by default (CoffeeScript is optional) 26 | * default view engine is now Hogan (formerly CoffeeKup) 27 | * dynamic helpers are no longer supported 28 | * sessions are no longer enabled by default 29 | * uses Winston for logging (formerly coloured-log) 30 | 31 | I'll be making a post on my [blog](http://mred9.com) providing more details on these changes, and the reasoning behind them. 32 | 33 | Please be aware that the 0.7.x series should be considered experimental, and not for production use. Once we've stabilized this rewrite it will be released as v0.8.x 34 | 35 | ### Running previous versions on 0.7 36 | 37 | Projects using timbits prior to v0.7 based on CoffeeScript, and Coffeecup require a few changes in order to run. The instructions here provide the details required to get a simple timbit functional. 38 | 39 | There are two specific modifications required, the first is to include a dependency for CoffeeScript, and Coffeecup in your timbits package.json file: 40 | 41 | "dependencies": { 42 | "coffee-script": "~1.3.3", 43 | "coffeecup": "~0.3.20", 44 | ..... additional dependencies here 45 | } 46 | 47 | The second change is to the server.js file, you need to do three things: 48 | * require coffee-script 49 | * specify the engine parameter when instantiating the server object 50 | * set the rendering method to handle coffeecup templates. 51 | 52 | The structure of the main server.js file could look like this: 53 | 54 | require('coffee-script'); 55 | 56 | var timbits = require('timbits'); 57 | var server = timbits.serve({ engine: 'coffee' }); 58 | 59 | server.engine( 'coffee', require('coffeecup').__express ); 60 | 61 | That is all it will take, unless of course you are using dynamic view helpers, for that we need to make a change to the individual timbits themselves. 62 | 63 | Let's suppose you had a helper file helpers/headlines.coffee in your timbits project, in versions prior to 0.7, timbits dynamically loaded all helpers in the 'helpers' path. These helpers were automatically loaded into the context based on the helper filename, so the example mentioned would load any helper functionality under context.headlines, now you must do this manually. 64 | 65 | Include your helpers in each timbit that requires them, at the start of the fetch method. Assuming for example the helper file is headlines.coffee, you would use: 66 | 67 | @fetch req, res, context, options, => 68 | # Add helpers manually at beginning of fetch 69 | context.headlines = require("../helpers/headlines.coffee"); 70 | 71 | 72 | ## Using 73 | 74 | The structure of a Timbits application is fairly simple. To start, you need a two lines in your main server.js file. 75 | 76 | var timbits = require('timbits'); 77 | var server = timbits.serve(); 78 | 79 | Timbits, like many other frameworks, prefers convention over configuration. It will look for specific folders upon startup, and if you manually create your project (vs using the Timbits command line to generate a new project) you should create the following folders. 80 | 81 | * /public - images, javascript, stylesheets etc. 82 | * /timbits - this is where we place the individual timbit (widget) files 83 | * /views - views for a particular timbit are placed in a subfolder of the same name 84 | 85 | When you start the server, it will automatically load all the timbits found in the /timbits folder. The name of the timbit is determined by the name of the file, and that name in turn determines the default route (/name/:view?), and the default view (/views/name/default.coffee). Aside from the location, and name of the timbits, the rest is customizable as shown in the examples. 86 | 87 | Timbits can be created in JavaScript (default), or CoffeeScript. This documentation will assume JavaScript. 88 | 89 | The simplest of timbits takes the following form: 90 | 91 | // Load the timbits module 92 | var timbits = require('../../lib/timbits'); 93 | 94 | // create and export the timbit 95 | var timbit = module.exports = new timbits.Timbit(); 96 | 97 | That's it! If you created this as /timbits/test.js, and placed a default view at /views/test/default.hjs, you could "eat" this timbit by going to /test in a browser. See the "plain" timbit for an example of this. 98 | 99 | ## How it works 100 | 101 | When you start up a Timbits based project, the framework will dynamically load each timbit from the /timbits/ folder. A timbit is 102 | nothing more than a .js, or .coffee file which creates, and exports a new instance of the Timbit class. 103 | 104 | This Timbit class has one very important method named "eat" which is the entry point for every request. This method is intended to 105 | be overwritten. When called, the framework will pass in the following three variables 106 | 107 | * req - the http request object 108 | * res - the http response object 109 | * context - a hash of data properties 110 | 111 | The req, and res parameters come straight from Express.js and are documented [here](http://expressjs.com/guide.html). 112 | The context object originates within Timbits, and will initially contain the following properties: 113 | 114 | * name - the name of the timbit being executed 115 | * view - the name of the specified view 116 | * maxAge - the number of seconds the response should be cached for 117 | 118 | It's important to note that it is possible to override the view, and/or the maxAge during the execution of the request if needed. 119 | 120 | Each timbit by default supports HTTP GET method, it is possible to support one of the alternativce standard HTTP methods as defined in [RFC 2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html), specifically we support "GET", "POST", "PUT", "HEAD", and "DELETE". To specified the methods you want to allow (beyond "GET") define the methods as json properties in the timbit.methods property like this 121 | 122 | timbit.methods = { 'get': false, 'post': true } 123 | 124 | Values are boolean, the methods are not case sensitive, you only need to specify the methods you wish to override from the default settings. Any request for your timbit with a method not specified will result in an HTTP 405 "Method Not Supported" error. 125 | 126 | Each route, querystring, and post parameter is also added to the context as a property. So, if one included ?page=5 in the request url this value would be made available to you as context.page 127 | 128 | Within the eat method, the simplest of implementations will merely call the render method to, well, render the given view using the data found with the context. The render method takes the same three parameters originally passed into the eat method. Within the render method Timbits will pass the context to the view rendering engine so that those values can be 129 | referenced within the view. So a very simple eat method could look like this 130 | 131 | timbit.eat = function(req, res, context) { 132 | this.render(req, res, context); 133 | }; 134 | 135 | In fact, that's exactly what it looks like within the Timbit class, so if you don't override it, the timbit will simply pass on the request parameters for rendering. But that's generally not very useful. The intent in most cases is to add additional data to the context, whether it be generated on the local server, like a timestamp (see the cherry timbit example), or more likely from an external resource like a RSS, or JSON feed/API. 136 | 137 | In most cases you'll need to pull in remote data, so we've incorporated [Pantry](https://github.com/Postmedia/pantry) into Timbits to make fetching (and caching) those resources extra easy. This is available to you via the built in fetch method. 138 | 139 | The fetch method takes five parameters. The first three are the same as the render method: req, res, context. It then adds two additional parameters 140 | 141 | * options - This is a hash of options passed directly to Pantry's fetch method. It identifies the source URI of the resource you want to retrieve along with a host of other options as documented within Pantry 142 | * callback - This is an optional parameter which identifies the function to execute once the request has been completed. It defaults to the render method 143 | 144 | The fetch method will automatically add the retrieved resource to context.data by default. If that isn't acceptable, you can also add a property called "name" to the options parameter to specify an alternate property name to store the data in. This is shown in the included "chocolate" example as follows: 145 | 146 | // specify the data source 147 | var src = { 148 | name: 'ip', 149 | uri: "http://jsonip.com?q=" + context.q + "&rpp=" + context.rpp 150 | }; 151 | 152 | // use the helper method to fetch the data 153 | // timbit.fetch will call timbit.render once we have the data 154 | timbit.fetch(req, res, context, src); 155 | 156 | Be aware that if (using the example above) context.ip already exists, timbits will transform context.ip into an array (if it isn't already), and append the retrieved resource to the array. 157 | 158 | Once the request has been completed, the data added to the context, and no callback is specified, the fetch method will then call the render method for you. If you need to request some additional resources, or perhaps further manipulate the resource once it's been fetched, simply tack on the optional callback parameter, and implement your follow up code. Timbits will pass on the req, res, and context parameters to the callback. Just be sure to call the render method once you're done. 159 | 160 | timbit.fetch(req, res, context, src, function(req, res, context){ 161 | // run your custom code here 162 | // ...... 163 | // ...... 164 | 165 | // and then call render 166 | timbit.render(req, res, context); 167 | }); 168 | 169 | ## Additional Features 170 | 171 | ### Command Line 172 | 173 | Create a new project, generate a timbit (and default view), or run the project with runjs. Run timbits -h for complete list of commands, and options available. 174 | 175 | timbits n[ew] [project] 176 | timbits g[enerate] [timbit] 177 | timbits s[erver] [filename] 178 | timbits t[est] 179 | 180 | ### Running timbits 181 | 182 | There are a number of ways to run timbits, but the easiest approach during development is to utilize the timbits server command. Doing so will by default run the server.js file, although an alternate filename (server.coffee) can be given. The timbits server command has the added benefits of loading up any environment variables from an optional .env file, as well as automatically restarting the server if any files change. 183 | 184 | ### Server Configuration 185 | 186 | The server object can be configured during initialization as needed. The following is a list of possible configuration parameters and their [default value] 187 | 188 | * appName - Friendly name of the timbits application ['Timbits'] 189 | * engine - Default view engine ['hjs'] 190 | * base - A base path for nested servers. e.g. '/my/nested/server' [''] 191 | * port - The server port to listen on if PORT or C9_PORT env labels are not present. [5678] 192 | * home - The physical path to the root project folder [process.cwd()] 193 | * maxAge - The default cache length in seconds [60] 194 | * discovery - Enables you to serve out your library definition via /timbits/json [true] 195 | * help - Enables the automated help pages via /timbits/help and /[name]/help [true] 196 | * test - Enables the automated test pages via /timbits/test and /[name]/test 197 | * json - Enables the built in json view for timbits via /[name]/json [true] 198 | * jsonp - Enables jsonp requests to json resources 199 | 200 | The following example shows how to run a nested application using the [coffeecup](https://github.com/gradus/coffeecup) view engine in place of Hogan and disables the automated test routes. 201 | 202 | var timbits = require('timbits'); 203 | var server = timbits.serve({appName: 'My Nested Timbits', engine: 'coffee', base: '/nested/app', test: false}); 204 | server.engine('coffee', require('coffeecup').__express); 205 | console.log("Press Ctrl+C to Exit"); 206 | 207 | ### Parameter declaration and validation 208 | 209 | For each timbit, you can define a list of parameters which are automatically validated during execution. This also powers the automated help, test, and discovery functions (see further below). Parameter attributes you can manipulate are: 210 | 211 | * alias - an alternate name for the parameter 212 | * description 213 | * type - data type expected, one of String (default), Number, Boolean, or Date 214 | * default - default value to use if value not specified 215 | * multiple - true/false (defaults to false), indicates whether multiple values are allowed 216 | * required - true/false (defaults to false), indicates whether this is a required parameter 217 | * values - an array of possible values 218 | * strict - true/false (defaults to false), indicates whether the value must be one of the defined possible values 219 | 220 | Example (from plain) 221 | 222 | timbit.params = { 223 | who: {description: 'Name of person to greet', alias: 'name', default: 'Anonymous', multiple: false, required: false, strict: false, values: ['Ed', 'World']} 224 | year: {description: 'To test multi parameters and drive Kevin crazy', type: 'Number', values: [1999, 2011]} 225 | } 226 | 227 | ### Dynamic Help 228 | 229 | Along with the list of parameters, one can also augment a timbit definition with a description (timbit.about), and a list of valid examples (timbit.examples). 230 | 231 | Example (from plain) 232 | 233 | timbit.params = { 234 | who: { 235 | description: 'Name of person to greet', 236 | "default": 'Anonymous', 237 | multiple: false, 238 | required: false, 239 | strict: false, 240 | values: ['Ed', 'World'] 241 | }, 242 | retro: { 243 | description: 'To test multi parameters and drive Kevin crazy', 244 | type: 'Boolean' 245 | } 246 | }; 247 | 248 | Together these all help power the automated, dynamic help page for each timbit, which can be found using the hardcoded 'help' view. e.g. /plain/help 249 | 250 | There is also a built in help index that for any timbits project located at /timbits/help 251 | 252 | ### Dynamic Testing 253 | 254 | The examples, and params used to power the help pages are also used to power the automated test pages. Each timbit has a 'test' view which will utilize this information to define, and execute some basic testing of the timbit. e.g. /plain/test 255 | 256 | There is also a master test page located at /timbits/test which will execute tests for all timbits 257 | 258 | Although not overly sophisticated, it will ensure your definitions, examples, and views are valid and compile properly. It is also useful for remote monitoring of production systems. 259 | 260 | Additional functional testing can, and should be implemented via a testing library, such as [mocha](http://visionmedia.github.com/mocha/) 261 | 262 | By default, only a limited number of tests are executed, but you can run through a much larger set of tests by appending /all to the test path. This will generate a much more thorough list of test urls using every possible combination of required parameters, and their sample values, along with each possible sample value for optional parameters. 263 | 264 | e.g. /mytimbit/test/all (run through all tests for mytimbit) 265 | 266 | e.g. /timbits/test/all (run through all tests for all timbits) 267 | 268 | Note that you can provide your own list of test urls in place of the automated ones by simple overriding the generateTests() method for a given timbit, and returning a simply array of virtual paths 269 | 270 | Example: 271 | 272 | timbit.generateTests = function(alltests){ 273 | var tests = [ 274 | '/mytimbit/myview?name=bob', 275 | '/mytimbit/altview?name=sue' 276 | ]; 277 | 278 | if (alltests) 279 | tests.push( 280 | '/mytimbit/myview?name=bob&age=39', 281 | '/mytimbit/myview?name=sue&gender=F' 282 | ); 283 | 284 | return tests; 285 | }; 286 | 287 | If you use the test cases provided by the timbit new project template, you can also run through these same tests (and any others you may want to add) from a terminal via the test command 288 | 289 | timbits test 290 | 291 | Running the test command will invoke the mocha test framework. The command will also load up any environment variables found in the optional .env file before starting the server, and running the tests. 292 | 293 | Just as with running tests via the browser, you can indicate you want to run through all the available tests via the --all flag. In addition, you can pass a number of options to mocha (run timbits help to see the full list). For example, to watch for file changes, and retest use the --watch flag. 294 | 295 | Example: 296 | 297 | timbits test --all --watch 298 | 299 | Or (if you want to minimize typing) 300 | 301 | timbits t -aw 302 | 303 | ### Default view 304 | 305 | By default, the name of the default view is, well, default! You do have the ability to specify something more descriptive if you so desire. Simply set the defaultView property to whatever view name you'd like to use 306 | 307 | // the name default is so booooring. use my fancy view by default instead 308 | timbit.defaultView = 'fancy'; 309 | 310 | ### Sharing of views 311 | 312 | If you have two, or more timbits for which you would like to share views, simply set the viewBase property on the timbit to the name of the timbit who's views you'd like to utilize (see the Dutchie timbit as an example) 313 | 314 | // this timbit should use the views from the chocolate timbit 315 | timbit.viewBase = 'chocolate'; 316 | 317 | ### Downstream Caching 318 | 319 | There is a maxAge property that can set the number of seconds client can/should cache the response for. If not set, the default value will come from the Timbits configuration, which if not set, is currently 60 seconds by default. You can also override this value on a request by request basis by setting the context.maxAge value to whatever is appropriate. 320 | 321 | // timbit should be cached for 5 minutes 322 | timbit.maxAge = 300; 323 | 324 | The maxAge value will be included in two response headers, the standard Cache-Control header as well as a special Edge-Control header (used by Akamai) 325 | 326 | ### Built in JSON view 327 | 328 | If you're interested in rendering the data elsewhere (for example client side), or you simply want to see what's available to you in the current context for development, and debugging purposes, Timbits includes a built in view named "json" that will render the context object as 'application/json'. e.g. /dutchie/json 329 | 330 | ### Dynamic View Helpers 331 | 332 | As of v0.7.0, support for dynamic view helpers has been dropped. 333 | 334 | If you need/want to run your previous timbit project on 0.7 or above, please see the [change notes](https://github.com/postmedia/timbits#changes) from above. Details are provided on how to manually require helpers. 335 | 336 | ### Advanced caching via pantry 337 | 338 | As of [Pantry](https://github.com/Postmedia/pantry) v0.3.0, you are now able to configure alternate storage caches. As of Timbits v0.5.0 one can now control the pantry configuration properties as well as substitute the default MemoryStorage caching via the exported timbits.pantry property. 339 | 340 | var timbits = require('../src/timbits') 341 | ,RedisStorage = require('pantry/lib/pantry-redis'); 342 | 343 | timbits.pantry.storage = new RedisStorage(null, null, null, 'DEBUG'); 344 | timbits.pantry.configure({ 345 | verbosity: 'DEBUG' 346 | }); 347 | 348 | var server = timbits.serve({ 349 | home: __dirname 350 | }); 351 | 352 | ### Client Side Rendering 353 | 354 | Not everyone will be able to utilize an ESI processor in front of their site, or more likely on their development workstation, so we've added a quick and easy way to pull in timbits client side. Timbits supports an optional callback parameter which will package the rendered view as a simple JSONP package. 355 | 356 | Example: 357 | 358 | /chocolate?q=winning&callback=done 359 | 360 | Included with Timbits is a simple client-side JS library (/javascript/timbits.csi.js) which utilizes jQuery to post-process an HTML page for tags containing the data-csi attribute, automatically tacking on the callback parameter when needed, and pulling in the results client side. We've compiled a minimized version as well (/javascript/timbits.csi.min.js) 361 | 362 | Example: 363 | 364 |
365 |
366 | 367 | By default, the CSI library runs in "replace" mode, i.e. the returned html replaces any content within the parent element, but you can control that by setting the data-mode attribute to either "prepend", or "append" 368 | 369 |
370 |

This will be replaced by the results of the include

371 |

372 |
373 |

This will appear below the results of the include

374 |

375 |
376 |

This HTML will appear above the results of the include

377 |

378 | 379 | We've also added support for the dynamic insertion of query string values into your client side includes via the {#name} syntax. For example, the following will grab the query string parameter 'term' from the host page and insert it into the source url prior to making the request. 380 | 381 |
382 | 383 | ### Responsive Rendering 384 | 385 | In an effort to assist with minimizing page sizes (for mobile clients specifically) we've introduced a 'load' event as well as media queries within our timbits.csi.js library. 386 | 387 | To run some javascript once a timbit has been loaded, simply bind to the element's load event like this: 388 | 389 | 394 | 395 | To implement media queries, add a data-media attribute to the element like this: 396 | 397 | 398 | 399 | If you want to get even fancier, you can load (not show, see below) two different views of the same timbit depending on a specific breakpoint as in this example: 400 | 401 |
402 | 403 | 404 | Here's another example which shows some of the power behind this. Say you want to show four blog posts to all users, but an additional six for larger devices. The following will keep default payload small for smartphones while expanding the available content for tablets and desktops. 405 | 406 |
407 |
408 |
409 | 410 | Any valid media query will do. In fact, we also will respond to media query changes (including orientation!) so that as you resize the browser, any csi elements that were skipped due to unmet media queries will load once the media query is valid. 411 | 412 | A couple caveats you should be aware of. First, we will load csi includes based on the media query, but we won't unload them. Secondly, since we depend on the window.matchMedia() method, this doesn't work across all browsers. Specifically Opera, and IE9 or below. On these browsers, timbits csi will ignore the media queries and load each and every include. So you should still use CSS media queries to show/hide elements as if all the content was loaded. 413 | 414 | ### Wordpress Plugin 415 | 416 | For those of you who would like to utilize Timbits within your Wordpress environment, we've created a plugin for that called [wp-timbits](https://github.com/Postmedia/wp-timbits) which provides shortcode and widget support, supports Timbits' auto-discovery feature, includes the csi rendering library (with media query support) and has the ability to turn your Wordpress instance into a Timbits server. 417 | 418 | ## Road Map 419 | 420 | We have a number of items in the pipeline which we believe will provide a lot of power to this platform, such as: 421 | 422 | * Integrated benchmarks 423 | * Real-time data updates via Socket.IO 424 | 425 | 426 | ## Created by 427 | 428 | * Edward de Groot 429 | * Keith Benedict 430 | * Donnie Marges 431 | * Stephen Veerman 432 | * Kevin Gamble 433 | -------------------------------------------------------------------------------- /bin/template/History.md: -------------------------------------------------------------------------------- 1 | 0.0.1 / current date 2 | ==================== 3 | 4 | * Created new Timbits project 5 | -------------------------------------------------------------------------------- /bin/template/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 20XX [Some Company] 2 | -------------------------------------------------------------------------------- /bin/template/README.md: -------------------------------------------------------------------------------- 1 | # Timbits 2 | Built using [Timbits](http://github/com/postmedia/timbits) 3 | 4 | -------------------------------------------------------------------------------- /bin/template/server.js: -------------------------------------------------------------------------------- 1 | var timbits = require('timbits'); 2 | var server = timbits.serve(); 3 | console.log("Press Ctrl+C to Exit"); -------------------------------------------------------------------------------- /bin/template/server_coffee.js: -------------------------------------------------------------------------------- 1 | require('coffee-script'); 2 | var timbits = require('timbits'); 3 | var server = timbits.serve(); 4 | console.log("Press Ctrl+C to Exit"); -------------------------------------------------------------------------------- /bin/template/test/timbits-test.coffee: -------------------------------------------------------------------------------- 1 | # reduce logging levels to provide clean test feedback 2 | process.env.TIMBITS_VERBOSITY = 'critical' 3 | process.env.PANTRY_VERBOSITY = 'critical' 4 | 5 | # Module dependencies. 6 | timbits = require 'timbits' 7 | should = require 'timbits/node_modules/should' 8 | request = require 'timbits/node_modules/request' 9 | 10 | # should http extensions 11 | require 'timbits/node_modules/should-http' 12 | 13 | port = 8785 14 | alltests = process.env.TIMBITS_TEST_WHICH is 'all' 15 | 16 | server = timbits.serve( {port: port }) 17 | 18 | validateRequest = (method, vpath, expect = 'html') -> 19 | describe vpath, -> 20 | test_msg = "should respond with #{expect} and status 200" 21 | if typeof expect isnt 'string' 22 | test_msg = "should respond with status #{expect}" 23 | 24 | it test_msg, (done) -> 25 | if method is "GET" 26 | request "http://localhost:#{port}#{vpath}", (err, res) -> 27 | should.not.exist err 28 | if typeof expect is 'string' 29 | res.should.have.status 200 30 | if expect is 'json' 31 | res.should.be.json 32 | else 33 | res.should.be.html 34 | else 35 | res.should.have.status expect 36 | done() 37 | else if method is "POST" 38 | request.post "http://localhost:#{port}#{vpath}", (err, res) -> 39 | should.not.exist err 40 | if typeof expect is 'string' 41 | res.should.have.status 200 42 | if expect is 'json' 43 | res.should.be.json 44 | else 45 | res.should.be.html 46 | else 47 | res.should.have.status expect 48 | done() 49 | 50 | 51 | describe 'timbits', -> 52 | 53 | describe 'automated test cases', -> 54 | for name, timbit of timbits.box 55 | if timbit.examples? and timbit.methods["GET"] 56 | describe "specified examples for #{name}", -> 57 | for example in timbit.examples 58 | validateRequest "GET", example.href 59 | 60 | if timbit.examples? and timbit.methods["POST"] 61 | describe "specified examples for #{name}", -> 62 | for example in timbit.examples 63 | validateRequest "POST", example.href 64 | 65 | dynatests = timbit.generateTests(alltests) 66 | if dynatests.length 67 | describe "dynamic tests for #{name}", -> 68 | for href in dynatests 69 | validateRequest "GET", href if timbit.methods["GET"] 70 | validateRequest "POST", href if timbit.methods["POST"] 71 | 72 | -------------------------------------------------------------------------------- /bin/template/test/timbits-test.js: -------------------------------------------------------------------------------- 1 | // reduce logging levels to provide clean test feedback 2 | process.env.TIMBITS_VERBOSITY = 'critical'; 3 | process.env.PANTRY_VERBOSITY = 'critical'; 4 | 5 | // load modules 6 | var timbits = require('../lib/timbits') 7 | , should = require('should') 8 | , path = require('path') 9 | , request = require('request') 10 | 11 | // load hhtp extensions for should 12 | require('should-http'); 13 | 14 | // set testing environment 15 | var port = 8785 16 | , alltests = process.env.TIMBITS_TEST_WHICH === 'all'; 17 | 18 | // create test server 19 | var server = timbits.serve({ 20 | home: process.cwd(), 21 | port: port, 22 | verbosity: 'critical' 23 | }); 24 | 25 | 26 | // helper function for testing requests 27 | function validateRequest(method, vpath, expect) { 28 | expect = expect || 'html'; 29 | 30 | describe(vpath, function() { 31 | var test_msg = "should respond with " + expect + " and status 200"; 32 | if (typeof expect !== 'string') 33 | test_msg = "should respond with status " + expect; 34 | 35 | it(test_msg, function(done) { 36 | if (method=="GET") { 37 | request("http://localhost:" + port + vpath, function(err, res) { 38 | should.not.exist(err); 39 | if (typeof expect === 'string') { 40 | res.should.have.status(200); 41 | if (expect === 'json') 42 | res.should.be.json; 43 | else 44 | res.should.be.html; 45 | } else { 46 | res.should.have.status(expect); 47 | } 48 | done(); 49 | }); 50 | } 51 | else if (method=="POST") { 52 | request.post("http://localhost:" + port + vpath, function(err, res) { 53 | should.not.exist(err); 54 | if (typeof expect === 'string') { 55 | res.should.have.status(200); 56 | if (expect === 'json') 57 | res.should.be.json; 58 | else 59 | res.should.be.html; 60 | } else { 61 | res.should.have.status(expect); 62 | } 63 | done(); 64 | }); 65 | } 66 | }); 67 | }); 68 | }; 69 | 70 | 71 | describe('timbits', function() { 72 | describe('automated test cases', function() { 73 | for (name in timbits.box) { 74 | 75 | timbit = timbits.box[name]; 76 | 77 | // GET requests 78 | if (timbit.examples != null && timbit.methods['GET']) { 79 | describe('specified examples for ' + name, function() { 80 | timbit.examples.forEach(function(example) { 81 | validateRequest('GET', example.href); 82 | }); 83 | }); 84 | } 85 | // Post Requests 86 | if (timbit.examples != null && timbit.methods['POST']) { 87 | describe('specified examples for ' + name, function() { 88 | timbit.examples.forEach(function(example) { 89 | validateRequest('POST', example.href); 90 | }); 91 | }); 92 | } 93 | 94 | var dynatests = timbit.generateTests(alltests); 95 | if (dynatests.length) { 96 | describe("dynamic tests for " + name, function() { 97 | dynatests.forEach(function(href) { 98 | if (timbit.methods['GET']) 99 | validateRequest('GET', href); 100 | if (timbit.methods['POST']) 101 | validateRequest('POST', href); 102 | }); 103 | }); 104 | } 105 | } 106 | }); 107 | }); -------------------------------------------------------------------------------- /bin/template/timbit.coffee: -------------------------------------------------------------------------------- 1 | # Timbit 2 | 3 | # load the timbits module 4 | timbits = require 'timbits' 5 | 6 | # create and export the timbit 7 | timbit = module.exports = new timbits.Timbit() 8 | 9 | # additional timbit implementation code follows... 10 | 11 | #timbit.about = 'a description about this timbit' 12 | 13 | #timbit.examples = [ 14 | # {href: '/timbit/?q=winning', caption: 'Default View'} 15 | # {href: '/timbit/alternate?q=winning', caption: 'Alternate View'} 16 | #] 17 | 18 | #timbit.params = { 19 | # q: {description: 'Keyword to search for',required: true, strict: false, values: ['Coffee', 'Timbits']} 20 | #} 21 | 22 | timbit.eat = (req, res, context) -> 23 | 24 | src = { 25 | uri: "http://jsonip.com/?q=#{context.q}" 26 | } 27 | 28 | # use the helper method to @fetch the data 29 | # @fetch will call @render once we have the data 30 | @fetch req, res, context, src -------------------------------------------------------------------------------- /bin/template/timbit.js: -------------------------------------------------------------------------------- 1 | // Timbit 2 | 3 | // load the timbits module 4 | var timbits = require('timbits'); 5 | 6 | //create and export the timbit 7 | var timbit = module.exports = new timbits.Timbit(); 8 | 9 | // additional timbit implementation code follows... 10 | 11 | /* 12 | timbit.about = 'a description about this timbit'; 13 | 14 | timbit.examples = [ 15 | { 16 | href: '/timbit/?q=winning', 17 | caption: 'Default View' 18 | }, { 19 | href: '/timbit/alternate?q=winning', 20 | caption: 'Alternate View' 21 | } 22 | ]; 23 | 24 | timbit.params = { 25 | q: { 26 | description: 'Keyword to search for', 27 | required: true, 28 | strict: false, 29 | values: ['Coffee', 'Timbits'] 30 | } 31 | }; 32 | */ 33 | 34 | timbit.eat = function(req, res, context) { 35 | var src = { 36 | uri: 'http://jsonip.com/?q=' + context.q 37 | }; 38 | timbit.fetch(req, res, context, src); 39 | }; -------------------------------------------------------------------------------- /bin/timbits: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // load modules 4 | var fs = require('fs') 5 | , util = require('util') 6 | , path = require('path') 7 | , spawn = require("child_process").spawn 8 | , which = require(path.join(__dirname, '..', 'node_modules', 'which')).sync; 9 | 10 | // configure optimist 11 | var optimist = require(path.join(__dirname, '..', 'node_modules', 'optimist')) 12 | .alias('coffee', 'c') 13 | .describe('c', 'language: Use coffee-script files') 14 | .alias('all', 'a') 15 | .describe('a', 'test: run all dynamic tests') 16 | .alias('R', 'reporter').string('R') 17 | .describe('R', 'test: specify reporter to use') 18 | .alias('g', 'grep').string('g') 19 | .describe('g', 'test: only run tests matching ') 20 | .alias('i', 'invert') 21 | .describe('i', 'test: inverts --grep matches') 22 | .alias('t', 'timeout').default('t', 2000) 23 | .describe('t', 'test: set test-case timeout in milliseconds') 24 | .alias('s', 'slow').default('s', 75) 25 | .describe('s', 'test: "slow" test threshold in milliseconds') 26 | .alias('w', 'watch') 27 | .describe('w', 'test: watch for changes and retest') 28 | .alias('d', 'debug') 29 | .describe('d', 'test: enable node debugger') 30 | .alias('b', 'bail') 31 | .describe('b', 'test: bail after first test failure') 32 | .usage( 33 | "Timbits Command Help\n\n \ 34 | timbits n[ew] [project] [-c|--coffee]\n\ \ 35 | Creates new project template and subfolders\n\n \ 36 | timbits g[enerate] [timbit] [-c|--coffee]\n \ 37 | Scaffolds a new timbit and default view\n\n \ 38 | timbits s[erver] [filename]\n \ 39 | Launches runjs with [server.js]\n\n \ 40 | timbits t[est] [options]\n \ 41 | Run mocha test cases\n\n \ 42 | timbits [-]v[ersion]\n \ 43 | Displays installed timbits library version \ 44 | "); 45 | 46 | // determine path to our project template files 47 | var template = path.join(__dirname, 'template'); 48 | 49 | // determine path to a command in local node modules 50 | function localCmd(name) { 51 | var cmd = path.join(__dirname, '..', 'node_modules', '.bin', name); 52 | if(process.platform === 'win32') { 53 | cmd += '.cmd'; 54 | } 55 | return cmd; 56 | } 57 | 58 | function cp(source, destination) { 59 | var data = fs.readFileSync(source); 60 | fs.writeFileSync(destination, data); 61 | } 62 | 63 | function loadEnv() { 64 | if (fs.existsSync('.env')) { 65 | var env_lines = fs.readFileSync('.env').toString().split('\n'); 66 | for (var i = 0; i < env_lines.length; i++) { 67 | var key_value = env_lines[i].trim().split('='); 68 | if (key_value.length === 2) { 69 | console.log('Setting env var: ' + env_lines[i]); 70 | process.env[key_value[0]] = key_value[1]; 71 | } 72 | } 73 | } 74 | } 75 | 76 | function newProject(name, ext) { 77 | console.log("Creating project '" + name + "'"); 78 | 79 | var directories = ["timbits", "views", "test"]; 80 | var files = ["History.md", "README.md", "LICENSE"]; 81 | 82 | var project_home = path.join(process.cwd(), name); 83 | 84 | if(fs.existsSync(project_home)) { 85 | console.error('\033[31mERROR! ' + project_home + ' already exists!\033[0m'); 86 | return; 87 | } 88 | 89 | // create parent directory 90 | console.log('Creating project directory: ' + project_home); 91 | fs.mkdirSync(project_home); 92 | 93 | // create child directories 94 | for (var i = 0; i < directories.length; i++) { 95 | console.log('Creating child directory: ' + directories[i]); 96 | fs.mkdirSync(path.join(project_home, directories[i])); 97 | } 98 | 99 | // copy template files 100 | for (var i = 0; i < files.length; i++) { 101 | console.log('Copying template file: ' + files[i]); 102 | cp( 103 | path.join(template, files[i]), 104 | path.join(project_home, files[i]) 105 | ); 106 | } 107 | 108 | // copy server.js file 109 | if (ext == 'js') { 110 | console.log('Copying template file: server.js'); 111 | cp( 112 | path.join(template, 'server.js'), 113 | path.join(project_home, 'server.js') 114 | ); 115 | } else { 116 | console.log('Copying template file: server_coffee.js -> server.js'); 117 | cp( 118 | path.join(template, 'server_coffee.js'), 119 | path.join(project_home, 'server.js') 120 | ); 121 | } 122 | 123 | // copy test template 124 | console.log('Generating test template: ' + name + '-test.' + ext ); 125 | cp( 126 | path.join(template, 'test', 'timbits-test.' + ext), 127 | path.join(project_home, 'test', name + '-test.' + ext) 128 | ); 129 | 130 | // create package file 131 | console.log('Generating package.json: package.json'); 132 | var pkg; 133 | if ( ext == 'js' ) { 134 | pkg = { 135 | name: name, 136 | description: 'Widgets built using Timbits', 137 | version: '0.0.1', 138 | dependencies: { 139 | timbits: getPackage().version 140 | } 141 | }; 142 | } else { 143 | pkg = { 144 | name: name, 145 | description: 'Widgets built using Timbits', 146 | version: '0.0.1', 147 | dependencies: { 148 | timbits: getPackage().version, 149 | 'coffee-script': '~1.3.3' 150 | } 151 | }; 152 | } 153 | fs.writeFileSync( 154 | path.join(project_home, 'package.json'), 155 | JSON.stringify(pkg) 156 | ); 157 | 158 | // prompt user to configure npm package 159 | var npm_init = spawn( 160 | which('npm'), 161 | ['init'], 162 | { 163 | cwd: project_home, 164 | env: process.env, 165 | stdio: 'inherit' 166 | } 167 | ); 168 | 169 | npm_init.on('exit', function(code) { 170 | if(code === 0) { 171 | // install dependencies 172 | spawn( 173 | which('npm'), 174 | ['install', '-d'], 175 | { 176 | cwd: project_home, 177 | env: process.env, 178 | stdio: 'inherit' 179 | } 180 | ); 181 | } 182 | 183 | console.log('Finished!'); 184 | }); 185 | 186 | } 187 | 188 | function generateTimbit(name, ext) { 189 | console.log("Generating timbit '" + name + "'"); 190 | if( 191 | fs.existsSync(path.join('timbits', name + '.' + ext)) || 192 | fs.existsSync(path.join('views', name)) 193 | ) { 194 | console.error('\033[31mERROR! The timbit ' + name + ' may already exist!\033[0m'); 195 | return; 196 | } 197 | 198 | if ( 199 | ! fs.existsSync(path.join('timbits')) || 200 | ! fs.existsSync(path.join('views')) 201 | ) { 202 | console.error('\033[31mERROR! Current folder does not appear to be timbits project!\033[0m'); 203 | return; 204 | } 205 | 206 | // create timbit 207 | console.log('Creating timbits/' + name + '.' + ext); 208 | cp( 209 | path.join(template, 'timbit.' + ext), 210 | path.join('timbits', name + '.' + ext) 211 | ); 212 | 213 | // create default view 214 | console.log('Creating views/' + name + '/default.hjs'); 215 | fs.mkdirSync( 216 | path.join('views', name) 217 | ); 218 | 219 | fs.writeFileSync( 220 | path.join('views', name, 'default.hjs'), 221 | "

Default {{name}} Timbit View

" 222 | ); 223 | 224 | } 225 | 226 | 227 | 228 | function startServer(filename) { 229 | console.log("Starting server (" + filename + ")"); 230 | loadEnv(); 231 | 232 | spawn(localCmd('runjs'), [filename], { 233 | stdio: [null, process.stdout, process.stderr], 234 | env: process.env 235 | }); 236 | } 237 | 238 | function runTests(alltests, options) { 239 | if (alltests) { 240 | process.env.TIMBITS_TEST_WHICH = 'all'; 241 | } 242 | loadEnv(); 243 | 244 | var mocha = spawn(localCmd('mocha'), options, { 245 | stdio: [null, process.stdout, process.stderr], 246 | env: process.env 247 | }); 248 | } 249 | 250 | function getPackage() { 251 | var data = fs.readFileSync(path.join(__dirname, '..', 'package.json')); 252 | return JSON.parse(data); 253 | } 254 | 255 | function showVersion() { 256 | var pkg = getPackage(); 257 | console.log(pkg.version); 258 | } 259 | 260 | function showHelp() { 261 | console.log(optimist.help()); 262 | } 263 | 264 | var argv = optimist.argv; 265 | var ext; 266 | switch (argv._[0]) { 267 | case 'n': 268 | case 'new': 269 | if (argv._.length < 2) { 270 | console.error('\033[31mnew project requires a name\033[0m'); 271 | showHelp(); 272 | } else { 273 | if ( argv['c'] || argv['coffee'] ) { 274 | ext = 'coffee'; 275 | } else { 276 | ext = 'js'; 277 | } 278 | newProject(argv._[1], ext); 279 | } 280 | break; 281 | 282 | case 'g': 283 | case 'generate': 284 | if (argv._.length < 2) { 285 | console.error('\033[31mnew timbit requires a name\033[0m'); 286 | showHelp(); 287 | } else { 288 | if ( argv['c'] || argv['coffee'] ) { 289 | ext = 'coffee'; 290 | } else { 291 | ext = 'js'; 292 | } 293 | generateTimbit(argv._[1], ext); 294 | } 295 | break; 296 | 297 | case 's': 298 | case 'server': 299 | filename = argv._.length > 1 ? optimist.argv._[1] : 'server.js'; 300 | startServer(filename); 301 | break; 302 | 303 | case 't': 304 | case 'test': 305 | options = ['--growl', '--colors']; 306 | for (key in argv) { 307 | switch(key){ 308 | case 'reporter': 309 | case 'grep': 310 | case 'timeout': 311 | case 'slow': 312 | options.push('--' + key, argv[key]); 313 | break; 314 | case 'invert': 315 | case 'watch': 316 | case 'debug': 317 | case 'bail': 318 | options.push('--' + key) 319 | break; 320 | case 'coffee': 321 | options.push( '--compilers', 'coffee:coffee-script' ); 322 | break; 323 | } 324 | } 325 | runTests(argv.a, options); 326 | break; 327 | 328 | case 'v': 329 | case 'version': 330 | showVersion(); 331 | break; 332 | 333 | default: 334 | if (argv.v != null) { 335 | showVersion(); 336 | } else { 337 | showHelp(); 338 | } 339 | } -------------------------------------------------------------------------------- /examples/helpers/hello.js: -------------------------------------------------------------------------------- 1 | module.exports = function hello(name) { 2 | return 'Hello there ' + name; 3 | }; -------------------------------------------------------------------------------- /examples/server-redis.coffee: -------------------------------------------------------------------------------- 1 | # Module dependencies. 2 | timbits = require '../src/timbits' 3 | 4 | # use redis for caching 5 | RedisStorage = require 'pantry/lib/pantry-redis' 6 | timbits.pantry.storage = new RedisStorage(null, null, null, 'DEBUG') 7 | timbits.pantry.configure {verbosity: 'DEBUG'} 8 | 9 | # start serving timbits 10 | server = timbits.serve {home: __dirname} 11 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | //process.env.TIMBITS_VERBOSITY = 'debug'; 2 | //process.env.PANTRY_VERBOSITY = 'debug'; 3 | 4 | var timbits = require('../lib/timbits'); 5 | var server = timbits.serve({ 6 | home: __dirname 7 | }); -------------------------------------------------------------------------------- /examples/timbits/cherry.js: -------------------------------------------------------------------------------- 1 | // Cherry Timbit 2 | 3 | // load the timbits module 4 | var timbits = require('../../lib/timbits'); 5 | 6 | // create and export the timbit 7 | var timbit = module.exports = new timbits.Timbit(); 8 | 9 | // additional timbit implementation code follows... 10 | timbit.about = '\ 11 | Example of a timbit which actually does something.\ 12 | This timbit will display the current server time.\ 13 | '; 14 | 15 | timbit.examples = [ 16 | { 17 | href: '/cherry/', 18 | caption: 'Current Time' 19 | } 20 | ]; 21 | 22 | timbit.eat = function(req, res, context) { 23 | context.now = new Date(); 24 | return this.render(req, res, context); 25 | }; -------------------------------------------------------------------------------- /examples/timbits/chocolate.js: -------------------------------------------------------------------------------- 1 | // Chocolate Timbit 2 | 3 | // load the timbits module 4 | var timbits = require('../../lib/timbits'); 5 | 6 | // create and export the timbit 7 | var timbit = module.exports = new timbits.Timbit(); 8 | 9 | // additional timbit implementation code follows... 10 | timbit.about = '\ 11 | Example of a timbit which transforms remote data.\ 12 | This timbit will query Wordpress API and display the results\ 13 | There are two views available, the default and the alternate\ 14 | '; 15 | 16 | timbit.examples = [ 17 | { 18 | href: '/chocolate/?site=sports.nationalpost.com', 19 | caption: 'Latest Sports news from The National Post' 20 | }, { 21 | href: '/chocolate/alternate-view?site=sports.nationalpost.com&tag=Hockey&number=5', 22 | caption: 'Latest five posts on Hockey from The National Post' 23 | } 24 | ]; 25 | 26 | timbit.params = { 27 | site: { 28 | description: 'The Wordpress site to query', 29 | required: true, 30 | strict: false, 31 | values: ['sports.nationalpost.com', 'o.canada.com'] 32 | }, 33 | tag: { 34 | description: 'Tag to filter by', 35 | required: false, 36 | strict: false, 37 | values: ['Hockey', 'Detroit'] 38 | }, 39 | "number": { 40 | description: 'The number of posts to display', 41 | alias: 'rpp', 42 | "default": 10, 43 | strict: false, 44 | values: [3, 5, 10] 45 | } 46 | }; 47 | 48 | timbit.eat = function(req, res, context) { 49 | 50 | // specify the data source 51 | var src = { 52 | name: 'wordpress', 53 | uri: "http://public-api.wordpress.com/rest/v1/sites/" + context.site + "/posts?number=" + context.number 54 | }; 55 | 56 | if (context.tag) { 57 | src.uri += "&tag=" + context.tag; 58 | } 59 | 60 | // use the helper method to fetch the data 61 | // timbit.fetch will call timbit.render once we have the data 62 | timbit.fetch(req, res, context, src); 63 | }; -------------------------------------------------------------------------------- /examples/timbits/dutchie.js: -------------------------------------------------------------------------------- 1 | // Dutchie Timbit 2 | 3 | // load the timbits module 4 | var timbits = require('../../lib/timbits'); 5 | 6 | // create and export the timbit 7 | var timbit = module.exports = new timbits.Timbit(); 8 | 9 | // additional timbit implementation code follows... 10 | timbit.about = '\ 11 | Example of a timbit which re-uses another timbits views\ 12 | This timbit will query Wordpress API and display the results\ 13 | This timbit re-uses the views from the chocolate timbit\ 14 | '; 15 | 16 | timbit.examples = [ 17 | { 18 | href: '/dutchie/?site=news.nationalpost.com', 19 | caption: 'Latest news from The National Post' 20 | }, { 21 | href: '/dutchie/alternate-view?site=news.nationalpost.com&tag=Apple&number=5', 22 | caption: 'Latest five news posts on Apple from The National Post' 23 | } 24 | ]; 25 | 26 | timbit.params = { 27 | site: { 28 | description: 'The Wordpress site to query', 29 | required: true, 30 | strict: false, 31 | values: ['news.nationalpost.com', 'o.canada.com'] 32 | }, 33 | tag: { 34 | description: 'Tag to filter by', 35 | required: false, 36 | strict: false, 37 | values: ['Apple', 'Canada'] 38 | }, 39 | "number": { 40 | description: 'The number of posts to display', 41 | alias: 'rpp', 42 | "default": 10, 43 | strict: false, 44 | values: [3, 5, 10] 45 | } 46 | }; 47 | 48 | // let's just re-use the chocolate timbit views and use additional cache time 49 | timbit.viewBase = 'chocolate'; 50 | timbit.maxAge = 300; 51 | 52 | timbit.eat = function(req, res, context) { 53 | 54 | // specify the data source 55 | var src = { 56 | uri: "http://public-api.wordpress.com/rest/v1/sites/" + context.site + "/posts?number=" + context.number 57 | }; 58 | 59 | if (context.tag) { 60 | src.uri += "&tag=" + context.tag; 61 | } 62 | 63 | // instead of using the fetch helper method, let's show how to use pantry directly 64 | timbits.pantry.fetch(src, function(error, results) { 65 | context.wordpress = results; 66 | timbit.render(req, res, context); 67 | }); 68 | }; -------------------------------------------------------------------------------- /examples/timbits/plain.js: -------------------------------------------------------------------------------- 1 | // Plain Timbit 2 | 3 | // Load the timbits module 4 | var timbits = require('../../lib/timbits'); 5 | 6 | // create and export the timbit 7 | var timbit = module.exports = new timbits.Timbit(); 8 | 9 | 10 | // additional timbit implementation code follows... 11 | timbit.about = '\ 12 | Example of the simplest timbit that could possibly be created.\ 13 | This timbit will simply render a view using data from the query string.\ 14 | '; 15 | 16 | timbit.examples = [ 17 | { 18 | href: '/plain/', 19 | caption: 'Anonymous' 20 | }, { 21 | href: '/plain/?who=world', 22 | caption: 'Hello World' 23 | }, { 24 | href: '/plain/?who=Kevin&retro=true', 25 | caption: 'Flashback' 26 | } 27 | ]; 28 | 29 | timbit.params = { 30 | who: { 31 | description: 'Name of person to greet', 32 | "default": 'Anonymous', 33 | multiple: false, 34 | required: false, 35 | strict: false, 36 | values: ['Ed', 'World'] 37 | }, 38 | retro: { 39 | description: 'To test multi parameters and drive Kevin crazy', 40 | type: 'Boolean' 41 | } 42 | }; -------------------------------------------------------------------------------- /examples/timbits/post.js: -------------------------------------------------------------------------------- 1 | // Timbit 2 | 3 | // load the timbits module 4 | var timbits = require('../../lib/timbits'); 5 | 6 | //create and export the timbit 7 | var timbit = module.exports = new timbits.Timbit(); 8 | 9 | // additional timbit implementation code follows... 10 | 11 | timbit.about = 'An example timbit that handles just the post event'; 12 | 13 | // Set allowed methods, by default only GET is ever allowed 14 | timbit.methods = { 'GET': false, 'POST': true }; 15 | 16 | timbit.examples = [ 17 | { 18 | href: '/post', 19 | caption: 'Default View' 20 | } 21 | ]; 22 | 23 | 24 | timbit.eat = function(req, res, context) { 25 | var body = ''; 26 | req.on( 'data', function( data ) { 27 | body += data; 28 | } ); 29 | 30 | req.on( 'end', function () { 31 | context.body = body; 32 | timbit.render( req, res, context ) 33 | }); 34 | }; -------------------------------------------------------------------------------- /examples/views/cherry/default.hjs: -------------------------------------------------------------------------------- 1 |

The current server date/time is {{now}}

-------------------------------------------------------------------------------- /examples/views/chocolate/alternate-view.hjs: -------------------------------------------------------------------------------- 1 |

Alternate {{name}} Timbit View

2 |

Latest posts from '{{site}}'

3 | 4 | -------------------------------------------------------------------------------- /examples/views/chocolate/default.hjs: -------------------------------------------------------------------------------- 1 |

Default {{name}} Timbit View

2 |

Latest posts from '{{site}}'

3 | 4 | -------------------------------------------------------------------------------- /examples/views/plain/default.hjs: -------------------------------------------------------------------------------- 1 |

2 | {{#retro}}Wazzzzzzup{{/retro}} 3 | {{^retro}}Hello{{/retro}} 4 | {{who}} 5 |

6 | {{#retro}} 7 | 15 | {{/retro}} -------------------------------------------------------------------------------- /examples/views/post/default.hjs: -------------------------------------------------------------------------------- 1 |

HTTP Post Timbit Example

2 |
Send a post request with content in the body, the content is shown below the line 3 |
4 | {{body}} 5 |
-------------------------------------------------------------------------------- /lib/templates/help.hjs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 46 | 47 | 48 |
49 |
50 |

{{title}}

51 |
    52 | {{#timbits}} 53 |
  • {{.}} »
  • 54 | {{/timbits}} 55 |
56 |
57 |
58 | 59 | -------------------------------------------------------------------------------- /lib/templates/test.hjs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 6 | 33 | 34 | 35 |
36 |

{{title}}

37 | 38 | {{#failed}} 39 |
40 | 41 |

Failed {{failed}} of {{results.length}}

42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {{#results}} 53 | {{#failed}} 54 | 55 | 56 | 57 | 58 | 59 | 60 | {{/failed}} 61 | {{/results}} 62 | 63 |
TimbitURLHTTP StatusError Message
{{timbit}}{{href}}{{status}}{{error}}
64 |
65 | {{/failed}} 66 | 67 | {{#passed}} 68 |
69 | 70 |

Passed {{passed}} of {{results.length}}

71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | {{#results}} 80 | {{#passed}} 81 | 82 | 83 | 84 | 85 | {{/passed}} 86 | {{/results}} 87 | 88 |
TimbitURL
{{timbit}}{{href}}
89 |
90 | {{/passed}} 91 | 92 |
93 | 94 | -------------------------------------------------------------------------------- /lib/templates/timbit-help.hjs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 52 | 53 | 54 |
55 |
56 | 57 |

{{name}}

58 |

{{about}}

59 | 60 |

Examples:

61 | {{^examples}} 62 |

Developer was too lazy to define any examples.

63 | {{/examples}} 64 |
    65 | {{#examples}} 66 |
  • {{caption}}
  • 67 | {{/examples}} 68 |
69 | 70 |

Views

71 |
    72 | {{#views}} 73 |
  • {{.}}
  • 74 | {{/views}} 75 |
76 | 77 |

Parameters

78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | {{#paramsAsArray}} 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 108 | 109 | {{/paramsAsArray}} 110 | 111 |
NameAliasDescriptionTypeRequiredMultipleDefaultValues
{{key}}{{value.alias}}{{value.description}}{{value.type}}{{^value.type}}String{{/value.type}}{{value.required}}{{value.multiple}}{{value.default}} 100 | {{#value.strict}}One of:{{/value.strict}} 101 | {{^value.strict}}Examples:{{/value.strict}} 102 |
    103 | {{#value.values}} 104 |
  • {{.}}
  • 105 | {{/value.values}} 106 |
107 |
112 | 113 | 114 | 115 |
116 |
117 | 118 | -------------------------------------------------------------------------------- /lib/timbits.js: -------------------------------------------------------------------------------- 1 | 2 | // load required modules 3 | var fs = require('fs') 4 | , path = require('path') 5 | , querystring = require('querystring') 6 | , url = require('url') 7 | , winston = require('winston') 8 | , express = require('express') 9 | , hogan = require('hogan.js') 10 | , request = require('request'); 11 | 12 | // default configuration 13 | var config = { 14 | appName: 'Timbits', 15 | base: '', 16 | port: 5678, 17 | home: process.cwd(), 18 | maxAge: 60, // default widget output cache time 19 | engine: 'hjs', // default view engine 20 | discovery: true, // support automatic discovery via /timbits/json 21 | help: true, // allow automatic help pages at /timbits/help and /[name]/help 22 | test: true, // allow automatic test pages at /timbits/test and /[name]/test 23 | json: true, // allow built in json view at /[name]/json 24 | jsonp: true // allow jsonp calls via /[name]/json?callback= 25 | }; 26 | 27 | // retrieve list of matching files in a folder 28 | function filteredFiles(folder, pattern) { 29 | var files = []; 30 | 31 | if (fs.existsSync(folder)){ 32 | fs.readdirSync(folder).forEach(function(file) { 33 | if (file.match(pattern) != null) 34 | files.push(file); 35 | }); 36 | } 37 | return files; 38 | } 39 | 40 | // automagically load timbits found in the ./timbits folder 41 | function loadTimbits(callback) { 42 | 43 | var folder = path.join(config.home, "/timbits"); 44 | var files = filteredFiles(folder, /\.(coffee|js)$/); 45 | var pending = files.length; 46 | 47 | files.forEach(function(file) { 48 | var name = file.substring(0, file.lastIndexOf(".")); 49 | timbits.add(name, require(path.join(folder, file)), function() { 50 | pending--; 51 | if (pending === 0) callback(); 52 | }); 53 | }); 54 | } 55 | 56 | // automagically load views for a given timbit 57 | function loadViews(timbit) { 58 | timbit.views = []; 59 | 60 | var pattern = new RegExp('\.' + timbits.app.settings['view engine'] + '$') 61 | , folder = path.join(config.home, 'views', timbit.viewBase); 62 | 63 | if (fs.existsSync(folder)) { 64 | var files = fs.readdirSync(folder); 65 | files.forEach(function(file) { 66 | timbit.views.push(file.replace(pattern, '')); 67 | }); 68 | } 69 | 70 | // We will attempt the default view anyway and hope the timbit knows what it is doing. 71 | if (timbit.views.length === 0) timbit.views.push(timbit.defaultView); 72 | } 73 | 74 | // return a list of possible test values 75 | function getTestValues(values, alltests) { 76 | if (values != null && values.length != null && values.length !== 0) { 77 | if (alltests) 78 | return values; 79 | else 80 | return values.slice(0, 1); 81 | } else { 82 | return []; 83 | } 84 | } 85 | 86 | // compile built in templates 87 | function compileTemplate(name) { 88 | var filename = path.join(__dirname, "templates", name + '.hjs'); 89 | var contents = fs.readFileSync(filename); 90 | return hogan.compile(contents.toString()); 91 | } 92 | 93 | // generates the allowed methods 94 | function allowedMethods(methods) { 95 | // default values 96 | var methodsAllowed = { 'GET': true, 'POST': false, 'PUT': false, 'HEAD': false, 'DELETE': false }; 97 | // check and override if one of the default methods 98 | for (var key in methods) { 99 | var newKey = key.toUpperCase(); 100 | if ( methodsAllowed[newKey]!=undefined) { 101 | methodsAllowed[newKey] = Boolean(methods[key]); 102 | } 103 | } 104 | return methodsAllowed; 105 | } 106 | 107 | var timbits = this; 108 | this.box = {}; 109 | this.pantry = require('pantry'); 110 | this.templates = { 111 | help: compileTemplate('help'), 112 | timbitHelp: compileTemplate('timbit-help'), 113 | test: compileTemplate('test'), 114 | }; 115 | 116 | this.getLogLevel = function() { 117 | switch (process.env.NODE_ENV) { 118 | case 'production': 119 | return 'info'; 120 | case 'test': 121 | return 'error'; 122 | default: 123 | return 'silly'; 124 | } 125 | }; 126 | 127 | //added winston object for logging. 128 | this.log = new (winston.Logger)({ 129 | transports: [ 130 | new (winston.transports.Console)({ 131 | colorize: true, 132 | timestamp: true, 133 | level: this.getLogLevel() 134 | }) 135 | ] 136 | }); 137 | 138 | // creates, configures, and returns a standard express app 139 | this.serve = function(options) { 140 | 141 | /* configure options */ 142 | for (var key in options) { 143 | value = options[key]; 144 | config[key] = value; 145 | } 146 | 147 | /* configure express app */ 148 | var app = timbits.app = express(); 149 | 150 | app.set('views', "" + config.home + "/views"); 151 | app.set('view engine', config.engine); 152 | app.set('jsonp callback', config.jsonp); 153 | 154 | app.use(express.favicon()); 155 | 156 | // disable request logging for tests (avoid console clutter) 157 | if (app.get('env') === 'development') 158 | app.use(express.logger('dev')); 159 | 160 | app.use(express.compress()); 161 | app.use(express.bodyParser()); 162 | app.use(express.cookieParser()); 163 | app.use(express.static(path.join(config.home, "public"))); 164 | app.use(express.static(path.join(__dirname, "../resources"))); 165 | app.use(express.errorHandler()); 166 | 167 | // redirect root to help page 168 | if (config.help) { 169 | app.all(config.base + "/", function(req, res) { 170 | res.redirect(config.base + "/timbits/help"); 171 | }); 172 | } 173 | 174 | // route json discovery 175 | app.get(config.base + "/timbits/json", function(req, res) { 176 | if (config.discovery) { 177 | res.json(timbits.box); 178 | } else { 179 | res.send(404, "Automatic Discovery has been disabled"); 180 | } 181 | }); 182 | 183 | // route help page 184 | app.get(config.base + "/timbits/help", function(req, res) { 185 | if (config.help) { 186 | var context = {title: 'Timbits Help', timbits: []}; 187 | for(var key in timbits.box) { 188 | context.timbits.push(key); 189 | } 190 | res.send(timbits.templates.help.render(context)); 191 | } else { 192 | res.send(404, "Automatic Help has been disabled"); 193 | } 194 | }); 195 | 196 | // route master test page 197 | app.get(config.base + '/timbits/test/:which?', function(req, res) { 198 | if (config.test) { 199 | var alltests = (req.params.which === 'all') 200 | , all_results = [] 201 | , pending = Object.keys(timbits.box).length; 202 | 203 | if (pending) { 204 | for (name in timbits.box) { 205 | var timbit = timbits.box[name]; 206 | timbit.test('http://' + req.headers.host, alltests, function(results) { 207 | results.forEach(function(result) { 208 | all_results.push(result); 209 | }); 210 | if (--pending === 0) { 211 | var passed = 0, failed = 0; 212 | all_results.forEach(function(result) { 213 | if (result.passed) passed++; else failed++; 214 | }); 215 | res.send(timbits.templates.test.render({ 216 | title: 'Testing Summary: all timbits', 217 | passed: passed, 218 | failed: failed, 219 | results: all_results 220 | })); 221 | } 222 | }); 223 | } 224 | } else { 225 | res.send(ck.render(views.test, {})); 226 | } 227 | } else { 228 | res.send(404, "Automatic Test has been disabled"); 229 | } 230 | }); 231 | 232 | // automagically load timbits 233 | loadTimbits(function() { 234 | try { 235 | timbits.server = app.listen(process.env.PORT || process.env.C9_PORT || config.port); 236 | timbits.log.info("Timbits server listening on port " + timbits.server.address().port + " in " + app.settings.env + " mode"); 237 | } catch (err) { 238 | timbits.log.error("Server could not start on port " + (process.env.PORT || process.env.C9_PORT || config.port) + ". (" + err + ")"); 239 | console.log("\nPress Ctrl+C to Exit"); 240 | process.exit(1); 241 | } 242 | }); 243 | 244 | return app; 245 | }; 246 | 247 | // use the 'add' method to place a timbit in the box 248 | this.add = function(name, timbit, callback) { 249 | timbits.log.info("Placing " + name + " in the box"); 250 | timbits.box[name] = timbit; 251 | 252 | timbit.name = name; 253 | if (timbit.viewBase == null) timbit.viewBase = name; 254 | if (timbit.defaultView == null) timbit.defaultView = 'default'; 255 | if (timbit.maxAge == null) timbit.maxAge = config.maxAge; 256 | timbit.methods = allowedMethods( (timbit.methods == null) ? {} : timbit.methods ); 257 | 258 | loadViews(timbit); 259 | 260 | // route timbit help 261 | timbits.app.get(config.base + "/" + name + "/help", function(req, res) { 262 | if (config.help) 263 | res.send(timbits.templates.timbitHelp.render(timbit)); 264 | else 265 | res.send(404, "Automatic Help has been disabled"); 266 | }); 267 | 268 | // route timbit testing 269 | timbits.app.get(config.base + "/" + name + "/test/:which?", function(req, res) { 270 | var alltests; 271 | if (config.test) { 272 | alltests = req.params.which === 'all'; 273 | timbit.test("http://" + req.headers.host, alltests, function(results) { 274 | var passed = 0, failed = 0; 275 | results.forEach(function(result) { 276 | if (result.passed) passed++; else failed++; 277 | }); 278 | res.send(timbits.templates.test.render({ 279 | title: 'Testing Summary: ' + timbit.name, 280 | passed: passed, 281 | failed: failed, 282 | results: results 283 | })); 284 | }); 285 | } else { 286 | res.send(404, "Automatic Test has been disabled"); 287 | } 288 | }); 289 | 290 | // main timbit route 291 | timbits.app.all(config.base + '/' + name + '/:view?', function(req, res) { 292 | 293 | // test if the method used is in the allowed methods, 294 | if ( timbit.methods[req.method] == undefined || timbit.methods[req.method] == false ) { 295 | res.send(405, "Method Not Allowed"); 296 | return; 297 | } 298 | 299 | // set view name to default view if not specified 300 | if (req.params.view == null) req.params.view = timbit.defaultView; 301 | 302 | // initialize current request context 303 | var context = { 304 | name: timbit.name, 305 | view: timbit.viewBase + '/' + req.params.view, 306 | maxAge: timbit.maxAge 307 | }; 308 | 309 | // add query string parameters to context 310 | for (var key in req.query) { 311 | var has_alias = false; 312 | 313 | if (context[key] == null && req.query[key] != null && req.query[key] !== '') { 314 | 315 | // handle aliased parameters 316 | for (var p in timbit.params) { 317 | if (timbit.params[p] === key) { 318 | has_alias = true; 319 | context[p] = req.query[key]; 320 | } 321 | } 322 | 323 | // no alias found 324 | if (!has_alias) 325 | context[key] = req.query[key]; 326 | } 327 | } 328 | 329 | // validate request 330 | for (var key in timbit.params) { 331 | var param = timbit.params[key]; 332 | 333 | // if parameter isn't specified, use default value 334 | if (context[key] == null) context[key] = param.default; 335 | 336 | // test provided value based on type 337 | var value = context[key]; 338 | if (value != null) { 339 | 340 | // default parameter type is String 341 | if (param.type == null) param.type = 'String'; 342 | 343 | switch (param.type.toLowerCase()) { 344 | 345 | case 'number': 346 | context[key] = Number(value); 347 | if (isNaN(context[key])) 348 | throw value + ' is not a valid Number for ' + key; 349 | break; 350 | 351 | case 'boolean': 352 | switch (value.toLowerCase()) { 353 | case 'true': 354 | context[key] = true; 355 | break; 356 | case 'false': 357 | context[key]= false; 358 | break; 359 | default: 360 | throw value + ' is not a valid value for ' + key + '. Must be true of false'; 361 | } 362 | break; 363 | 364 | case 'date': 365 | context[key] = Date.parse(value); 366 | if (isNaN(context[key])) 367 | throw value + ' is not a valid date for ' + key; 368 | break; 369 | 370 | } 371 | } 372 | 373 | if (param.required && value == null) { 374 | throw key + " is a required parameter"; 375 | } 376 | if (value != null && param.strict && param.values.indexOf(value) === -1) { 377 | throw value + " is not a valid value for " + key + ". Must be one of [" + (param.values.join()) + "]"; 378 | } 379 | if (value instanceof Array && !param.multiple) { 380 | throw key + " must be a single value"; 381 | } 382 | 383 | } 384 | 385 | // with context created, it's time to consume this timbit 386 | timbit.eat(req, res, context); 387 | }); 388 | 389 | // update example urls if base vpath specified 390 | if (config.base != null && timbit.examples != null) { 391 | timbit.examples.forEach(function(example) { 392 | example.href = config.base + example.href; 393 | }); 394 | } 395 | 396 | // callback after timbit has been loaded 397 | callback(); 398 | 399 | }; 400 | 401 | // prototype for Timbit 402 | var Timbit = this.Timbit = function() {}; 403 | 404 | Timbit.prototype.render = function(req, res, context) { 405 | 406 | // add caching headers 407 | res.setHeader("Cache-Control", "max-age=" + context.maxAge); 408 | res.setHeader("Edge-Control", "!no-store, max-age=" + context.maxAge); 409 | 410 | if (/^(\w+|(\w+-)+\w+)\/json$/.test(context.view)) { 411 | if (config.json) 412 | res.json(context); 413 | else 414 | res.send(404, "JSON view has been disabled"); 415 | } else { 416 | res.render(context.view, context, function(err, str) { 417 | if (err) { 418 | timbits.log.error("Error rendering view " + context.view); 419 | req.next(err); 420 | } else { 421 | if (context.callback != null) 422 | res.json(str); 423 | else 424 | res.send(str); 425 | } 426 | }); 427 | } 428 | }; 429 | 430 | Timbit.prototype.fetch = function(req, res, context, options, callback) { 431 | 432 | // use built in render method by default 433 | if (callback == null) callback = this.render; 434 | 435 | var name = options.name || 'data'; 436 | timbits.pantry.fetch(options, function(error, results) { 437 | if (error) { 438 | timbits.log.error("Error fetching resource '" + options.uri); 439 | req.next(error); 440 | } else { 441 | if (context[name] != null) { 442 | if (Object.prototype.toString.call(context[name][0]) === "[object Array]") 443 | context[name].push(results); 444 | else 445 | context[name] = [context[name], results]; 446 | } else { 447 | context[name] = results; 448 | } 449 | callback(req, res, context); 450 | } 451 | }); 452 | }; 453 | 454 | Timbit.prototype.eat = function(req, res, context) { 455 | this.render(req, res, context); 456 | }; 457 | 458 | 459 | Timbit.prototype.generateTests = function(alltests) { 460 | 461 | // create combination of required parameters 462 | var required = []; 463 | for (var name in this.params) { 464 | var param = this.params[name]; 465 | if (param.required) { 466 | var temp = []; 467 | getTestValues(param.values, alltests).forEach(function(value) { 468 | if (required.length === 0) 469 | temp.push(name + "=" + value); 470 | else 471 | required.forEach(function(item) { 472 | temp.push(item + "&" + name + "=" + value); 473 | }); 474 | }); 475 | required = temp; 476 | } 477 | } 478 | 479 | // create list of possible queries using required and optional parameters 480 | var queries = []; 481 | required.forEach(function(item) { 482 | queries.push(item) 483 | }); 484 | 485 | // only include optional parameters if all tests are requested 486 | if (alltests) { 487 | for (var name in this.params) { 488 | var param = this.params[name]; 489 | if (!param.required) { 490 | getTestValues(param.values, alltests).forEach(function(value) { 491 | if (required.length === 0) 492 | queries.push(name + "=" + value); 493 | else 494 | required.forEach(function(item) { 495 | queries.push(item + "&" + name + "=" + value); 496 | }); 497 | }); 498 | } 499 | } 500 | } 501 | 502 | //create list of testable paths using available views and quiries 503 | var hrefs = []; 504 | var name = this.name 505 | this.views.forEach(function(view) { 506 | if (queries.length) 507 | queries.forEach(function(query) { 508 | hrefs.push("/" + name + "/" + view + "?" + query); 509 | }); 510 | else 511 | hrefs.push("/" + name + "/" + view); 512 | }); 513 | 514 | return hrefs; 515 | }; 516 | 517 | Timbit.prototype.test = function(host, alltests, callback) { 518 | 519 | // generate dynamic list of test urls 520 | var tests = this.generateTests(alltests); 521 | 522 | // multiplier for tests total based on allowed methods, determine which methods are used 523 | var testsLength = 1; 524 | var getMethod = this.methods["GET"]; 525 | var postMethod = this.methods["POST"]; 526 | if (getMethod && postMethod) 527 | testsLength = 2; 528 | 529 | // add examples to list of tests 530 | if (this.examples) { 531 | this.examples.forEach(function(example) { 532 | tests.push(example.href); 533 | }); 534 | } 535 | 536 | // run each test 537 | var results = []; 538 | var name = this.name; 539 | 540 | tests.forEach(function(href) { 541 | if (getMethod) { 542 | request(host + href, function(error, response, body) { 543 | error = error || (response.statusCode === 200 ? '' : body); 544 | results.push({ 545 | timbit: name, 546 | href: href, 547 | error: error, 548 | status: response.statusCode, 549 | passed: response.statusCode === 200, 550 | failed: response.statusCode !== 200 551 | }); 552 | if (results.length === (tests.length * testsLength)) 553 | return callback(results); 554 | }); 555 | } 556 | if (postMethod) { 557 | request.post(host + href, function(error, response, body) { 558 | error = error || (response.statusCode === 200 ? '' : body); 559 | results.push({ 560 | timbit: name, 561 | href: href, 562 | error: error, 563 | status: response.statusCode, 564 | passed: response.statusCode === 200, 565 | failed: response.statusCode !== 200 566 | }); 567 | if (results.length === (tests.length * testsLength)) 568 | return callback(results); 569 | }); 570 | } 571 | }); 572 | 573 | }; 574 | 575 | Timbit.prototype.paramsAsArray = function() { 576 | var array = []; 577 | for (var key in this.params) { 578 | array.push({key: key, value: this.params[key]}); 579 | } 580 | return array; 581 | } 582 | 583 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timbits", 3 | "description": "Widget framework based on Express", 4 | "version": "0.7.3", 5 | "homepage": "https://github.com/Postmedia/timbits", 6 | "author": "Edward de Groot (http://mred9.com)", 7 | "contributors": [ 8 | { "name": "Keith Benedict", "email": "kbenedict@postmedia.com" }, 9 | { "name": "Stephen Veerman", "email": "sveerman@postmedia.com" }, 10 | { "name": "Donnie Marges", "email": "dmarges@postmedia.com" }, 11 | { "name": "Kevin Gamble", "email": "kgamble@postmedia.com" } 12 | ], 13 | "dependencies": { 14 | "winston": "~0.7.1", 15 | "express": "~3.10.5", 16 | "hjs": "~0.0.6", 17 | "hogan.js": "~3.0.1", 18 | "pantry": "~0.7.0", 19 | "request": "~2.34", 20 | "optimist": "~0.3.4", 21 | "run": "~1.2", 22 | "mocha": "~1.20.1", 23 | "should": "~4.0.4", 24 | "which": "~1.0.5", 25 | "should-http": "~0.0.2" 26 | }, 27 | "bugs": { "url": "https://github.com/Postmedia/timbits/issues"}, 28 | "licenses": [ 29 | { "type": "MIT", "url": "https://github.com/Postmedia/timbits/blob/master/LICENSE"} 30 | ], 31 | "keywords": ["framework", "widgets", "express"], 32 | "repository": "git://github.com/postmedia/timbits", 33 | "main": "./lib/timbits", 34 | "bin": { 35 | "timbits": "./bin/timbits" 36 | }, 37 | "scripts": { 38 | "test": "mocha" 39 | }, 40 | "engines": { "node": ">=0.8.0 <0.11", "npm": "1.x" } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /resources/images/accept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Postmedia-Archive/timbits/0213593db6cf94bb35679a4fad01588b19f7fe62/resources/images/accept.png -------------------------------------------------------------------------------- /resources/images/brick_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Postmedia-Archive/timbits/0213593db6cf94bb35679a4fad01588b19f7fe62/resources/images/brick_add.png -------------------------------------------------------------------------------- /resources/images/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Postmedia-Archive/timbits/0213593db6cf94bb35679a4fad01588b19f7fe62/resources/images/cancel.png -------------------------------------------------------------------------------- /resources/images/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Postmedia-Archive/timbits/0213593db6cf94bb35679a4fad01588b19f7fe62/resources/images/error.png -------------------------------------------------------------------------------- /resources/images/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Postmedia-Archive/timbits/0213593db6cf94bb35679a4fad01588b19f7fe62/resources/images/eye.png -------------------------------------------------------------------------------- /resources/javascript/timbits.csi.js: -------------------------------------------------------------------------------- 1 | /* 2 | Timbits CSI - v0.6.5 3 | Copyright (c) 2012 Postmedia Network Inc 4 | Written by Edward de Groot (http://mred9.com) 5 | Licensed under the MIT license 6 | https://github.com/Postmedia/timbits 7 | */ 8 | 9 | var timbits = timbits || {}; 10 | 11 | timbits.renderCSI = function(include, data) { 12 | switch(include.attr('data-mode')) { 13 | case 'append': 14 | include.append(data); 15 | break; 16 | case 'prepend': 17 | include.prepend(data); 18 | break; 19 | default: 20 | include.html(data); 21 | } 22 | include.trigger('load'); 23 | } 24 | 25 | timbits.processCSI = function(index, element) { 26 | var include = jQuery(element); 27 | var src = include.attr('data-csi'); 28 | var process_this = true; 29 | 30 | // test for optional media query 31 | media = include.attr('data-media'); 32 | if (media && window.matchMedia) { 33 | mql = window.matchMedia(media); 34 | process_this = mql.matches; 35 | if (!mql.matches) 36 | mql.addListener(timbits.discoverCSI); 37 | } 38 | 39 | if (process_this) { 40 | // remove data-csi attribute to prevent processing more than once 41 | include.removeAttr('data-csi'); 42 | 43 | // dynamic QS insertion 44 | var _re = /{#(\w+)}/; 45 | var match; 46 | while ((match = src.match(_re)) != null) { 47 | src = src.replace(match[0], timbits.csi_qs_params[match[1]] ? timbits.csi_qs_params[match[1]] : ''); 48 | } 49 | 50 | console.log('Processing CSI: ' + src); 51 | 52 | if (src.search(/http:|https:/i) === 0) 53 | { 54 | // remote request - add callback parameter 55 | src += (src.indexOf('?') === -1) ? '?' : '&'; 56 | src += 'callback=?'; 57 | 58 | // fetch the remote resource and render 59 | jQuery.getJSON(src, function(data) { 60 | timbits.renderCSI(include, data); 61 | }); 62 | } else { 63 | // fetch the load resource and render 64 | jQuery.get(src, function(data) { 65 | timbits.renderCSI(include, data); 66 | }); 67 | } 68 | } 69 | } 70 | 71 | timbits.discoverCSI = function(mql) { 72 | console.log("Discovering CSI: " + (mql ? mql.media : 'Init')); 73 | jQuery('[data-csi]').each(timbits.processCSI); 74 | } 75 | 76 | jQuery( function() { 77 | // parse QS value for optional use within CSI calls 78 | timbits.csi_qs_params = {}; 79 | 80 | var csi_qs_regex = /[\?\&](\w+)=([^&]*)/g; 81 | var csi_qs_match; 82 | 83 | while ((csi_qs_match = csi_qs_regex.exec(location.search)) != null) { 84 | timbits.csi_qs_params[csi_qs_match[1]] = csi_qs_match[2]; 85 | } 86 | // discover and process CSI calls 87 | timbits.discoverCSI(); 88 | }); -------------------------------------------------------------------------------- /resources/javascript/timbits.csi.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Timbits CSI - v0.6.5 3 | Copyright (c) 2012 Postmedia Network Inc 4 | Written by Edward de Groot (http://mred9.com) 5 | Licensed under the MIT license 6 | https://github.com/Postmedia/timbits 7 | */ 8 | var timbits=timbits||{};timbits.renderCSI=function(e,t){switch(e.attr("data-mode")){case"append":e.append(t);break;case"prepend":e.prepend(t);break;default:e.html(t)}e.trigger("load")},timbits.processCSI=function(e,t){var n=jQuery(t),r=n.attr("data-csi"),i=!0;media=n.attr("data-media"),media&&window.matchMedia&&(mql=window.matchMedia(media),i=mql.matches,mql.matches||mql.addListener(timbits.discoverCSI));if(i){n.removeAttr("data-csi");var s=/{#(\w+)}/,o;while((o=r.match(s))!=null)r=r.replace(o[0],timbits.csi_qs_params[o[1]]?timbits.csi_qs_params[o[1]]:"");console.log("Processing CSI: "+r),r.search(/http:|https:/i)===0?(r+=r.indexOf("?")===-1?"?":"&",r+="callback=?",jQuery.getJSON(r,function(e){timbits.renderCSI(n,e)})):jQuery.get(r,function(e){timbits.renderCSI(n,e)})}},timbits.discoverCSI=function(e){console.log("Discovering CSI: "+(e?e.media:"Init")),jQuery("[data-csi]").each(timbits.processCSI)},jQuery(function(){timbits.csi_qs_params={};var e=/[\?\&](\w+)=([^&]*)/g,t;while((t=e.exec(location.search))!=null)timbits.csi_qs_params[t[1]]=t[2];timbits.discoverCSI()}); -------------------------------------------------------------------------------- /resources/stylesheets/timbits.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Postmedia-Archive/timbits/0213593db6cf94bb35679a4fad01588b19f7fe62/resources/stylesheets/timbits.css -------------------------------------------------------------------------------- /test/timbits-test.js: -------------------------------------------------------------------------------- 1 | // reduce logging levels to provide clean test feedback 2 | process.env.TIMBITS_VERBOSITY = 'critical'; 3 | process.env.PANTRY_VERBOSITY = 'critical'; 4 | 5 | // load modules 6 | var timbits = require('../lib/timbits') 7 | , should = require('should') 8 | , path = require('path') 9 | , request = require('request') 10 | 11 | // load hhtp extensions for should 12 | require('should-http'); 13 | 14 | // set testing environment 15 | var homeFolder = path.join(process.cwd(), "examples") 16 | , port = 8785 17 | , alltests = process.env.TIMBITS_TEST_WHICH === 'all'; 18 | 19 | process.env.NODE_ENV = 'test'; 20 | 21 | // create test server 22 | var server = timbits.serve({ 23 | home: homeFolder, 24 | port: port, 25 | verbosity: 'critical' 26 | }); 27 | 28 | // helper function for testing requests 29 | function validateRequest(method, vpath, expect) { 30 | expect = expect || 'html'; 31 | 32 | describe(vpath, function() { 33 | var test_msg = "should respond with " + expect + " and status 200"; 34 | if (typeof expect !== 'string') 35 | test_msg = "should respond with status " + expect; 36 | 37 | it(test_msg, function(done) { 38 | if (method=="GET") { 39 | request("http://localhost:" + port + vpath, function(err, res) { 40 | should.not.exist(err); 41 | if (typeof expect === 'string') { 42 | res.should.have.status(200); 43 | if (expect === 'json') 44 | res.should.be.json; 45 | else 46 | res.should.be.html; 47 | } else { 48 | res.should.have.status(expect); 49 | } 50 | done(); 51 | }); 52 | } 53 | else if (method=="POST") { 54 | request.post("http://localhost:" + port + vpath, function(err, res) { 55 | should.not.exist(err); 56 | if (typeof expect === 'string') { 57 | res.should.have.status(200); 58 | if (expect === 'json') 59 | res.should.be.json; 60 | else 61 | res.should.be.html; 62 | } else { 63 | res.should.have.status(expect); 64 | } 65 | done(); 66 | }); 67 | } 68 | }); 69 | }); 70 | }; 71 | 72 | describe('timbits', function() { 73 | 74 | describe('default resources', function() { 75 | validateRequest('GET', '/timbits/help'); 76 | validateRequest('GET', '/timbits/json', 'json'); 77 | }); 78 | 79 | describe('individual help pages', function() { 80 | for (var name in timbits.box) { 81 | validateRequest('GET', "/" + name + "/help"); 82 | } 83 | }); 84 | 85 | describe('individual test pages', function() { 86 | for (var name in timbits.box) { 87 | validateRequest('GET', "/" + name + "/test"); 88 | } 89 | }); 90 | 91 | describe('automated test cases', function() { 92 | for (name in timbits.box) { 93 | 94 | timbit = timbits.box[name]; 95 | 96 | // GET requests 97 | if (timbit.examples != null && timbit.methods['GET']) { 98 | describe('specified examples for ' + name, function() { 99 | timbit.examples.forEach(function(example) { 100 | validateRequest('GET', example.href); 101 | }); 102 | }); 103 | } 104 | // Post Requests 105 | if (timbit.examples != null && timbit.methods['POST']) { 106 | describe('specified examples for ' + name, function() { 107 | timbit.examples.forEach(function(example) { 108 | validateRequest('POST', example.href); 109 | }); 110 | }); 111 | } 112 | var dynatests = timbit.generateTests(alltests); 113 | if (dynatests.length) { 114 | describe("dynamic tests for " + name, function() { 115 | dynatests.forEach(function(href) { 116 | if (timbit.methods['GET']) 117 | validateRequest('GET', href); 118 | if (timbit.methods['POST']) 119 | validateRequest('POST', href); 120 | }); 121 | }); 122 | } 123 | } 124 | }); 125 | 126 | }); --------------------------------------------------------------------------------