apostrophe
Version:
Apostrophe is a user-friendly content management system. You'll need more than this core module. See apostrophenow.org to get started.
937 lines (847 loc) • 34.2 kB
JavaScript
var path = require('path');
var extend = require('extend');
var _ = require('lodash');
var moment = require('moment');
var findTheFat = require('find-the-fat');
/**
* aposLocals
* @augments Augments the apos object with methods providing locals for Express
*/
module.exports = function(self) {
self._aposLocals = {
aposPrefix: self.prefix,
// aposStylesheets renders markup to load CSS that is needed on
// any page that will use Apostrophe. Examples: editor.less, content.less
// and stylesheet assets pushed by other modules. `when` can be set to
// either `user` or `anon` and signifies whether a user is logged in or not;
// when users are logged in editing-related stylesheets are sent, otherwise not.
// The `when` parameter is made available to your page templates, so typically you
// just write this in your base layout template in the head element:
//
// `{{ aposScripts(when) }}`
//
// If `minify` is set to `true` in `data/local.js` then minification and combination of
// files is automatically performed.
//
// See `base.html` in the sandbox project.
aposStylesheets: function(when) {
if (!self._endAssetsCalled) {
throw new Error('CODE CHANGE REQUIRED: you must call apos.endAssets once after the last call to apos.pushAsset. Add apos.endAssets to the end of the list in your initApos function.');
}
if (self.options.minify) {
return '<link href="' + self.prefix + '/apos-minified/' + when + '-' + self._generation + '.css" rel="stylesheet" />';
} else {
if (self._lessMasters && self._lessMasters[when]) {
return '<link href="' + self.prefix + self._lessMasters[when].web + '" rel="stylesheet" />';
}
return _.map(self.filterAssets(self._assets['stylesheets'], when), function(stylesheet) {
return '<link href="' + self.prefix + stylesheet.web + '" rel="stylesheet" />';
}).join("\n");
}
},
// aposScripts renders markup to load browser-side javascript that is needed on
// any page that will use Apostrophe. Examples: content.js, editor.js, various
// jquery plugins, jquery itself. `when` can be set to either `user` or `anon` and
// signifies whether a user is logged in or not; when users are logged in editing-related
// stylesheets are sent, otherwise not. The `when` parameter is made available to your
// page templates, so typically you just write this in your base layout template
// in the head element:
//
// `{{ aposScripts(when) }}`
//
// If `minify` is set to `true` in `data/local.js` then minification and combination of
// files is automatically performed.
//
// See `base.html` in the sandbox project.
aposScripts: function(when) {
if (!self._endAssetsCalled) {
throw new Error('CODE CHANGE REQUIRED: you must call apos.endAssets once after the last call to apos.pushAsset. Add apos.endAssets to the end of the list in your initApos function.');
}
if (!when) {
// Backwards compatibility with older layouts
when = 'all';
}
if (self.options.minify) {
var unminifiable = self.filterAssets(self._assets['scripts'], when, false);
var result = prefix() + scriptTags(unminifiable);
result += '<script src="' + self.prefix + '/apos-minified/' + when + '-' + self._generation + '.js"></script>\n' + setScene();
return result;
} else {
var minifiable = self.filterAssets(self._assets['scripts'], when);
return prefix() + scriptTags(minifiable) + setScene();
}
function scriptTags(scripts) {
return _.map(scripts, function(script) {
return '<script src="' + self.prefix + script.web + '"></script>';
}).join("\n");
}
// We don't want to use pushGlobalData for this in the usual way because
// we want to be certain of the timing
function setScene() {
return "<script>apos.scene = " + JSON.stringify(when) + ";</script>\n";
}
// apos.data.prefix must be available before any
// .js files load, notably beforeCkeditor.js
function prefix() {
return "<script>\n" +
"if (!window.apos) {\n" +
" window.apos = {};\n" +
"}\n" +
"window.apos.data = { prefix: " + JSON.stringify(self.prefix) + " };\n</script>\n";
}
},
// aposTemplates renders templates that are needed on any page that will
// use apos. Examples: slideshowEditor.html, codeEditor.html,
// etc. These lie dormant in the page until they are needed as prototypes to
// be cloned by jQuery. `when` can be set to either `user` or `anon` and signifies whether
// a user is logged in or not; when users are logged in editing-related stylesheets are sent,
// otherwise not.The `when` parameter is made available to your page templates, so typically
// you just write this in your base layout template at the end of the body element:
//
// `{{ aposTemplates(when) }}`
//
// See `base.html` in the sandbox project.
aposTemplates: function(when) {
if (!when) {
when = 'all';
}
var templates = self._assets['templates'];
templates = self.filterAssets(templates, when);
return _.map(templates, function(template) {
return self.renderTemplateAsset(template).trim();
}).join('');
},
/**
* Renders a content area. Three syntaxes are supported. We recommend
* the first one for most uses:
*
* `aposArea(page, 'body', { ... other options ... })`
*
* `aposArea({ page: page, name: 'body', ... other options ... })`
*
* `aposArea({ slug: page.slug + ':body', area: page.body, ... other options ... })`
*
* Other supported options include `controls` and an option named for each
* widget type, which is passed on as the options to that widget.
*
* Note that you do not have to specify the `edit` option if you use the first two syntaxes,
* but you may if you wish to specifically forbid editing for a user who normally could.
*/
aposArea: function(page, name, options) {
// Boil the newer syntax options down to the old first before
// we actually try to access anything in options. -Tom
if (arguments.length >= 2) {
if (!options) {
options = {};
}
options.page = arguments[0];
options.name = arguments[1];
} else {
options = arguments[0];
}
if (options.page && options.name) {
options.slug = options.page.slug + ':' + options.name;
options.area = options.page[options.name];
if (options.edit === undefined) {
options.edit = options.page._edit;
}
// So we don't choke the browser with JSON of the entire page in
// the area options
delete options.page;
}
// If the slug is not explicitly given, try to get it from
// the area. This works for areas that already exist, but
// cannot be relied upon for new areas obviously
if (options.area && (!options.slug)) {
options.slug = options.area.slug;
}
/* OK, now we can acccess options for other things. */
if (!options.controls) {
options.controls = self.defaultControls;
}
options.styles = options.styles || self.controlTypes.style.choices;
if (!options.initialContent) {
if (options.textOnly) {
options.initialContent = '<p data-initial-content>Click the pencil to get started.</p>';
} else {
options.initialContent = '<p data-initial-content>Use the Add Content button to get started.</p>';
}
}
if (!options.initialContent.match(/^<p data-initial-content/)) {
// We must be able to recognize and remove it. -Tom
options.initialContent = '<p data-initial-content>' + options.initialContent + '</p>';
}
var area = options.area;
delete options.area;
if (!area) {
// Invent the area if it doesn't exist yet, so we can
// edit pages not previously edited
area = { items: [] };
if (options.textOnly) {
// If the area is text only then it should initially contain an
// empty text widget as there is no way to add another and we want to
// be able to apply the apos-empty class immediately
// area.items.push({ type: 'richText', content: '' });
}
if (options.initialContent && options.edit) {
area.items.push({ type: 'richText', content: options.initialContent });
}
}
// Keep options and area separate, area is much too big to stuff into
// the options attribute of every area element, whoops
return self.partial('area', { options: options, area: area });
},
/**
* Renders a singleton (a standalone widget). Three syntaxes are supported. We recommend
* the first one for most uses:
*
* `aposSingleton(page, 'body', 'slideshow', { ... other options ... })`
*
* `aposSingleton({ page: page, name: 'body', type: 'slideshow', ... other options ... })`
*
* `aposSingleton({ slug: page.slug + ':body', area: page.body, type: 'slideshow', ... other options ... })`
*
* Other options are passed on to the widget.
*
* Note that you do not have to specify the `edit` option if you use the first two syntaxes,
* but you may if you wish to specifically forbid editing for a user who normally could.
*/
aposSingleton: function(page, name, type, options) {
// Transform the first syntax to the second
if (arguments.length >= 3) {
if (!options) {
options = {};
}
options.page = page;
options.name = name;
options.type = type;
} else {
options = arguments[0];
}
if (!self.itemTypes[options.type]) {
console.error("Attempt to insert a singleton of a nonexistent type: " + options.type);
return;
}
// Find area based on page and name
if (options.page && options.name) {
options.slug = options.page.slug + ':' + options.name;
options.area = options.page[options.name];
if (options.edit === undefined) {
options.edit = options.page._edit;
}
// So we don't choke the browser with JSON of the entire page in
// the area options
delete options.page;
}
// If the slug is not explicitly given, try to get it from
// the area. This works for areas that already exist, but
// cannot be relied upon for new areas obviously
if (options.area && (!options.slug)) {
options.slug = options.area.slug;
}
if (!options.area) {
// Invent the area if it doesn't exist yet, so we can
// edit pages not previously edited
options.area = { items: [] };
}
// If someone transforms an existing area into a singleton, do a reasonable thing by
// taking the first existing item of the proper type
var item = _.find(options.area.items, function(item) {
return item.type === options.type;
});
options.itemType = self.itemTypes[options.type];
options.item = item;
if (options.item) {
options.item.position = 'middle';
options.item.size = 'full';
}
// Options to pass on to the widget
options.options = _.omit(options, 'area', 'item', 'slug', 'type', 'page', 'name', 'edit');
// Prune this back so it's not circular
options.options.itemType = self.jsonItemType(options.itemType);
// Options to pass on to the DOM
return self.partial('singleton', options);
},
// Returns true if the specified area is empty. Expects a page
// object and an area name, or an options object containing an
// object with an `area` property.
aposAreaIsEmpty: function(page, name) {
var area;
if (arguments.length === 2) {
area = page[name];
} else {
// "page" is an options object
area = page.area;
}
if (!area) {
return true;
}
return !_.some(area.items, function(item) {
if (self.itemTypes[item.type] && self.itemTypes[item.type].empty) {
return !self.itemTypes[item.type].empty(item);
} else {
return true;
}
});
},
// Returns true if the specified area is empty when considered as a
// singleton of the specified type. Expects a page object, an area name
// and a widget type name. Alternatively you may pass a single object
// with an `area` property and a `type` property, in which case the
// `area` property is the actual area object.
aposSingletonIsEmpty: function(page, name, type) {
var options;
// Wrong syntax - warn the developer
if (arguments.length === 2) {
throw new Error("aposSingletonIsEmpty takes 3 arguments (page, name, type) ");
}
// Transform the first syntax to the second
else if (arguments.length >= 3) {
if (!options) {
options = {};
}
options.area = page[name];
options.type = type;
} else {
options = arguments[0];
}
if (!options.area) {
return true;
}
return !_.some(options.area.items , function(item) {
if (self.itemTypes[item.type] && (item.type === options.type) && self.itemTypes[item.type].empty) {
return !self.itemTypes[item.type].empty(item);
} else {
return true;
}
});
},
aposSlugify: function(string) {
return self.slugify(string);
},
// Outputs the normal views of all of the content items (widgets and/or
// rich text blocks) specified by the first argument, passing on the specified
// options. Typically invoked for you by `aposArea` or `aposSingleton`, but can
// be called directly with `area.items` when you do not wish to render the
// wrapper markup for the area as a whole (permissible only if editing is
// not a possibility here). The `allowed` option, if present, should be a list
// of permitted item types; any items not on that list are not rendered.
//
// If a widget has a lockup property the widget and the text item following it
// are wrapped in an apos-lockup div. If the item following the widget is not
// a text item the lockup is ignored.
//
// Options for individual widget types can be passed as follows:
// `{ slideshow: { ... options ... } }`
aposAreaContent: function(items, options) {
var result = '';
var allowed = options.allowed;
var lockupOpen = false;
var i;
var lockup;
var limit = items.length;
if (options.limit && (options.limit < items.length)) {
limit = options.limit;
}
for (i = 0; (i < limit); i++) {
var item = items[i];
if (allowed && (!_.contains(allowed, item.type))) {
continue;
}
var itemOptionsIn = options ? options[item.type] : {};
var itemOptions = {};
extend(true, itemOptions, itemOptionsIn);
if (options.editView) {
itemOptions.editView = true;
}
// LOCKUPS: if a widget has the lockup property, do some sanity checks,
// then output that widget and the rich text item that follows it inside
// a wrapper div with the apos-lockup class as well as a class derived
// from the name of the lockup
lockup = false;
if (item.lockup && self.lockups[item.lockup] && _.contains(self.lockups[item.lockup].widgets, item.type) && ((i + 1) < items.length) && (items[i + 1].type === 'richText')) {
lockup = self.lockups[item.lockup];
}
if (lockup) {
result += '<div class="apos-lockup ' + self.cssName(item.lockup) + '">';
// Lockups can contain custom options per widget type
extend(true, itemOptions, lockup[item.type] || {});
}
result += self._aposLocals.aposItemNormalView(item, itemOptions).trim();
if (lockupOpen) {
result += "</div>\n";
lockupOpen = false;
}
if (lockup) {
lockupOpen = true;
}
}
return result;
},
// Find an image referenced within an area, such as an image in a slideshow widget.
// Returns the first image matching the criteria. Only GIF, JPEG and PNG images
// will ever be returned.
//
// EASY SYNTAX:
//
// aposAreaImage(page, 'body')
//
// You may also add options, such as "extension" to force the results to
// include JPEGs only:
//
// aposAreaImage(page, 'body', { extension: 'jpg' })
//
// (Note Apostrophe always uses .jpg for JPEGs.)
//
// CLASSIC SYNTAX (this is the hard way):
//
// aposAreaImage({ area: page.body })
//
// OPTIONS:
//
// You may specify `extension` or `extensions` (an array of extensions)
// to filter the results.
aposAreaImage: function(options /* OR page, name, options */) {
var result = self.areaImage.apply(self, arguments);
return result;
},
// Find images referenced within an area, such as images in a slideshow widget.
// Returns all the files matching the criteria unless the "limit" option is used.
//
// EASY SYNTAX:
//
// aposAreaImages(page, 'body')
//
// Now you can loop over them with "for".
//
// You may also add options:
//
// aposAreaImages(page, 'body', { extension: 'jpg', limit: 2 })
//
// Note that Apostrophe always uses three-letter lowercase extensions.
//
// CLASSIC SYNTAX:
//
// aposAreaImage({ area: page.body })
//
// OPTIONS:
//
// You may specify `extension`, or `extensions` (an array of extensions).
//
// The `limit` option limits the number of results returned. Note that
// `areaImage` is more convenient than `aposAreaImages` if limit is 1.
//
// See also aposAreaFiles.
aposAreaImages: function(options /* or page, name, _options */) {
return self.areaImages.apply(self, arguments);
},
// Find a file referenced within an area, such as an image in a slideshow widget,
// or a PDF in a file widget.
//
// Returns the first file matching the criteria.
//
// EASY SYNTAX:
//
// aposAreaFile(page, 'body')
//
// You may also add options:
//
// aposAreaFile(page, 'body', { extension: 'jpg' })
//
// CLASSIC SYNTAX:
//
// aposAreaFile({ area: page.body })
//
// OPTIONS:
//
// You may specify `extension`, `extensions` (an array of extensions)
// or `group` to filter the results. By default the `images` and `office` groups
// are available.
//
// If you are using `group: "images"` consider calling aposAreaImage instead.
// This is convenient and protects you from accidentally getting a PDF file.
aposAreaFile: function(options /* or page, name, _options */) {
return self.areaFile.apply(self, arguments);
},
// Find files referenced within an area, such as images in a slideshow widget.
// Returns all the files matching the criteria unless the "limit" option is used.
//
// EASY SYNTAX:
//
// aposAreaFiles(page, 'body')
//
// Now you can loop over them with "for".
//
// You may also add options:
//
// aposAreaFiles(page, 'body', { extension: 'jpg', limit: 2 })
//
// CLASSIC SYNTAX:
//
// aposAreaFile({ area: page.body })
//
// OPTIONS:
//
// You may specify `extension`, `extensions` (an array of extensions)
// or `group` to filter the results. By default the `images` and `office` groups
// are available. See also `aposAreaImage` and `aposAreaImages` for
// convenience.
//
// The `limit` option limits the number of results returned. Note that
// `aposAreaFile` is more convenient than `aposAreaFiles` if you want
// just one file.
//
// If you are using `group: "images"` consider calling aposAreaImages instead.
// This is convenient and protects you from accidentally getting a PDF file.
aposAreaFiles: function(options /* or page, name, _options */) {
return self.areaFiles.apply(self, arguments);
},
// bc wrapper for aposAreaFile, see aposAreaFile
aposAreaFindFile: function(options /* or page, name, _options */) {
return self.areaFile.apply(self, arguments);
},
// Outputs markup for a button that accesses the media
// admin interface.
aposMediaMenu: function(permissions) {
if (self.permissions.can({
user: {
permissions: permissions
}
}, 'edit-file')) {
return self.partial('mediaMenu', permissions);
}
},
// Outputs markup for a button that access the tag
// admin interface. TODO: in 0.6 switch to a permissions
// object as the only argument.
aposTagsMenu: function(options) {
return self.partial('tagsMenu', options);
},
// Renders the normal, public view of a widget or rich text item. Normally invoked
// by aposAreaContent, which is usually invoked by aposArea or aposSingleton.
// Typically you will not need to call this yourself.
aposItemNormalView: function(item, options) {
if (!options) {
options = {};
}
if (!self.itemTypes[item.type]) {
console.error("Unknown item type: " + item.type);
// Empty string so concatenation does not crash the server
return '';
}
var itemType = self.itemTypes[item.type];
options.widget = itemType.widget;
if (options.bodyOnly) {
options.widget = false;
}
// The content property doesn't belong in a data attribute,
// and neither does any property beginning with an _, unless
// whitelisted
var attributes = {};
if (itemType.json) {
// Type has a custom JSON filter
attributes = itemType.json(item);
} else {
// use forOwn rather than each() to avoid being faked out
// by any object that happens to have a "length" property -Tom and Joel
_.forOwn(item, function(value, key) {
if (_.contains(itemType.jsonProperties, key)) {
// Whitelisted
} else {
// By default, all properties that do not start with _ and
// are not the "content" property are made available on
// the element as JSON attributes
if ((typeof(key) === 'string') && ((key === 'content') || (key.substr(0, 1) === '_'))) {
return;
}
}
attributes[key] = value;
});
}
// findTheFat(options, 'options: ' + item.type, 50000);
// findTheFat(attributes, 'attributes: ' + item.type, 50000);
// All options must be made available on the browser side,
// otherwise it is not possible to see the widget properly
// after it is saved without a page refresh. Corollary:
// giant things like page objects should never be passed
// as options. Let the loader function get them
var empty = itemType.empty ? itemType.empty(item) : false;
var response = self.partial('itemNormalView', { item: item, itemType: itemType, empty: empty, options: options, attributes: attributes });
return response;
},
// Given a file object (as found in a slideshow widget for instance),
// return the file URL. If options.size is set, return the URL for
// that size (one-third, one-half, two-thirds, full). full is
// "full width" (1140px), not the original. For the original, don't pass size
// There is a matching client-side implementation accessible as apos.filePath
aposFilePath: function(file, options) {
return self.filePath(file, options);
},
// Convenience method for the file path of a slideshow's first image.
aposAreaImagePath: function(page, name, options) {
options = options || {};
return self._aposLocals.aposFilePath( self.areaImage.apply(self, arguments), options);
},
// Log a message to the console from a Nunjucks template. Great for debugging.
aposLog: function(m) {
console.log(m);
return '';
},
// Generate a globally unique ID
aposGenerateId: function() {
return self.generateId();
},
// Generate the right range of page numbers to display in the pager.
// Just a little too much math to be comfortable in pure Nunjucks
aposPageRange: function(options) {
var pages = [];
var fromPage = options.page - 2;
if (fromPage < 2) {
fromPage = 2;
}
for (var page = fromPage; (page < (fromPage + options.shown)); page++) {
pages.push(page);
}
return pages;
},
// For an array of versions, return either the last 7 days's or this months versions.
aposVersionsDateRange: function(versions, range){
var stop;
if (range === 'week'){
stop = moment().subtract(7, 'days');
} else {
stop = moment().subtract(1, 'month').endOf('month');
}
return _.filter(versions, function(version){
return moment(version.createdAt).isAfter(stop);
});
},
// Test whether the specified date object refers to a date in the current year.
// The events module utilizes this
aposIsCurrentYear: function(date) {
var now = new Date();
return date.getYear() === now.getYear();
},
// Returns true if the list contains the specified value.
// If value is an array, returns true if the list contains
// *any of* the specified values
aposContains: function(list, value) {
if (_.isArray(value)) {
for (var i = 0; i < value.length; i++) {
var valueItem = value[i];
if (_.contains(list, valueItem)){
return true;
}
}
return false;
}
else{
return _.contains(list, value);
}
},
// Returns true if the list contains at least one
// object with the named property.
// The first parameter may also be a single object, in
// which case this function returns true if that object
// has the named property.
aposContainsProperty: function(list, property) {
if (_.isArray(list)) {
return _.some(list, function(item){ return _.has(item, property); });
} else {
return _.has(list, property);
}
},
// Reverses the order of the array. This MODIFIES the array
// in addition to returning it
aposReverse: function(array){
return array.reverse();
},
// If the `list` argument is a string, returns true if it begins
// with `value`. If the `list` argument is an array, returns
// true if at least one of its elements begins with `value`.
aposBeginsWith: function(list, value) {
if (_.isArray(list)) {
for (var i = 0; i < list.length; i++) {
var listItem = list[i];
if (listItem.indexOf(value) === 0) {
return true;
}
}
} else {
if (list.indexOf(value) === 0) {
return true;
}
}
return false;
},
// Pass as many objects as you want; they will get merged via
// `extend` into a new object, without modifying any of them, and
// the resulting object will be returned. If several objects have
// a property, the last object wins.
//
// This is useful to add one more option to an options object
// which was passed to you.
//
// If any argument is null, it is skipped gracefully. This allows
// you to pass in an options object without checking if it is null.
aposMerge: function() {
var result = {};
var i;
for (i = 0; (i < arguments.length); i++) {
if (!arguments[i]) {
continue;
}
extend(true, result, arguments[i]);
}
return result;
},
// Find the first array element, if any, that has the specified value for
// the specified property.
aposFind: function(arr, property, value) {
return _.find(arr, function(item) {
return (item[property] === value);
});
},
// Find all the array elements, if any, that have the specified value for
// the specified property.
aposFilter: function(arr, property, value) {
return _.filter(arr, function(item) {
return (item[property] === value);
});
},
// Find all the array elements, if any, for which the specified property
// is truthy.
aposFilterNonempty: function(arr, property) {
return _.filter(arr, function(item) {
return (item[property]);
});
},
// Given an array of objects with the given property, return an array with
// the value of that property for each object.
aposPluck: function(arr, property) {
return _.pluck(arr, property);
},
// Concatenate all of the given arrays and/or values
// into a single array. If an argument is an array, all
// of its elements are individually added to the
// resulting array. If an argument is a value, it is
// added directly to the array.
aposConcat: function(arrOrObj1, arrOrObj2 /* , ... */) {
// I tried to implement this with call() but I kept
// getting arrays in my values. We still get the
// benefit of concat() and its built-in support for
// treating arrays and scalar values differently. -Tom
var result = [];
var i;
for (i = 0; (i < arguments.length); i++) {
result = result.concat(arguments[i]);
}
return result;
},
// Convert an area to plaintext. This will only contain text
// for items that clearly have an appropriate plaintext
// representation for the public, so most widgets will not want
// to be represented as they have no reasonable plaintext
// equivalent, but you can define the 'getPlaintext' method
// for any widget to return one (see self.itemTypes for the
// richText example).
// Plaintext means truly plain, so if you want to output the
// text with nunjucks, be sure to use the "e" filter.
// If the truncate option is present, it is used as a character
// limit. The plaintext is cut at the closest word boundary
// before that length. If this cannot be done a hard cutoff is
// applied so that the result is never longer than
// options.truncate characters.
// You may call with the page, areaName, options syntax:
// {{ aposAreaPlaintext(page, 'body', { truncate: 200 }) }}
// Or a single options object:
// {{ aposAreaPlaintext({ area: page.body, truncate: 200 }) }}
aposAreaPlaintext: function(page, name, options) {
if (!options) {
options = {};
}
if (arguments.length === 1) {
options = page;
} else {
options.area = page[name];
}
return self.getAreaPlaintext(options);
},
// Groups by the property named by 'key' on each of the values.
// If the property referred to by the string 'key' is found to be
// an array property of the first object, aposGroupByArray is called.
//
// Usage: {{ aposGroupBy(people, 'age') }} or {{ aposGroupBy(items, 'tags') }}
aposGroupBy: function(items, key){
if(items.length && Array.isArray(items[0][key])) {
return self._groupByArray(items, key);
}
return _.groupBy(items, key);
},
// Returns true if the permissions specified indicate that the
// user can edit *something*, it doesn't matter what. This includes
// being able to edit your profile. Useful to
// decide whether to show the admin bar.
aposCanEditSomething: function(permissions) {
return _.some(permissions, function(val, key) {
return key.match(/^(guest|edit|admin)/);
});
},
// Given a series of alternating keys and values, this
// function returns an object with those values for
// those keys. For instance, aposObject('name', 'bob')
// returns { name: 'bob' }. This is useful because
// Nunjucks does not allow you to create an object with
// a property whose name is unknown at the time the
// template is written.
aposObject: function(/* key, value, ... */) {
var o = {};
var i = 0;
while (i < arguments.length) {
o[arguments[i]] = arguments[i + 1];
i += 2;
}
return o;
}
};
// Add more locals for Apostrophe later. Used by extension modules
// like apostrophe-pages. Can be used to push flags as well as functions.
self.addLocal = function(name, fn) {
var add = fn;
if (typeof(fn) === 'function') {
// Nunjucks discards information about the true location of an error that
// happens in a function invoked inside {{ }}. Make this information visible
// again with our own wrapper function.
var _fn = function() {
try {
return fn.apply(this, arguments);
} catch (e) {
console.error('Error occurred in a nunjucks local (an apos* function or one of your own locals)');
if (e.stack) {
console.error(e.stack);
} else {
console.error('Error has no stack trace, when throwing exceptions always use new Error("message")');
}
console.error('^^^^^ HEY LOOK UP HERE FIRST\n');
throw e;
}
};
add = _fn;
}
self._aposLocals[name] = add;
self.app.locals[name] = add;
};
// An extra bit of trickery to support arrays in self._locals.groupBy
self._groupByArray =function(items, arrayName) {
var results = {};
_.each(items, function(item) {
_.each(item[arrayName] || [], function(inner) {
if (!results[inner]) {
results[inner] = [];
}
results[inner].push(item);
});
});
return results;
};
};