apostrophe
Version:
The Apostrophe Content Management System.
218 lines (200 loc) • 7.09 kB
JavaScript
// Provides the `build` method, a flexible and powerful way to build
// URLs with query parameters and more. This method is made available
// as the `build` filter in Nunjucks. This is also the logical place
// to add new utility methods relating to URLs.
var _ = require('@sailshq/lodash');
var qs = require('qs');
module.exports = {
alias: 'urls',
construct: function(self, options) {
// Build filter URLs. `data` is an object whose properties
// become new query parameters. These parameters override any
// existing parameters of the same name in the URL. If you
// pass a property with a value of `undefined`, `null` or an
// empty string, that parameter is removed from the
// URL if already present (note that the number `0` does not
// do this). This is very useful for maintaining filter
// parameters in a query string without redundant code.
//
// Pretty URLs
//
// If the optional `path` argument is present, it must be an
// array. (You may skip this argument if you are just
// adding query parameters.)
//
// Any properties of `data` whose names appear in `path`
// are concatenated to the URL directly, separated by slashes,
// in the order they appear in that array.
//
// The first missing or empty value for a property in `path`
// stops this process to prevent an ambiguous URL.
//
// Note that there is no automatic detection that this has
// already happened in an existing URL, so you can't override
// existing components of the path.
//
// If a property's value is not equal to the slugification of
// itself as determined by apos.utils.slugify, then a query
// parameter is set instead.
//
// If you don't want to handle a property as a query parameter,
// make sure it is always slug-safe.
//
// Overrides: multiple data objects
//
// You may pass additional data objects. The last one wins, so
// you can pass your existing parameters first and pass new
// parameters you are changing as a second data object.
//
// Working with Arrays
//
// Normally, a new value for a property replaces any old one,
// and `undefined`, `null` or `''` removes the old one. If you
// wish to build up an array property instead you'll need
// to use the MongoDB-style $addToSet and $pull operators to add and
// remove values from an array field in the URL:
//
// Add tags[]=blue to the query string, if not already present
//
// `{ tags: { $addToSet: 'blue' } }`
//
// Remove tags[]=blue from the query string, if present
//
// `{ tags: { $pull: 'blue' } }`
//
// All values passed to $addToSet or $pull must be strings or
// convertible to strings via `toString()` (e.g. numbers, booleans)
//
// (The actual query string syntax includes array indices and
// is fully URI escaped, so it's slightly different but has
// the same impact. PHP does the same thing.)
self.build = function(url, path, data) {
var hash;
// Preserve hash separately
var matches = url.match(/^(.*)?#(.*)$/);
if (matches) {
url = matches[1];
hash = matches[2];
if (url === undefined) {
// Why, JavaScript? Why? -Tom
url = '';
}
}
// Sometimes necessary with nunjucks, we may otherwise be
// exposed to a SafeString object and throw an exception
url = url.toString();
var qat = url.indexOf('?');
var base = url;
var dataObjects = [];
var pathKeys;
var original;
var query = {};
var i, j;
var key;
if (qat !== -1) {
original = qs.parse(url.substr(qat + 1));
base = url.substr(0, qat);
}
var dataStart = 1;
if (path && Array.isArray(path)) {
pathKeys = path;
dataStart = 2;
} else {
pathKeys = [];
}
// Process data objects in reverse order so the last
// override wins
for (i = arguments.length - 1; (i >= dataStart); i--) {
dataObjects.push(arguments[i]);
}
if (original) {
dataObjects.push(original);
}
var done = {};
var stop = false;
var dataObject;
var value;
for (i = 0; (i < pathKeys.length); i++) {
if (stop) {
break;
}
key = pathKeys[i];
for (j = 0; (j < dataObjects.length); j++) {
dataObject = dataObjects[j];
if (dataObject[key] !== undefined) {
value = dataObject[key];
// If we hit an empty value we need to stop all path processing to avoid
// ambiguous URLs
if ((value === undefined) || (value === null) || (value === '')) {
done[key] = true;
stop = true;
break;
}
// If the value is an object it can't be stored in the path,
// so stop path processing, but don't mark this key 'done'
// because we can still store it as a query parameter
if (typeof (value) === 'object') {
stop = true;
break;
}
var s = dataObject[key].toString();
if (s === self.apos.utils.slugify(s)) {
// Don't append double /
if (base !== '/') {
base += '/' + s;
} else {
base += s;
}
done[key] = true;
break;
} else {
// A value that cannot be slugified also forces an end to
// path processing
stop = true;
break;
}
}
}
}
// For non-path parameters we process starting with the original
// object so cumulative operations like $addToSet and $pull can work
for (i = dataObjects.length - 1; (i >= 0); i--) {
dataObject = dataObjects[i];
for (key in dataObject) {
if (done[key]) {
continue;
}
value = dataObject[key];
if (value && (value.$pull !== undefined)) {
value = _.difference(query[key] || [], [ value.$pull.toString() ]);
if (!value.length) {
value = undefined;
}
} else if (value && (value.$addToSet !== undefined)) {
value = _.union(query[key] || [], [ value.$addToSet.toString() ]);
if (!value.length) {
value = undefined;
}
}
if ((value === undefined) || (value === null) || (value === '')) {
delete query[key];
} else {
query[key] = value;
}
}
}
function restoreHash(url) {
if (hash !== undefined) {
return url + '#' + hash;
} else {
return url;
}
}
if (_.size(query)) {
return restoreHash(base + '?' + qs.stringify(query));
} else {
return restoreHash(base);
}
};
}
};