├── .gitignore ├── lib ├── formatters │ ├── count.js │ ├── json.js │ ├── safe.js │ ├── url-encode.js │ ├── htmltag.js │ ├── slugify.js │ ├── htmlattr.js │ ├── json-pretty.js │ ├── timesince.js │ ├── comment-link.js │ ├── smartypants.js │ ├── activate-twitter-links.js │ ├── date.js │ ├── product-status.js │ ├── social-button.js │ ├── pluralize.js │ ├── social-button-inline.js │ ├── like-button.js │ ├── comment-count.js │ ├── image-meta.js │ ├── product-price.js │ ├── comments.js │ ├── image.js │ ├── product-checkout.js │ ├── item-classes.js │ └── index.js ├── predicates │ ├── odd.js │ ├── even.js │ ├── excerpt.js │ ├── event.js │ ├── image.js │ ├── video.js │ ├── collection.js │ ├── comments.js │ ├── passthrough.js │ ├── external-link.js │ ├── folder.js │ ├── index-predicate.js │ ├── equal.js │ ├── lessThan.js │ ├── greaterThan.js │ ├── disqus.js │ ├── lessThanOrEqual.js │ ├── collectionTypeNameEquals.js │ ├── greaterThanOrEqual.js │ ├── main-image.js │ ├── location.js │ └── index.js ├── util.js └── jsontemplate.js ├── README.md ├── LICENSE ├── squarespace-jsont.js ├── package.json └── .jshintrc /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | test* 4 | .notes 5 | npm-debug.log -------------------------------------------------------------------------------- /lib/formatters/count.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: count 4 | * Returns the length of a collection 5 | * 6 | * @usage: {@|count} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return val.length; 11 | }; -------------------------------------------------------------------------------- /lib/formatters/json.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: json 4 | * Returns object JSON representation 5 | * 6 | * @usage: {@|json} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return JSON.stringify( val ); 11 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-squarespace-jsont 2 | ====================== 3 | 4 | > This project is deprecated and you can now use the [official Squarespace local development server](https://engineering.squarespace.com/blog/2016/the-developer-platform-gets-local). 5 | -------------------------------------------------------------------------------- /lib/predicates/odd.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: odd 4 | * Evaluates a value as odd 5 | * 6 | * @usage: {.odd?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | return !require( "./even" )( data, ctx, args ); 11 | }; -------------------------------------------------------------------------------- /lib/predicates/even.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: even 4 | * Evaluates a value as even 5 | * 6 | * @usage: {.even?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | return (ctx._LookUpStack( args[ 0 ] ) % 2 == 0); 11 | }; -------------------------------------------------------------------------------- /lib/predicates/excerpt.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: excerpt 4 | * Evaluates collections excerpt 5 | * 6 | * @usage: {.excerpt?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | return ( data.excerpt ) ? true : false; 11 | }; -------------------------------------------------------------------------------- /lib/formatters/safe.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: safe 4 | * Returns a string with tags stripped out 5 | * 6 | * @usage: {@|safe} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return require( "phpjs" ).strip_tags( val ); 11 | }; -------------------------------------------------------------------------------- /lib/predicates/event.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: event 4 | * Evaluates collections event 5 | * 6 | * @usage: {.event?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | return ( data.recordTypeLabel === "event" ) ? true : false; 11 | }; -------------------------------------------------------------------------------- /lib/predicates/image.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: image 4 | * Evaluates collections images 5 | * 6 | * @usage: {.image?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | return ( data.recordTypeLabel === "image" ) ? true : false; 11 | }; -------------------------------------------------------------------------------- /lib/predicates/video.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: video 4 | * Evaluates collections video 5 | * 6 | * @usage: {.video?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | return ( data.recordTypeLabel === "video" ) ? true : false; 11 | }; -------------------------------------------------------------------------------- /lib/formatters/url-encode.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: url-encode 4 | * Returns an encoded url string 5 | * 6 | * @usage: {@|url-encode} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return encodeURI( encodeURIComponent( val ) ); 11 | }; -------------------------------------------------------------------------------- /lib/predicates/collection.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: collection 4 | * Evaluates collections collection 5 | * 6 | * @usage: {.collection?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | return ( data.collection ) ? true : false; 11 | }; -------------------------------------------------------------------------------- /lib/predicates/comments.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: comments 4 | * Evaluates collections comments 5 | * 6 | * @usage: {.comments?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | return ( data.commentState === 1 ) ? true : false; 11 | }; -------------------------------------------------------------------------------- /lib/formatters/htmltag.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: htmltag 4 | * Returns an html tag escaped string 5 | * 6 | * @usage: {@|htmltag} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return require( "../jsontemplate" ).HtmlTagEscape( val ); 11 | }; -------------------------------------------------------------------------------- /lib/formatters/slugify.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: slugify 4 | * Returns a string url in standard slug format 5 | * 6 | * @usage: {@|slugify} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return require( "slug" )( val.toLowerCase() ); 11 | }; -------------------------------------------------------------------------------- /lib/predicates/passthrough.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: passthrough 4 | * Evaluates collections passthrough 5 | * 6 | * @usage: {.passthrough?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | return ( data.passthrough ) ? true : false; 11 | }; -------------------------------------------------------------------------------- /lib/formatters/htmlattr.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: htmlattr 4 | * Returns an html tag escaped string 5 | * 6 | * @usage: {@|htmlattr} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return require( "../jsontemplate" ).HtmlTagEscape( val ); 11 | }; -------------------------------------------------------------------------------- /lib/formatters/json-pretty.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: json-pretty 4 | * Returns object JSON representation formatted 5 | * 6 | * @usage: {@|json-pretty} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return JSON.stringify( val, null, 4 ); 11 | }; -------------------------------------------------------------------------------- /lib/predicates/external-link.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: external-link 4 | * Evaluates collections external-link 5 | * 6 | * @usage: {.external-link?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | return ( data.externalLink ) ? true : false; 11 | }; -------------------------------------------------------------------------------- /lib/formatters/timesince.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: timesince 4 | * Returns a formatted date string for `time since date` 5 | * 6 | * @usage: {@|timesince} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return require( "moment" )( val ).fromNow(); 11 | }; -------------------------------------------------------------------------------- /lib/predicates/folder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: folder 4 | * Evaluates collections folder 5 | * 6 | * @usage: {.folder?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | return ( (data.collection && data.collection.folder) || data.folder ) ? true : false; 11 | }; -------------------------------------------------------------------------------- /lib/predicates/index-predicate.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: index 4 | * Evaluates collections index 5 | * 6 | * @usage: {.index?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | return ( (data.collection && data.collection.typeName === "index") || data.typeName === "index" ) ? true : false; 11 | }; -------------------------------------------------------------------------------- /lib/formatters/comment-link.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: comment-link 4 | * Returns a comment-link block... 5 | * 6 | * @usage: {@|comment-link} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return ''; 11 | }; -------------------------------------------------------------------------------- /lib/formatters/smartypants.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: smartypants 4 | * Translates plain ASCII punctuation characters into “smart” 5 | * typographic punctuation HTML entities. 6 | * 7 | * @usage: {@|smartypants} 8 | * 9 | */ 10 | module.exports = function ( val, args, ctx ) { 11 | return require( "typogr" ).smartypants( val ); 12 | }; -------------------------------------------------------------------------------- /lib/predicates/equal.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: equal 4 | * Evaluates 2 values as equal 5 | * 6 | * @usage: {.equal? foo "bar"} 7 | * 8 | */ 9 | var util = require( "../util" ); 10 | 11 | 12 | module.exports = function ( data, ctx, args ) { 13 | args = util.getArgsProcessed( args, ctx ); 14 | 15 | return ( args[ 0 ] == args[ 1 ] ); 16 | }; -------------------------------------------------------------------------------- /lib/formatters/activate-twitter-links.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: activate-twitter-links 4 | * Returns a string with links activated 5 | * 6 | * @usage: {@|activate-twitter-links} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return val.replace( /(^|\s)@(\w+)/g, "$1@$2" ); 11 | }; -------------------------------------------------------------------------------- /lib/predicates/lessThan.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: lessThan 4 | * Evaluates a value to be greater than another 5 | * 6 | * @usage: {.lessThan? @index 2} 7 | * 8 | */ 9 | var util = require( "../util" ); 10 | 11 | 12 | module.exports = function ( data, ctx, args ) { 13 | args = util.getArgsProcessed( args, ctx ); 14 | 15 | return ( args[ 0 ] < args[ 1 ] ); 16 | }; -------------------------------------------------------------------------------- /lib/predicates/greaterThan.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: greaterThan 4 | * Evaluates a value to be greater than another 5 | * 6 | * @usage: {.greaterThan? @index 2} 7 | * 8 | */ 9 | var util = require( "../util" ); 10 | 11 | 12 | module.exports = function ( data, ctx, args ) { 13 | args = util.getArgsProcessed( args, ctx ); 14 | 15 | return ( args[ 0 ] > args[ 1 ] ); 16 | }; -------------------------------------------------------------------------------- /lib/predicates/disqus.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: disqus 4 | * Evaluates collections disqus 5 | * 6 | * @usage: {.disqus?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | // The only place `disqus` appears in data is here: 11 | // websiteSettings.disqusShortname 12 | // So, not really sure how to use this at the item level. 13 | return false; 14 | }; -------------------------------------------------------------------------------- /lib/predicates/lessThanOrEqual.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: lessThanOrEqual 4 | * Evaluates a value to be greater than another 5 | * 6 | * @usage: {.lessThanOrEqual? @index 2} 7 | * 8 | */ 9 | var util = require( "../util" ); 10 | 11 | 12 | module.exports = function ( data, ctx, args ) { 13 | args = util.getArgsProcessed( args, ctx ); 14 | 15 | return ( args[ 0 ] <= args[ 1 ] ); 16 | }; -------------------------------------------------------------------------------- /lib/predicates/collectionTypeNameEquals.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: collectionTypeNameEquals 4 | * Evaluates collections typeName 5 | * 6 | * @usage: {.collectionTypeNameEquals? "foo"} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | var typeName = String( args[ 0 ].replace( /"|'/g, "" ) ); 11 | 12 | return (data.typeName && data.typeName === typeName || false); 13 | }; -------------------------------------------------------------------------------- /lib/predicates/greaterThanOrEqual.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: greaterThanOrEqual 4 | * Evaluates a value to be greater than another 5 | * 6 | * @usage: {.greaterThanOrEqual? @index 2} 7 | * 8 | */ 9 | var util = require( "../util" ); 10 | 11 | 12 | module.exports = function ( data, ctx, args ) { 13 | args = util.getArgsProcessed( args, ctx ); 14 | 15 | return ( args[ 0 ] >= args[ 1 ] ); 16 | }; -------------------------------------------------------------------------------- /lib/formatters/date.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: date 4 | * Returns a formatted date string 5 | * 6 | * @usage: {@|date `format`} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | // Divide by 1000 since phpjs accounts for this: 11 | // PHP API expects UNIX timestamp (auto-convert to int) 12 | return require( "phpjs" ).strftime( args.join( " " ), (parseInt( val, 10 ) / 1000) ); 13 | }; -------------------------------------------------------------------------------- /lib/formatters/product-status.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: product-status 4 | * Returns a product status block... 5 | * 6 | * @usage: {@|product-status} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | var ret = ""; 11 | 12 | if ( val.variants && val.variants[ 0 ] && val.variants[ 0 ].qtyInStock === 0 ) { 13 | ret = '
sold out
'; 14 | } 15 | 16 | return ret; 17 | }; -------------------------------------------------------------------------------- /lib/formatters/social-button.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: social-button 4 | * Returns a social button... 5 | * 6 | * @usage: {@|social-button} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return '
'; 11 | }; -------------------------------------------------------------------------------- /lib/formatters/pluralize.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: pluralize 4 | * Returns a pluralized string... 5 | * 6 | * @usage: {@|pluralize} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | var pluralizer = "s"; 11 | 12 | if ( args && args.length > 1 ) { 13 | pluralizer = ( val > 0 ) ? args[ 1 ] : args[ 0 ]; 14 | 15 | } else if ( args && args.length ) { 16 | pluralizer = args[ 0 ]; 17 | } 18 | 19 | return ( val > 0 ) ? pluralizer : ""; 20 | }; -------------------------------------------------------------------------------- /lib/formatters/social-button-inline.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: social-button-inline 4 | * Returns a social button... 5 | * 6 | * @usage: {@|social-button-inline} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | return '
'; 11 | }; -------------------------------------------------------------------------------- /lib/predicates/main-image.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: main-image 4 | * Evaluates collections mainImage 5 | * 6 | * @usage: {.main-image?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | var ret = false; 11 | 12 | // 0.1 => A collection 13 | // 0.2 => A collection item 14 | if ( (data.mainImage && data.mainImage.systemDataOrigin === "USER_UPLOAD") || (data.systemDataOrigin === "USER_UPLOAD") ) { 15 | ret = true; 16 | } 17 | 18 | return ret; 19 | }; -------------------------------------------------------------------------------- /lib/formatters/like-button.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: like-button 4 | * Returns a like button... 5 | * 6 | * @usage: {@|like-button} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | var ret = ""; 11 | 12 | ret += ''; 13 | ret += ''; 14 | ret += ''; 15 | ret += ''; 16 | 17 | return ret; 18 | }; -------------------------------------------------------------------------------- /lib/formatters/comment-count.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: comment-count 4 | * Returns a comment-count block... 5 | * 6 | * @usage: {@|comment-count} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | var ret = "[comment-count?]"; 11 | 12 | if ( !val.commentCount ) { 13 | ret = "No Comments"; 14 | 15 | } else if ( val.commentCount === 1 ) { 16 | ret = "1 Comment"; 17 | 18 | } else { 19 | ret = val.commentCount + " Comments"; 20 | } 21 | 22 | return ret; 23 | }; -------------------------------------------------------------------------------- /lib/formatters/image-meta.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: image-meta 4 | * Returns the render for an images meta data 5 | * 6 | * @usage: {@|image-meta} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | var focalPoint; 11 | 12 | if (val.mediaFocalPoint) { 13 | focalPoint = val.mediaFocalPoint.x + ',' + val.mediaFocalPoint.y; 14 | } 15 | 16 | return 'data-src="' + val.assetUrl + '" data-image="' + val.assetUrl + '" data-image-dimensions="' + val.originalSize + '" data-image-focal-point="' + focalPoint + '" alt="' + val.filename + '"'; 17 | }; -------------------------------------------------------------------------------- /lib/formatters/product-price.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: product-price 4 | * Returns a product price block... 5 | * 6 | * @usage: {@|product-price} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | var price = ("" + (val.variants[ 0 ].price / 100)); 11 | price = price.split( "." ); 12 | 13 | if ( !price[ 1 ] ) { 14 | price[ 1 ] = "00"; 15 | 16 | } else if ( price[ 1 ].length === 1 ) { 17 | price[ 1 ] = (price[ 1 ] + "0"); 18 | } 19 | 20 | price = price.join( "." ); 21 | 22 | return '
' + price + '
'; 23 | }; -------------------------------------------------------------------------------- /lib/formatters/comments.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: comments 4 | * Returns a comments block... 5 | * 6 | * @usage: {@|comments} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | //return '
'; 11 | 12 | /** 13 | * 14 | * XMLHttpRequest cannot load 15 | * https://XXX.squarespace.com/api/comment/GetComments?targetId=XXX&targetType=1&since=&page=1&sortBy=2 16 | * No 'Access-Control-Allow-Origin' header is present on the requested resource. 17 | * Origin 'http://localhost:XXXX' is therefore not allowed access. 18 | * 19 | */ 20 | 21 | return "[comments?]"; 22 | }; -------------------------------------------------------------------------------- /lib/predicates/location.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Predicate: location 4 | * Evaluates collections location 5 | * 6 | * @usage: {.location?} 7 | * 8 | */ 9 | module.exports = function ( data, ctx, args ) { 10 | // All items have default location info: 11 | // mapLat 12 | // mapLng 13 | // markerLat 14 | // markerLng 15 | 16 | // Custom location info adds extra fields: 17 | // addressTitle 18 | // addressLine1 19 | // addressLine2 20 | // addressCountry 21 | 22 | // Assume that any 1 of these additional fields means item has a location 23 | return ( data.location.addressTitle || data.location.addressLine1 || data.location.addressLine2 || data.location.addressCountry ) ? true : false; 24 | }; -------------------------------------------------------------------------------- /lib/formatters/image.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: image 4 | * Returns the render for an image 5 | * 6 | * @usage: {@|image} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | var focalPoint; 11 | 12 | if (val.mediaFocalPoint) { 13 | focalPoint = val.mediaFocalPoint.x + ',' + val.mediaFocalPoint.y; 14 | } 15 | 16 | return '' + val.filename + ''; 17 | }; -------------------------------------------------------------------------------- /lib/formatters/product-checkout.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: product-checkout 4 | * Returns a product checkout block... 5 | * 6 | * @usage: {@|product-checkout} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | var ret = ""; 11 | 12 | ret += '
'; 13 | ret += '
Quantity:
'; 14 | ret += ''; 15 | ret += '
'; 16 | ret += '
'; 17 | ret += '
'; 18 | ret += '
'; 19 | ret += val.customAddButtonText; 20 | ret += '
'; 21 | ret += '
'; 22 | ret += '
'; 23 | 24 | return ret; 25 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Brandon Lee Kitajchuk 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /squarespace-jsont.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Squarespace JSON-T Rendering Engine. 4 | * 5 | */ 6 | var jsonTemplate = require( "./lib/jsontemplate" ), 7 | jsontOptions = { 8 | more_formatters: require( "./lib/formatters/index" ), 9 | more_predicates: require( "./lib/predicates/index" ), 10 | undefined_str: "" 11 | }, 12 | 13 | 14 | /****************************************************************************** 15 | * @Public 16 | *******************************************************************************/ 17 | 18 | /** 19 | * 20 | * @method render 21 | * @param {string} render The template string 22 | * @param {object} data The data context 23 | * @returns {string} 24 | * @public 25 | * 26 | */ 27 | render = function ( template, data ) { 28 | template = jsonTemplate.Template( template, jsontOptions ); 29 | template = template.expand( data ); 30 | 31 | return template; 32 | }; 33 | 34 | 35 | /****************************************************************************** 36 | * @Export 37 | *******************************************************************************/ 38 | module.exports = { 39 | render: render 40 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Node Squarespace JSONT", 3 | "name": "node-squarespace-jsont", 4 | "description": "The JSONT template rendering engine for Squarespace in node.js.", 5 | "version": "0.1.23", 6 | "homepage": "https://github.com/NodeSquarespace/node-squarespace-jsont", 7 | "readmeFilename": "README.md", 8 | "contributors": [ 9 | { 10 | "name": "Brandon Kitajchuk", 11 | "email": "kitajchuk@gmail.com", 12 | "url": "http://blkpdx.com" 13 | } 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/NodeSquarespace/node-squarespace-jsont.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/NodeSquarespace/node-squarespace-jsont/issues" 21 | }, 22 | "licenses": [ 23 | { 24 | "type": "MIT", 25 | "url": "https://github.com/NodeSquarespace/node-squarespace-jsont/blob/master/LICENSE" 26 | } 27 | ], 28 | "main": "squarespace-jsont", 29 | "dependencies": { 30 | "moment": "~2.8", 31 | "phpjs": "~1.3", 32 | "slug": "~0.7", 33 | "typogr": "^0.6.5" 34 | }, 35 | "keywords": [ 36 | "squarespace", 37 | "json-template", 38 | "jsont", 39 | "json" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /lib/formatters/item-classes.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Formatter: item-classes 4 | * Returns a formatted className string for an item 5 | * 6 | * @usage: {@|item-classes} 7 | * 8 | */ 9 | module.exports = function ( val, args, ctx ) { 10 | var classes = 'hentry'; 11 | 12 | if ( val.promotedBlockType ) { 13 | classes += ' promoted promoted-block-' + val.promotedBlockType; 14 | } 15 | 16 | if ( val.categories ) { 17 | classes += val.categories.map(function ( value ) { 18 | return ' category-' + require( "slug" )( value.toLowerCase() ); 19 | }).join(' '); 20 | } 21 | 22 | if ( val.tags ) { 23 | classes += val.tags.map(function ( value ) { 24 | return ' tag-' + require( "slug" )( value.toLowerCase() ); 25 | }).join(' '); 26 | } 27 | 28 | if ( val.starred ) { 29 | classes += ' featured'; 30 | } 31 | 32 | if ( val.structuredContent ) { 33 | if ( val.structuredContent.onSale ) { 34 | classes += ' on-sale'; 35 | } 36 | } 37 | 38 | classes += ' author-' + require( "slug" )( val.author.displayName.toLowerCase() ) 39 | classes += ' post-type-' + require( "slug" )( val.recordTypeLabel.toLowerCase() ); 40 | classes += ' article-index-' + ctx._LookUpStack( '@index' ); 41 | 42 | return classes; 43 | }; -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr": 50, 3 | "bitwise": true, 4 | "camelcase": false, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "forin": false, 8 | "immed": true, 9 | "indent": false, 10 | "latedef": false, 11 | "newcap": true, 12 | "noarg": true, 13 | "noempty": true, 14 | "nonew": false, 15 | "plusplus": false, 16 | "quotmark": false, 17 | "undef": false, 18 | "unused": true, 19 | "strict": false, 20 | "maxparams": false, 21 | "maxdepth": false, 22 | "maxstatements": false, 23 | "maxcomplexity": false, 24 | "maxlen": false, 25 | "asi": false, 26 | "boss": false, 27 | "debug": false, 28 | "eqnull": false, 29 | "es5": false, 30 | "esnext": true, 31 | "moz": false, 32 | "evil": false, 33 | "expr": false, 34 | "funcscope": false, 35 | "globalstrict": false, 36 | "iterator": false, 37 | "lastsemic": false, 38 | "laxbreak": false, 39 | "laxcomma": false, 40 | "loopfunc": false, 41 | "multistr": false, 42 | "proto": false, 43 | "scripturl": false, 44 | "shadow": false, 45 | "sub": true, 46 | "supernew": false, 47 | "validthis": false, 48 | "browser": true, 49 | "couch": false, 50 | "devel": true, 51 | "dojo": false, 52 | "jquery": true, 53 | "mootools": false, 54 | "node": true, 55 | "nonstandard": false, 56 | "prototypejs": false, 57 | "rhino": false, 58 | "worker": false, 59 | "wsh": false, 60 | "yui": false, 61 | "globals": {} 62 | } -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var rQuotes = /"|'/g; 2 | 3 | // Sourced from https://code.jquery.com/ 4 | var isNumeric = function ( obj ) { 5 | return !Array.isArray( obj ) && obj - parseFloat( obj ) >= 0; 6 | }; 7 | 8 | var getLookupArg = function ( ctx, arg ) { 9 | var get = ctx.get( arg ), 10 | lookup = ctx._LookUpStack( arg ), 11 | lookupIsNumeric = isNumeric( lookup ), 12 | getIsNumeric = isNumeric( get ); 13 | 14 | if ( (lookup !== undefined && lookup !== "") || lookupIsNumeric ) { 15 | arg = lookup; 16 | 17 | } else if ( (get !== undefined && get !== "") || getIsNumeric ) { 18 | arg = get; 19 | } 20 | 21 | return arg; 22 | }; 23 | 24 | var getProcessedArgs = function ( args, ctx ) { 25 | var arg1 = args[ 0 ], 26 | arg2 = args[ 1 ]; 27 | 28 | // One of the args is a string literal 29 | if ( rQuotes.test( arg1 ) ) { 30 | arg1 = String( arg1.replace( rQuotes, "" ) ); 31 | arg2 = ( ctx._LookUpStack( arg2 ) || ctx.get( arg2 ) ); 32 | 33 | } else if ( rQuotes.test( arg2 ) ) { 34 | arg2 = String( arg2.replace( rQuotes, "" ) ); 35 | arg1 = ( ctx._LookUpStack( arg1 ) || ctx.get( arg1 ) ); 36 | 37 | // Just make sure we don't mess up numeric values 38 | } else { 39 | if ( !isNumeric( arg1 ) ) { 40 | arg1 = getLookupArg( ctx, arg1 ); 41 | } 42 | 43 | if ( !isNumeric( arg2 ) ) { 44 | arg2 = getLookupArg( ctx, arg2 ); 45 | } 46 | } 47 | 48 | return [arg1, arg2]; 49 | }; 50 | 51 | var getArgsProcessed = function ( args, ctx ) { 52 | var arg1 = args[ 0 ], 53 | arg2 = args[ 1 ]; 54 | 55 | // Special Case 56 | // {.equal? typeName "collection"} 57 | // {.equal? title "collection"} 58 | // {...and so on...} 59 | // Cannot use `collection` string with the lookup stack. 60 | // It will return the {collection object} rather than a lookup value. 61 | if ( String( arg2.replace( rQuotes, "" ) ) === "collection" ) { 62 | arg1 = getLookupArg( ctx, arg1 ); 63 | 64 | // Use Cases 65 | // {.equal? foo.bar "baz"} 66 | // {.equal? foo "bar"} 67 | // {.equal? @index 5} 68 | // {.equal? "foo" foo.bar} 69 | // {... and so on...} 70 | } else { 71 | args = getProcessedArgs( args, ctx ); 72 | arg1 = args[ 0 ]; 73 | arg2 = args[ 1 ]; 74 | } 75 | 76 | return args; 77 | }; 78 | 79 | module.exports = { 80 | rQuotes: rQuotes, 81 | isNumeric: isNumeric, 82 | getLookupArg: getLookupArg, 83 | //getProcessedArgs: getProcessedArgs, 84 | getArgsProcessed: getArgsProcessed 85 | }; -------------------------------------------------------------------------------- /lib/formatters/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Squarespace formatters. 4 | * 5 | * http://jsont.squarespace.com/ 6 | * http://developers.squarespace.com/json-formatters/ 7 | * 8 | */ 9 | var defaultFormatters = [ 10 | "html", 11 | "htmltag", 12 | "html-attr-value", 13 | "str", 14 | "raw", 15 | "AbsUrl", 16 | "plain-url" 17 | ], 18 | formatters = { 19 | "count": require( "./count" ), 20 | "date": require( "./date" ), 21 | "timesince": require( "./timesince" ), 22 | "item-classes": require( "./item-classes" ), 23 | "image": require( "./image" ), 24 | "image-meta": require( "./image-meta" ), 25 | "json": require( "./json" ), 26 | "json-pretty": require( "./json-pretty" ), 27 | "slugify": require( "./slugify" ), 28 | "url-encode": require( "./url-encode" ), 29 | "htmlattr": require( "./htmlattr" ), 30 | "htmltag": require( "./htmltag" ), 31 | "activate-twitter-links": require( "./activate-twitter-links" ), 32 | "safe": require( "./safe" ), 33 | "social-button": require( "./social-button" ), 34 | "social-button-inline": require( "./social-button-inline" ), 35 | "comments": require( "./comments" ), 36 | "comment-link": require( "./comment-link" ), 37 | "comment-count": require( "./comment-count" ), 38 | "like-button": require( "./like-button" ), 39 | "product-price": require( "./product-price" ), 40 | "product-status": require( "./product-status" ), 41 | "product-checkout": require( "./product-checkout" ), 42 | "pluralize": require( "./pluralize" ), 43 | "smartypants": require( "./smartypants" ) 44 | }; 45 | 46 | 47 | 48 | module.exports = function ( arg ) { 49 | var index = arg.indexOf( " " ), 50 | formatter = null, 51 | args = null; 52 | 53 | if ( index === -1 ) { 54 | formatter = arg; 55 | args = []; 56 | 57 | } else { 58 | formatter = arg.substr( 0, index ); 59 | args = arg.substr( index + 1 ).split( " " ); 60 | } 61 | 62 | // Test unique case on original arg: 63 | // pluralize/It depends/They depend 64 | // http://jsont.squarespace.com/#Pluralize 65 | if ( /^pluralize\//.test( arg ) ) { 66 | formatter = "pluralize"; 67 | args = arg.split( "/" ).slice( 1 ); 68 | } 69 | 70 | // Only run if formatter is NOT a JSONT default internal 71 | if ( defaultFormatters.indexOf( formatter ) === -1 ) { 72 | // Wrapper function for passing along to correct formatter 73 | return function ( string, context ) { 74 | var func = formatters[ formatter ], 75 | ret = false; 76 | 77 | if ( typeof func === "function" ) { 78 | // Normalize `args` as an Array excluding `formatter` 79 | ret = func( string, args, context ); 80 | } 81 | 82 | return ret; 83 | }; 84 | } 85 | }; -------------------------------------------------------------------------------- /lib/predicates/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Squarespace predicates. 4 | * 5 | * http://jsont.squarespace.com/ 6 | * http://jsont.squarespace.com/scoping-example/ 7 | * http://developers.squarespace.com/quick-reference/ 8 | * 9 | */ 10 | var defaultPredicates = [ 11 | "singular", 12 | "plural", 13 | "singular?", 14 | "plural?", 15 | "Debug?" 16 | ], 17 | predicates = { 18 | "main-image?": require( "./main-image" ), 19 | "excerpt?": require( "./excerpt" ), 20 | "comments?": require( "./comments" ), 21 | "even?": require( "./even" ), 22 | "odd?": require( "./odd" ), 23 | "equal?": require( "./equal" ), 24 | "collection?": require( "./collection" ), 25 | "external-link?": require( "./external-link" ), 26 | "folder?": require( "./folder" ), 27 | "location?": require( "./location" ), 28 | "event?": require( "./event" ), 29 | "disqus?": require( "./disqus" ), 30 | "video?": require( "./video" ), 31 | "collectionTypeNameEquals?": require( "./collectionTypeNameEquals" ), 32 | "image?": require( "./image" ), 33 | "passthrough?": require( "./passthrough" ), 34 | "index?": require( "./index-predicate" ), 35 | "greaterThan?": require( "./greaterThan" ), 36 | "greaterThanOrEqual?": require( "./greaterThanOrEqual" ), 37 | "lessThan?": require( "./lessThan" ), 38 | "lessThanOrEqual?": require( "./lessThanOrEqual" ) 39 | }; 40 | 41 | 42 | 43 | module.exports = function ( arg ) { 44 | // get index for predicate, with handling for equal with non-space delimiters 45 | var index = arg.indexOf(arg.split('?')[1] ? arg.split('?')[1][0] : ' '), 46 | predicate = null, 47 | args = null; 48 | 49 | if ( index === -1 ) { 50 | predicate = arg; 51 | args = []; 52 | 53 | } else { 54 | predicate = arg.substr( 0, index ); 55 | 56 | //get delimiter for equal if not a space 57 | var delimiter = arg.split('?')[1] ? arg.split('?')[1][0] : ' '; 58 | var reg1 = new RegExp('^\\s+|\\s+$|' + delimiter, "g"); 59 | var reg2 = new RegExp('\\s|' + delimiter, "g"); 60 | 61 | // Clean predicate out of args now 62 | arg = arg.replace( predicate, "" ).replace( /^\s+|\s+$/g, "" ); 63 | 64 | // Filter and normalize the arguments list 65 | args = arg.split( (/\"/.test( arg ) ? /\"(.*?)\"/g : /\s/g) ) 66 | .map(function ( el ) { 67 | return el.replace(reg1, "" ); 68 | }) 69 | .filter(function ( el ) { 70 | var tmp = el.replace(reg2, "" ); 71 | 72 | return ( tmp !== "" ); 73 | }); 74 | } 75 | 76 | // Only run if predicate is NOT a JSONT default internal 77 | if ( defaultPredicates.indexOf( predicate ) === -1 ) { 78 | // Wrapper function for passing along to correct predicate 79 | return function ( data, context ) { 80 | var func = predicates[ predicate ], 81 | ret = false; 82 | 83 | // Capture {.if} conditions 84 | if ( !/\?$/.test( predicate ) ) { 85 | ret = context.get( arg ); 86 | 87 | } else if ( typeof func === "function" ) { 88 | // Normalize `args` as an Array excluding `predicate` 89 | ret = func( data, context, args ); 90 | } 91 | 92 | return ret; 93 | }; 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /lib/jsontemplate.js: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2009 Andy Chu 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // 16 | // JavaScript implementation of json-template. 17 | // 18 | 19 | // This is predefined in tests, shouldn't be defined anywhere else. TODO: Do 20 | // something nicer. 21 | var log = log || function() {}; 22 | var repr = repr || function() {}; 23 | 24 | 25 | // The "module" exported by this script is called "jsontemplate": 26 | 27 | var jsontemplate = function() { 28 | 29 | 30 | // Regex escaping for metacharacters 31 | function EscapeMeta(meta) { 32 | return meta.replace(/([\{\}\(\)\[\]\|\^\$\-\+\?])/g, '\\$1'); 33 | } 34 | 35 | var token_re_cache = {}; 36 | 37 | function _MakeTokenRegex(meta_left, meta_right) { 38 | var key = meta_left + meta_right; 39 | var regex = token_re_cache[key]; 40 | if (regex === undefined) { 41 | // there must at least one character inside {}, and the first one must be a 42 | // non-space, to be accepting of "function() { return {@}; }" 43 | var str = '(' + EscapeMeta(meta_left) + '\\S.*?' + EscapeMeta(meta_right) + 44 | '\n?)'; 45 | regex = new RegExp(str, 'g'); 46 | } 47 | return regex; 48 | } 49 | 50 | // 51 | // Formatters 52 | // 53 | 54 | function HtmlEscape(s) { 55 | return s.replace(/&/g,'&'). 56 | replace(/>/g,'>'). 57 | replace(//g,'>'). 63 | replace(/ 1) ? p : s; 95 | } 96 | 97 | function _Cycle(value, unused_context, args) { 98 | // Cycle between various values on consecutive integers. 99 | // @index starts from 1, so use 1-based indexing. 100 | return args[(value - 1) % args.length]; 101 | } 102 | 103 | var DEFAULT_FORMATTERS = { 104 | 'html': HtmlEscape, 105 | 'htmltag': HtmlTagEscape, 106 | 'html-attr-value': HtmlTagEscape, 107 | 'str': ToString, 108 | 'raw': function(x) { return x; }, 109 | 'AbsUrl': function(value, context) { 110 | // TODO: Normalize leading/trailing slashes 111 | return context.get('base-url') + '/' + value; 112 | }, 113 | 'plain-url': function(x) { 114 | return '' + HtmlEscape(x) + '' ; 115 | } 116 | }; 117 | 118 | var _TestAttribute = function(unused, context, args) { 119 | var name = args[0]; 120 | if (name === undefined) { 121 | throw { name: 'EvaluationError', 122 | message: 'The "test" predicate requires an argument.' }; 123 | } 124 | try { 125 | return context.get(name); 126 | } catch(err) { 127 | if (err.name == 'UndefinedVariable') { 128 | return false; 129 | } else { 130 | throw err; 131 | } 132 | } 133 | }; 134 | 135 | var singular = function(x) { return x == 1; }; 136 | var plural = function(x) { return x > 1; }; 137 | var DEFAULT_PREDICATES = { 138 | 'singular': singular, 139 | 'plural': plural, 140 | 141 | // OLD: do not use 142 | 'singular?': singular, 143 | 'plural?': plural, 144 | 'Debug?': function(unused, context) { 145 | return _TestAttribute(unused, context, ['debug']); 146 | } 147 | }; 148 | 149 | var FunctionRegistry = function() { 150 | return { 151 | lookup: function(user_str) { 152 | return [null, null]; 153 | } 154 | }; 155 | }; 156 | 157 | var SimpleRegistry = function(obj) { 158 | return { 159 | lookup: function(user_str) { 160 | var func = obj[user_str] || null; 161 | return [func, null]; 162 | } 163 | }; 164 | }; 165 | 166 | var CallableRegistry = function(callable) { 167 | return { 168 | lookup: function(user_str) { 169 | var func = callable(user_str); 170 | return [func, null]; 171 | } 172 | }; 173 | }; 174 | 175 | // Default formatters which can't be expressed in DEFAULT_FORMATTERS 176 | var PrefixRegistry = function(functions) { 177 | return { 178 | lookup: function(user_str) { 179 | for (var i = 0; i < functions.length; i++) { 180 | var name = functions[i].name, func = functions[i].func; 181 | if (user_str.slice(0, name.length) == name) { 182 | // Delimiter is usually a space, but could be something else 183 | var args; 184 | var splitchar = user_str.charAt(name.length); 185 | if (splitchar === '') { 186 | args = []; // No arguments 187 | } else { 188 | args = user_str.split(splitchar).slice(1); 189 | } 190 | return [func, args]; 191 | } 192 | } 193 | return [null, null]; // No formatter 194 | } 195 | }; 196 | }; 197 | 198 | var ChainedRegistry = function(registries) { 199 | return { 200 | lookup: function(user_str) { 201 | for (var i=0; i 1) { 236 | name = name_split.shift(); 237 | new_context = stack[stack.length-1].context[name]; 238 | while (name_split.length) { 239 | name = name_split.shift(); 240 | new_context = new_context[name] || null; 241 | } 242 | } else { 243 | new_context = stack[stack.length-1].context[name] || null; 244 | } 245 | } 246 | stack.push({context: new_context, index: -1}); 247 | return new_context; 248 | }, 249 | 250 | pop: function() { 251 | stack.pop(); 252 | }, 253 | 254 | next: function() { 255 | var stacktop = stack[stack.length-1]; 256 | 257 | // Now we're iterating -- push a new mutable object onto the stack 258 | if (stacktop.index == -1) { 259 | stacktop = {context: null, index: 0}; 260 | stack.push(stacktop); 261 | } 262 | 263 | // The thing we're iterating over 264 | var context_array = stack[stack.length-2].context; 265 | 266 | // We're already done 267 | if (stacktop.index == context_array.length) { 268 | stack.pop(); 269 | return undefined; // sentinel to say that we're done 270 | } 271 | 272 | stacktop.context = context_array[stacktop.index++]; 273 | return true; // OK, we mutated the stack 274 | }, 275 | 276 | _Undefined: function() { 277 | return (undefined_str === undefined) ? undefined : undefined_str; 278 | }, 279 | 280 | _LookUpStack: function(name) { 281 | var i = stack.length - 1; 282 | while (true) { 283 | var frame = stack[i]; 284 | if (name == '@index') { 285 | if (frame.index != -1) { // -1 is undefined 286 | return frame.index; 287 | } 288 | } else { 289 | var context = frame.context; 290 | if (typeof context === 'object') { 291 | var value = context[name]; 292 | if (value !== undefined) { 293 | return value; 294 | } 295 | } 296 | } 297 | i--; 298 | if (i <= -1) { 299 | return this._Undefined(name); 300 | } 301 | } 302 | }, 303 | 304 | get: function(name) { 305 | if (name == '@') { 306 | return stack[stack.length-1].context; 307 | } 308 | var opts = /\|\|/.test(name) ? name.replace(/\s\|\|\s/g, '||').split('||') : null, 309 | parts = name.split('.'), 310 | value, 311 | ctx; 312 | if (opts) { 313 | ctx = stack[stack.length-1].context; 314 | for (var i=0; i 0) { 503 | // TODO: check that items is an array; apparently this is hard in JavaScript 504 | //if type(items) is not list: 505 | // raise EvaluationError('Expected a list; got %s' % type(items)) 506 | 507 | // Execute the statements in the block for every item in the list. 508 | // Execute the alternate block on every iteration except the last. Each 509 | // item could be an atom (string, integer, etc.) or a dictionary. 510 | 511 | var last_index = items.length - 1; 512 | var statements = block.Statements(); 513 | var alt_statements = block.Statements('alternate'); 514 | 515 | for (var i=0; context.next() !== undefined; i++) { 516 | _Execute(statements, context, callback); 517 | if (i != last_index) { 518 | _Execute(alt_statements, context, callback); 519 | } 520 | } 521 | } else { 522 | _Execute(block.Statements('or'), context, callback); 523 | } 524 | 525 | context.pop(); 526 | } 527 | 528 | 529 | var _SECTION_RE = /(repeated)?\s*(section)\s+(\S+)?/; 530 | // Adding carrots - @kitajchuk 531 | var _OR_RE = /^or(?:\s+(.+))?/; 532 | var _IF_RE = /^if(?:\s+(.+))?/; 533 | 534 | 535 | // Turn a object literal, function, or Registry into a Registry 536 | function MakeRegistry(obj) { 537 | if (!obj) { 538 | // if null/undefined, use a totally empty FunctionRegistry 539 | return new FunctionRegistry(); 540 | } else if (typeof obj === 'function') { 541 | return new CallableRegistry(obj); 542 | } else if (obj.lookup !== undefined) { 543 | // TODO: Is this a good pattern? There is a namespace conflict where get 544 | // could be either a formatter or a method on a FunctionRegistry. 545 | // instanceof might be more robust. 546 | return obj; 547 | } else if (typeof obj === 'object') { 548 | return new SimpleRegistry(obj); 549 | } 550 | } 551 | 552 | // TODO: The compile function could be in a different module, in case we want to 553 | // compile on the server side. 554 | function _Compile(template_str, options) { 555 | var more_formatters = MakeRegistry(options.more_formatters); 556 | 557 | // default formatters with arguments 558 | var default_formatters = PrefixRegistry([ 559 | {name: 'pluralize', func: _Pluralize}, 560 | {name: 'cycle', func: _Cycle} 561 | ]); 562 | var all_formatters = new ChainedRegistry([ 563 | more_formatters, 564 | SimpleRegistry(DEFAULT_FORMATTERS), 565 | default_formatters 566 | ]); 567 | 568 | var more_predicates = MakeRegistry(options.more_predicates); 569 | 570 | // default predicates with arguments 571 | var default_predicates = PrefixRegistry([ 572 | {name: 'test', func: _TestAttribute} 573 | ]); 574 | var all_predicates = new ChainedRegistry([ 575 | more_predicates, SimpleRegistry(DEFAULT_PREDICATES), default_predicates 576 | ]); 577 | 578 | // We want to allow an explicit null value for default_formatter, which means 579 | // that an error is raised if no formatter is specified. 580 | var default_formatter; 581 | if (options.default_formatter === undefined) { 582 | default_formatter = 'str'; 583 | } else { 584 | default_formatter = options.default_formatter; 585 | } 586 | 587 | function GetFormatter(format_str) { 588 | var pair = all_formatters.lookup(format_str); 589 | if (!pair[0]) { 590 | throw { 591 | name: 'BadFormatter', 592 | message: format_str + ' is not a valid formatter' 593 | }; 594 | } 595 | return pair; 596 | } 597 | 598 | function GetPredicate(pred_str, test_attr) { 599 | var pair = all_predicates.lookup(pred_str); 600 | if (!pair[0]) { 601 | if (test_attr) { 602 | pair = [_TestAttribute, [pred_str.slice(null, -1)]]; 603 | } else { 604 | throw { 605 | name: 'BadPredicate', 606 | message: pred_str + ' is not a valid predicate' 607 | }; 608 | } 609 | } 610 | return pair; 611 | } 612 | 613 | var format_char = options.format_char || '|'; 614 | if (format_char != ':' && format_char != '|') { 615 | throw { 616 | name: 'ConfigurationError', 617 | message: 'Only format characters : and | are accepted' 618 | }; 619 | } 620 | 621 | var meta = options.meta || '{}'; 622 | var n = meta.length; 623 | if (n % 2 == 1) { 624 | throw { 625 | name: 'ConfigurationError', 626 | message: meta + ' has an odd number of metacharacters' 627 | }; 628 | } 629 | var meta_left = meta.substring(0, n/2); 630 | var meta_right = meta.substring(n/2, n); 631 | 632 | var token_re = _MakeTokenRegex(meta_left, meta_right); 633 | var current_block = _Section({}); 634 | var stack = [current_block]; 635 | 636 | var strip_num = meta_left.length; // assume they're the same length 637 | 638 | var token_match; 639 | var last_index = 0; 640 | 641 | while (true) { 642 | token_match = token_re.exec(template_str); 643 | var token; 644 | if (token_match === null) { 645 | break; 646 | } else { 647 | token = token_match[0]; 648 | } 649 | 650 | // Add the previous literal to the program 651 | if (token_match.index > last_index) { 652 | var tok = template_str.slice(last_index, token_match.index); 653 | current_block.Append(tok); 654 | } 655 | last_index = token_re.lastIndex; 656 | 657 | var had_newline = false; 658 | if (token.slice(-1) == '\n') { 659 | token = token.slice(null, -1); 660 | had_newline = true; 661 | } 662 | 663 | token = token.slice(strip_num, -strip_num); 664 | 665 | if (token.charAt(0) == '#') { 666 | continue; // comment 667 | } 668 | 669 | if (token.charAt(0) == '.') { // Keyword 670 | token = token.substring(1, token.length); 671 | 672 | var literal = { 673 | 'meta-left': meta_left, 674 | 'meta-right': meta_right, 675 | 'space': ' ', 676 | 'tab': '\t', 677 | 'newline': '\n' 678 | }[token]; 679 | 680 | if (literal !== undefined) { 681 | current_block.Append(literal); 682 | continue; 683 | } 684 | 685 | var new_block, func; 686 | 687 | var section_match = token.match(_SECTION_RE); 688 | if (section_match) { 689 | var repeated = section_match[1]; 690 | var section_name = section_match[3]; 691 | 692 | if (repeated) { 693 | func = _DoRepeatedSection; 694 | new_block = _RepeatedSection({section_name: section_name}); 695 | } else { 696 | func = _DoSection; 697 | new_block = _Section({section_name: section_name}); 698 | } 699 | current_block.Append([func, new_block]); 700 | stack.push(new_block); 701 | current_block = new_block; 702 | continue; 703 | } 704 | 705 | var pred_str, pred; 706 | 707 | // Check {.or pred?} before {.pred?} 708 | var or_match = token.match(_OR_RE); 709 | if (or_match) { 710 | pred_str = or_match[1]; 711 | pred = pred_str ? GetPredicate(pred_str, false) : null; 712 | current_block.NewOrClause(pred); 713 | continue; 714 | } 715 | 716 | // {.if predicate} or {.attr?} 717 | // this is where we need to fix for scope for {.if condition} 718 | // @see http://jsont.squarespace.com/advanced-jsont/ - @kitajchuk 719 | var if_token = false, pred_token = false; 720 | var if_match = token.match(_IF_RE); 721 | if (if_match) { 722 | pred_str = token; 723 | if_token = true; 724 | // also check token split in case of arguments - @kitajchuk 725 | } else { 726 | //get delimiter for equal if not a space 727 | var delimiter = token.split('?')[1] ? token.split('?')[1][0] : ' '; 728 | var token_split = token.split(delimiter).shift(); // split token from arguments - @kitajchuk 729 | if (token.charAt(token.length-1) == '?' || token_split.charAt(token_split.length-1) == '?') { 730 | pred_str = token; 731 | pred_token = true; 732 | } 733 | } 734 | if (if_token || pred_token) { 735 | // test_attr=true if we got the {.attr?} style (pred_token) 736 | pred = pred_str ? GetPredicate(pred_str, pred_token) : null; 737 | new_block = _PredicateSection(); 738 | new_block.NewOrClause(pred); 739 | current_block.Append([_DoPredicates, new_block]); 740 | stack.push(new_block); 741 | 742 | current_block = new_block; 743 | continue; 744 | } 745 | 746 | if (token == 'alternates with') { 747 | current_block.AlternatesWith(); 748 | continue; 749 | } 750 | 751 | if (token == 'end') { 752 | // End the block 753 | stack.pop(); 754 | if (stack.length > 0) { 755 | current_block = stack[stack.length-1]; 756 | } else { 757 | throw { 758 | name: 'TemplateSyntaxError', 759 | message: 'Got too many {end} statements' 760 | }; 761 | } 762 | continue; 763 | } 764 | } 765 | 766 | // A variable substitution 767 | var parts = token.split(format_char); 768 | var formatters; 769 | var name; 770 | if (parts.length == 1) { 771 | if (default_formatter === null) { 772 | throw { 773 | name: 'MissingFormatter', 774 | message: 'This template requires explicit formatters.' 775 | }; 776 | } 777 | // If no formatter is specified, use the default. 778 | formatters = [GetFormatter(default_formatter)]; 779 | name = token; 780 | } else { 781 | formatters = []; 782 | for (var j=1; j