<commit_before>var path = require('path'),
    fs = require('fs'),
    External = require('./external'),
    Step = require('step'),
    _ = require('underscore')._,
    sys = require('sys'),
    mess = require('mess'),
    tree = require('mess/tree');

require.paths.unshift(path.join(__dirname, '..', 'lib'));

/**
 * Rendering circuitry for JSON map manifests.
 *
 * This is node-only for the time being.
 */

mess.Renderer = function Renderer(env) {
    env = _.extend(env, {});
    if (!env.data_dir) env.data_dir = '/tmp/';
    if (!env.local_data_dir) env.local_data_dir = '';
    if (!env.validation_data) env.validation_data = false;

    return {
        /**
         * Keep a copy of passed-in environment variables
         */
        env: env,

        /**
         * Ensure that map layers have a populated SRS value and attempt to
         * autodetect SRS if missing. Requires that node-srs is available and
         * that any remote datasources have been localized.
         *
         * @param {Object} m map object.
         * @param {Function} callback
         */
        ensureSRS: function(m, callback) {
            Step(
                function autodetectSRS() {
                    var group = this.group();
                    var autodetect = _.filter(m.Layer, function(l) { return !l.srs; });
                    _.each(autodetect, function(l) {
                        var next = group();
                        Step(
                            function() {
                                fs.readdir(path.dirname(l.Datasource.file), this);
                            },
                            function(err, files) {
                                var prj = _.detect(files, function(f) {
                                    return path.extname(f).toLowerCase() == '.prj';
                                });
                                if (prj) {
                                    prj = path.join(path.dirname(l.Datasource.file), prj);
                                    fs.readFile(prj, 'utf-8', this);
                                } else {
                                    this(new Error('No projection found'));
                                }
                            },
                            function(err, srs) {
                                if (!err) {
                                    try {
                                        l.srs = require('srs').parse(srs).proj4;
                                    } catch (err) {}
                                }
                                next(err);
                            }
                        );
                    });
                    // If no layers missing SRS information, next.
                    autodetect.length === 0 && group()();
                },
                function waitForDetection(err) {
                    m.Layer = _.filter(m.Layer, function(l) { return l.srs; });
                    callback(err, m);
                }
            );
        },

        /**
         * Download any file-based remote datsources.
         *
         * Usable as an entry point: does not expect any modification to
         * the map object beyond JSON parsing.
         *
         * @param {Object} m map object.
         * @param {Function} callback
         */
        localizeExternals: function(m, callback) {
            var that = this;
            Step(
                function downloadExternals() {
                    var group = this.group();
                    m.Layer.forEach(function(l) {
                        if (l.Datasource.file) {
                            var next = group();
                            new External(that.env, l.Datasource.file)
                                .on('complete', function(external) {
                                    external.findByExtension('.shp', function(err, files) {
                                        if (err) {
                                            next(err);
                                        }
                                        else if (files.length) {
                                            l.Datasource.file = files[0];
                                            next(null);
                                        }
                                        else {
                                            next("No .shp file found in zipfile.");
                                        }
                                    });
                                });
                        } else {
                            throw 'Layer does not have a datasource';
                        }
                    });
                    // Continue even if we don't have any layers.
                    group()();
                },
                function waitForDownloads(err) {
                    callback(err, m);
                }
            );
        },

        /**
         * Download any remote stylesheets
         *
         * @param {Object} m map object.
         * @param {Function} callback
         */
        localizeStyle: function(m, callback) {
            var that = this;
            Step(
                function downloadStyles() {
                    var group = this.group();
                    m.Stylesheet.forEach(function(s, k) {
                        if (!s.id) {
                            var next = group();
                            new External(that.env, s)
                                .on('complete', function(external) {
                                    m.Stylesheet[k] = external.path();
                                    next();
                                });
                        }
                    });
                    group()();
                },
                function waitForDownloads(err, results) {
                    callback(err, m);
                }
            );
        },

        /**
         * Compile (already downloaded) styles with mess.js,
         * calling callback with an array of [map object, [stylesheet objects]]
         *
         * Called with the results of localizeStyle or localizeExternals:
         * expects not to handle downloading.
         *
         * @param {Object} m map object.
         * @param {Function} callback
         */
        style: function(m, callback) {
            var that = this;
            Step(
                function loadStyles() {
                    var group = this.group();
                    m.Stylesheet.forEach(function(s) {
                        var next = group();
                        if (s.id) {
                            next(null, [s.id, s.data]);
                        } else {
                            fs.readFile(s, 'utf-8', function(err, data) {
                                next(err, [s, data]);
                            });
                        }
                    });
                },
                function compileStyles(e, results) {
                    var options = {},
                        group = this.group();
                    for (var i = 0, l = results.length; i < l; i++) {
                        var next = group();
                        new mess.Parser(_.extend(_.extend({
                            filename: results[i][0]
                        }, that.env), this.env)).parse(results[i][1],
                            function(err, tree) {
                            if (err) {
                                mess.writeError(err, options);
                                throw err;
                            } else {
                                try {
                                    next(err, [
                                        results[i][0],
                                        tree]);
                                    return;
                                } catch (e) {
                                    throw e;
                                    return;
                                }
                            }
                        });
                    }
                },
                function waitForCompilation(err, res) {
                    callback(err, m, res);
                }
            );
        },

        /**
         * Split definitions into sub-lists of definitions
         * containing rules pertaining to only one
         * symbolizer each
         */
        splitSymbolizers: function(definitions) {
            var bySymbolizer = {};
            for (var i = 0; i < definitions.length; i++) {
                definitions[i].symbolizers().forEach(function(sym) {
                    var index = sym + '/' + definitions[i].selector.attachment;
                    if(!bySymbolizer[index]) {
                        bySymbolizer[index] = [];
                    }
                    bySymbolizer[index].push(
                        definitions[i].filterSymbolizer(sym));
                });
            }
            return bySymbolizer;
        },

        /**
         * Pick the 'winners' - all elements that select
         * properly. Apply inherited styles from their
         * ancestors to them.
         */
        processChain: function(definitions) {
            // definitions are ordered in specificity,
            // high to low
            //
            // basically if 'this level' has
            // a filter, then keep going, otherwise
            // this is the final selector.
            var winners = [];
            var ancestors = [];
            var belowThreshold = false;
            var def;

            while (def = definitions.shift()) {
                if (belowThreshold) {
                    ancestors.push(def);
                } else if (def.selector.specificity()[2] > 0) {
                    winners.push(def);
                } else {
                    winners.push(def);
                    // nothing below this level will win
                    belowThreshold = true;
                }
            }

            for (var i = 0; i < winners.length; i++) {
                for (var j = i + 1; j < winners.length; j++) {
                    winners[i].inheritFrom(winners[j]);
                }
                for (var j = 0; j < ancestors.length; j++) {
                    winners[i].inheritFrom(ancestors[j]);
                }
            }

            return this.resolveConditions(winners);
        },

        resolveConditions: function(definitions) {
            var rules = [];
            var negatedFilters = [];
            var negatedZoom = tree.Zoom.newFromRange([0, Infinity]);

            for (var i = 0; i < definitions.length; i++) {
                var selector = definitions[i].selector;

                // Only add the negated current zoom when there actually are zoom filters.
                if (selector.zoom) {
                    var zoom = selector.zoom.clone();
                    var negation = zoom.negate();
                    zoom.intersection(negatedZoom);
                } else {
                    var zoom = negatedZoom.clone();
                }

                var zoomRanges = zoom.getRanges();
                if (!zoomRanges.length) {
                    continue;
                } else if (negation) {
                    // This zoom range has ranged. Add it so that future rules
                    // will exclude the current zoom.
                    negatedZoom.intersection(negation);
                }

                // Resolve regular filters.
                var filters = selector.filters.filter(function(f) {
                    return f instanceof tree.Filter;
                });

                var negation = filters.map(function(f) { return f.negate(); });

                // add in existing negations
                filters.push.apply(filters, negatedFilters);

                // Throw out contradicting rules.
                if (!tree.Filter.sound(filters)) {
                    continue;
                } else {
                    // add this definition's filter's negations to the list
                    negatedFilters.push.apply(negatedFilters, negation);
                }

                selector.filters = filters;
                selector.zoom = zoom;

                // Add a separate rule for each zoom range.
                for (var j = 0; j < zoomRanges.length; j++) {
                    var rule = definitions[i].clone();
                    rule.selector.zoom = tree.Zoom.newFromRange(zoomRanges[j]);
                    rules.push(rule);
                }
            }
            return rules;
        },

        /**
         * Find a rule like Map { background-color: #fff; },
         * if any, and return a list of properties to be inserted
         * into the <Map element of the resulting XML.
         *
         * @param {Array} rulesets the output of toList.
         * @param {Object} env.
         * @return {String} rendered properties.
         */
        getMapProperties: function(rulesets, env) {
            var properties = [];
            rulesets.filter(function(r) {
                return r.selector.layers() === 'Map';
            }).forEach(function(r) {
                for (var i = 0; i < r.rules.length; i++) {
                    if (r.rules[i].eval) r.rules[i] = r.rules[i].eval(env);
                    properties.push(r.rules[i].toXML(env));
                }
            });
            return properties.join('');
        },

        /**
         * Prepare full XML map output. Called with the results
         * of this.style
         *
         * @param {Array} res array of [map object, stylesheets].
         * @param {Function} callback
         */
        template: function(err, m, stylesheets, callback) {
            if (err) {
                callback(err);
                return;
            }

            // frames is a container for variables and other less.js
            // constructs.
            //
            // effects is a container for side-effects, which currently
            // are limited to FontSets.
            var that = this,
                env = _.extend({
                    frames: [],
                    effects: [],
                    deferred_externals: []
                }, that.env),
                output = [];

            var rulesets = _.flatten(stylesheets.map(function(rulesets) {
                return rulesets[1].toList(env);
            }));

            m.Layer.forEach(function(l) {
                l.styles = [];
                var classes = (l['class'] || '').split(/\s+/g);

                var matching = rulesets.filter(function(ruleset) {
                    return ruleset.selector.matches(l.id, classes);
                });

                // matching is an array of matching selectors,
                // in order from high specificity to low.
                var bySymbolizer = that.splitSymbolizers(matching);

                for (var sym in bySymbolizer) {
                    // Create styles out of chains of one-symbolizer rules,
                    // and assign those styles to layers
                    var new_style = new mess.tree.Style(
                        l.id,
                        sym,
                        that.processChain(bySymbolizer[sym]));
                    l.styles.push(new_style.name());
                    // env.effects can be modified by this call
                    output.push(new_style.toXML(env));
                }

                var nl = new mess.tree.Layer(l);
                output.push(nl.toXML());
            });

            output.unshift(env.effects.map(function(e) {
                return e.toXML(env);
            }).join('\n'));

            Step(
                function() {
                    var group = this.group();
                    if (env.deferred_externals.length) {
                        env.deferred_externals.forEach(function(def) {
                            var next = group();
                            new External(that.env, def)
                                .on('complete', function(external) {
                                    next();
                                });
                        });
                    } else {
                        var next = group();
                        next()();
                    }
                },
                function(err, results) {
                    output.unshift(
                        '<?xml version="1.0" '
                        + 'encoding="utf-8"?>\n'
                        + '<!DOCTYPE Map[]>\n'
                        + '<Map '
                        + that.getMapProperties(rulesets, env)
                        + ' srs="+proj=merc +a=6378137 +b=6378137 '
                        + '+lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m '
                        + '+nadgrids=@null +no_defs">\n');

                    output.push('</Map>');
                    callback(null, output.join('\n'));
                }
            );
        },

        /**
         * Prepare a JML document (given as a string) into a
         * fully-localized XML file ready for Mapnik2 consumption
         *
         * @param {String} str the JSON file as a string.
         * @param {Function} callback to be called with err, XML representation.
         */
        render: function(str, callback) {
            var m = JSON.parse(str),
                that = this;
            this.localizeExternals(m, function(err, res) {
                that.ensureSRS(res, function(err, res) {
                    that.localizeStyle(res, function(err, res) {
                        that.style(res, function(err, m, res) {
                            that.template(err, m, res, function(err, res) {
                                callback(err, res);
                            });
                        });
                    });
                });
            });
        }
    };
};

module.exports = mess;
<commit_msg>Documenting new arg<commit_after>var path = require('path'),
    fs = require('fs'),
    External = require('./external'),
    Step = require('step'),
    _ = require('underscore')._,
    sys = require('sys'),
    mess = require('mess'),
    tree = require('mess/tree');

require.paths.unshift(path.join(__dirname, '..', 'lib'));

/**
 * Rendering circuitry for JSON map manifests.
 *
 * This is node-only for the time being.
 */

mess.Renderer = function Renderer(env) {
    env = _.extend(env, {});
    if (!env.data_dir) env.data_dir = '/tmp/';
    if (!env.local_data_dir) env.local_data_dir = '';
    if (!env.validation_data) env.validation_data = false;

    return {
        /**
         * Keep a copy of passed-in environment variables
         */
        env: env,

        /**
         * Ensure that map layers have a populated SRS value and attempt to
         * autodetect SRS if missing. Requires that node-srs is available and
         * that any remote datasources have been localized.
         *
         * @param {Object} m map object.
         * @param {Function} callback
         */
        ensureSRS: function(m, callback) {
            Step(
                function autodetectSRS() {
                    var group = this.group();
                    var autodetect = _.filter(m.Layer, function(l) { return !l.srs; });
                    _.each(autodetect, function(l) {
                        var next = group();
                        Step(
                            function() {
                                fs.readdir(path.dirname(l.Datasource.file), this);
                            },
                            function(err, files) {
                                var prj = _.detect(files, function(f) {
                                    return path.extname(f).toLowerCase() == '.prj';
                                });
                                if (prj) {
                                    prj = path.join(path.dirname(l.Datasource.file), prj);
                                    fs.readFile(prj, 'utf-8', this);
                                } else {
                                    this(new Error('No projection found'));
                                }
                            },
                            function(err, srs) {
                                if (!err) {
                                    try {
                                        l.srs = require('srs').parse(srs).proj4;
                                    } catch (err) {}
                                }
                                next(err);
                            }
                        );
                    });
                    // If no layers missing SRS information, next.
                    autodetect.length === 0 && group()();
                },
                function waitForDetection(err) {
                    m.Layer = _.filter(m.Layer, function(l) { return l.srs; });
                    callback(err, m);
                }
            );
        },

        /**
         * Download any file-based remote datsources.
         *
         * Usable as an entry point: does not expect any modification to
         * the map object beyond JSON parsing.
         *
         * @param {Object} m map object.
         * @param {Function} callback
         */
        localizeExternals: function(m, callback) {
            var that = this;
            Step(
                function downloadExternals() {
                    var group = this.group();
                    m.Layer.forEach(function(l) {
                        if (l.Datasource.file) {
                            var next = group();
                            new External(that.env, l.Datasource.file)
                                .on('complete', function(external) {
                                    external.findByExtension('.shp', function(err, files) {
                                        if (err) {
                                            next(err);
                                        }
                                        else if (files.length) {
                                            l.Datasource.file = files[0];
                                            next(null);
                                        }
                                        else {
                                            next("No .shp file found in zipfile.");
                                        }
                                    });
                                });
                        } else {
                            throw 'Layer does not have a datasource';
                        }
                    });
                    // Continue even if we don't have any layers.
                    group()();
                },
                function waitForDownloads(err) {
                    callback(err, m);
                }
            );
        },

        /**
         * Download any remote stylesheets
         *
         * @param {Object} m map object.
         * @param {Function} callback
         */
        localizeStyle: function(m, callback) {
            var that = this;
            Step(
                function downloadStyles() {
                    var group = this.group();
                    m.Stylesheet.forEach(function(s, k) {
                        if (!s.id) {
                            var next = group();
                            new External(that.env, s)
                                .on('complete', function(external) {
                                    m.Stylesheet[k] = external.path();
                                    next();
                                });
                        }
                    });
                    group()();
                },
                function waitForDownloads(err, results) {
                    callback(err, m);
                }
            );
        },

        /**
         * Compile (already downloaded) styles with mess.js,
         * calling callback with an array of [map object, [stylesheet objects]]
         *
         * Called with the results of localizeStyle or localizeExternals:
         * expects not to handle downloading.
         *
         * @param {Object} m map object.
         * @param {Function} callback
         */
        style: function(m, callback) {
            var that = this;
            Step(
                function loadStyles() {
                    var group = this.group();
                    m.Stylesheet.forEach(function(s) {
                        var next = group();
                        if (s.id) {
                            next(null, [s.id, s.data]);
                        } else {
                            fs.readFile(s, 'utf-8', function(err, data) {
                                next(err, [s, data]);
                            });
                        }
                    });
                },
                function compileStyles(e, results) {
                    var options = {},
                        group = this.group();
                    for (var i = 0, l = results.length; i < l; i++) {
                        var next = group();
                        new mess.Parser(_.extend(_.extend({
                            filename: results[i][0]
                        }, that.env), this.env)).parse(results[i][1],
                            function(err, tree) {
                            if (err) {
                                mess.writeError(err, options);
                                throw err;
                            } else {
                                try {
                                    next(err, [
                                        results[i][0],
                                        tree]);
                                    return;
                                } catch (e) {
                                    throw e;
                                    return;
                                }
                            }
                        });
                    }
                },
                function waitForCompilation(err, res) {
                    callback(err, m, res);
                }
            );
        },

        /**
         * Split definitions into sub-lists of definitions
         * containing rules pertaining to only one
         * symbolizer each
         */
        splitSymbolizers: function(definitions) {
            var bySymbolizer = {};
            for (var i = 0; i < definitions.length; i++) {
                definitions[i].symbolizers().forEach(function(sym) {
                    var index = sym + '/' + definitions[i].selector.attachment;
                    if(!bySymbolizer[index]) {
                        bySymbolizer[index] = [];
                    }
                    bySymbolizer[index].push(
                        definitions[i].filterSymbolizer(sym));
                });
            }
            return bySymbolizer;
        },

        /**
         * Pick the 'winners' - all elements that select
         * properly. Apply inherited styles from their
         * ancestors to them.
         */
        processChain: function(definitions) {
            // definitions are ordered in specificity,
            // high to low
            //
            // basically if 'this level' has
            // a filter, then keep going, otherwise
            // this is the final selector.
            var winners = [];
            var ancestors = [];
            var belowThreshold = false;
            var def;

            while (def = definitions.shift()) {
                if (belowThreshold) {
                    ancestors.push(def);
                } else if (def.selector.specificity()[2] > 0) {
                    winners.push(def);
                } else {
                    winners.push(def);
                    // nothing below this level will win
                    belowThreshold = true;
                }
            }

            for (var i = 0; i < winners.length; i++) {
                for (var j = i + 1; j < winners.length; j++) {
                    winners[i].inheritFrom(winners[j]);
                }
                for (var j = 0; j < ancestors.length; j++) {
                    winners[i].inheritFrom(ancestors[j]);
                }
            }

            return this.resolveConditions(winners);
        },

        resolveConditions: function(definitions) {
            var rules = [];
            var negatedFilters = [];
            var negatedZoom = tree.Zoom.newFromRange([0, Infinity]);

            for (var i = 0; i < definitions.length; i++) {
                var selector = definitions[i].selector;

                // Only add the negated current zoom when there actually are zoom filters.
                if (selector.zoom) {
                    var zoom = selector.zoom.clone();
                    var negation = zoom.negate();
                    zoom.intersection(negatedZoom);
                } else {
                    var zoom = negatedZoom.clone();
                }

                var zoomRanges = zoom.getRanges();
                if (!zoomRanges.length) {
                    continue;
                } else if (negation) {
                    // This zoom range has ranged. Add it so that future rules
                    // will exclude the current zoom.
                    negatedZoom.intersection(negation);
                }

                // Resolve regular filters.
                var filters = selector.filters.filter(function(f) {
                    return f instanceof tree.Filter;
                });

                var negation = filters.map(function(f) { return f.negate(); });

                // add in existing negations
                filters.push.apply(filters, negatedFilters);

                // Throw out contradicting rules.
                if (!tree.Filter.sound(filters)) {
                    continue;
                } else {
                    // add this definition's filter's negations to the list
                    negatedFilters.push.apply(negatedFilters, negation);
                }

                selector.filters = filters;
                selector.zoom = zoom;

                // Add a separate rule for each zoom range.
                for (var j = 0; j < zoomRanges.length; j++) {
                    var rule = definitions[i].clone();
                    rule.selector.zoom = tree.Zoom.newFromRange(zoomRanges[j]);
                    rules.push(rule);
                }
            }
            return rules;
        },

        /**
         * Find a rule like Map { background-color: #fff; },
         * if any, and return a list of properties to be inserted
         * into the <Map element of the resulting XML.
         *
         * @param {Array} rulesets the output of toList.
         * @param {Object} env.
         * @return {String} rendered properties.
         */
        getMapProperties: function(rulesets, env) {
            var properties = [];
            rulesets.filter(function(r) {
                return r.selector.layers() === 'Map';
            }).forEach(function(r) {
                for (var i = 0; i < r.rules.length; i++) {
                    if (r.rules[i].eval) r.rules[i] = r.rules[i].eval(env);
                    properties.push(r.rules[i].toXML(env));
                }
            });
            return properties.join('');
        },

        /**
         * Prepare full XML map output. Called with the results
         * of this.style
         *
         * @param {Array} res array of [map object, stylesheets].
         * @param {Function} callback
         */
        template: function(err, m, stylesheets, callback) {
            if (err) {
                callback(err);
                return;
            }

            // frames is a container for variables and other less.js
            // constructs.
            //
            // effects is a container for side-effects, which currently
            // are limited to FontSets.
            //
            // deferred_externals is a list of externals, like
            // pattern images, etc (by string URL)
            // referred to by url() constructs in styles
            var that = this,
                env = _.extend({
                    frames: [],
                    effects: [],
                    deferred_externals: []
                }, that.env),
                output = [];

            var rulesets = _.flatten(stylesheets.map(function(rulesets) {
                return rulesets[1].toList(env);
            }));

            m.Layer.forEach(function(l) {
                l.styles = [];
                var classes = (l['class'] || '').split(/\s+/g);

                var matching = rulesets.filter(function(ruleset) {
                    return ruleset.selector.matches(l.id, classes);
                });

                // matching is an array of matching selectors,
                // in order from high specificity to low.
                var bySymbolizer = that.splitSymbolizers(matching);

                for (var sym in bySymbolizer) {
                    // Create styles out of chains of one-symbolizer rules,
                    // and assign those styles to layers
                    var new_style = new mess.tree.Style(
                        l.id,
                        sym,
                        that.processChain(bySymbolizer[sym]));
                    l.styles.push(new_style.name());
                    // env.effects can be modified by this call
                    output.push(new_style.toXML(env));
                }

                var nl = new mess.tree.Layer(l);
                output.push(nl.toXML());
            });

            output.unshift(env.effects.map(function(e) {
                return e.toXML(env);
            }).join('\n'));

            Step(
                function() {
                    var group = this.group();
                    if (env.deferred_externals.length) {
                        env.deferred_externals.forEach(function(def) {
                            var next = group();
                            new External(that.env, def)
                                .on('complete', function(external) {
                                    next();
                                });
                        });
                    } else {
                        var next = group();
                        next()();
                    }
                },
                function(err, results) {
                    output.unshift(
                        '<?xml version="1.0" '
                        + 'encoding="utf-8"?>\n'
                        + '<!DOCTYPE Map[]>\n'
                        + '<Map '
                        + that.getMapProperties(rulesets, env)
                        + ' srs="+proj=merc +a=6378137 +b=6378137 '
                        + '+lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m '
                        + '+nadgrids=@null +no_defs">\n');

                    output.push('</Map>');
                    callback(null, output.join('\n'));
                }
            );
        },

        /**
         * Prepare a JML document (given as a string) into a
         * fully-localized XML file ready for Mapnik2 consumption
         *
         * @param {String} str the JSON file as a string.
         * @param {Function} callback to be called with err, XML representation.
         */
        render: function(str, callback) {
            var m = JSON.parse(str),
                that = this;
            this.localizeExternals(m, function(err, res) {
                that.ensureSRS(res, function(err, res) {
                    that.localizeStyle(res, function(err, res) {
                        that.style(res, function(err, m, res) {
                            that.template(err, m, res, function(err, res) {
                                callback(err, res);
                            });
                        });
                    });
                });
            });
        }
    };
};

module.exports = mess;
<|endoftext|>


<commit_before>var Compiler = require('./compiler');
var formatters = require('./formatters');
var Runtime = require('./runtime');

/** Utility getter/wrapper for pluralization functions from
 *  {@link http://github.com/eemeli/make-plural.js make-plural}
 *
 * @private
 */
function getPluralFunc(locale, noPluralKeyChecks) {
  var plurals = require('make-plural/umd/plurals');
  var pluralCategories = require('make-plural/umd/pluralCategories');
  for (var l = locale; l; l = l.replace(/[-_]?[^-_]*$/, '')) {
    var pf = plurals[l];
    if (pf) {
      var pc = noPluralKeyChecks ? { cardinal: [], ordinal: [] } : (pluralCategories[l] || {});
      var fn = function() { return pf.apply(this, arguments); };
      fn.toString = function() { return pf.toString(); };
      fn.cardinal = pc.cardinal;
      fn.ordinal = pc.ordinal;
      return fn;
    }
  }
  throw new Error('Localisation function not found for locale ' + JSON.stringify(locale));
}

/** Create a new message formatter
 *
 *  If `locale` is not set, calls to `compile()` will fetch the default locale
 *  each time. A string `locale` will create a single-locale MessageFormat
 *  instance, with pluralisation rules fetched from the Unicode CLDR using
 *  {@link http://github.com/eemeli/make-plural.js make-plural}.
 *
 *  Using an array of strings as `locale` will create a MessageFormat object
 *  with multi-language support, with pluralisation rules fetched as above. To
 *  select which to use, use the second parameter of `compile()`, or use message
 *  keys corresponding to your locales.
 *
 *  Using an object `locale` with all properties of type `function` allows for
 *  the use of custom/externally defined pluralisation rules.
 *
 * @class
 * @param {string|string[]|Object.<string,function>} [locale] - The locale(s) to use
 *
 * @classdesc MessageFormat-to-JavaScript compiler
 *
 * ```
 * import MessageFormat from 'messageformat'
 * ```
 */
function MessageFormat(locale) {
  this.pluralFuncs = {};
  if (locale) {
    if (typeof locale == 'string') {
      this.pluralFuncs[locale] = getPluralFunc(locale);
    } else if (Array.isArray(locale)) {
      locale.forEach(function(lc) { this.pluralFuncs[lc] = getPluralFunc(lc); }, this);
    } else if (typeof locale == 'object') {
      for (var lc in locale) if (locale.hasOwnProperty(lc)) {
        if (typeof locale[lc] != 'function') throw new Error('Expected function value for locale ' + JSON.stringify(lc));
        this.pluralFuncs[lc] = locale[lc];
      }
      this.hasCustomPluralFuncs = true;
    }
  }
  this.fmt = {};
  this.runtime = new Runtime(this);
}

/** The default locale
 *
 *  Read by `compile()` when no locale has been previously set
 *
 * @memberof MessageFormat
 * @default 'en'
 */
MessageFormat.defaultLocale = 'en';

/** Escape special characaters
 *
 *  Surround the characters `{` and `}` in the input string with 'quotes'.
 *  This will allow those characters to not be considered as MessageFormat
 *  control characters.
 *
 * @param {string} str - The input string
 * @param {boolean} [octothorpe=false] - Include `#` in the escaped characters
 * @returns {string} The escaped string
 */
MessageFormat.escape = function(str, octothorpe) {
  const esc = octothorpe ? '[#{}]' : '[{}]';
  return str.replace(new RegExp(esc, 'g'), "'$&'");
}

MessageFormat.formatters = formatters;

/** Add custom formatter functions to this MessageFormat instance
 *
 * The general syntax for calling a formatting function in MessageFormat is
 * `{var, fn[, arg]}`, where `var` is the variable that will be set by the
 * user code, `fn` determines the formatting function, and `arg` is an
 * optional string argument.
 *
 * In JavaScript, each formatting function is called with three parameters;
 * the variable value `v`, the current locale `lc`, and `arg` as a string, or
 * undefined if not set. `arg` will be trimmed of surrounding whitespace.
 * Formatting functions should not have side effects.
 *
 * @see Formatters
 *
 * @memberof MessageFormat
 * @param {Object.<string,function>} fmt - A map of formatting functions
 * @returns {MessageFormat} The MessageFormat instance, to allow for chaining
 *
 * @example
 * var mf = new MessageFormat('en-GB');
 * mf.addFormatters({
 *   upcase: function(v) { return v.toUpperCase(); },
 *   locale: function(v, lc) { return lc; },
 *   prop: function(v, lc, p) { return v[p] }
 * });
 *
 * mf.compile('This is {VAR, upcase}.')({ VAR: 'big' })
 * // 'This is BIG.'
 *
 * mf.compile('The current locale is {_, locale}.')({ _: '' })
 * // 'The current locale is en-GB.'
 *
 * mf.compile('Answer: {obj, prop, a}')({ obj: {q: 3, a: 42} })
 * // 'Answer: 42'
 */
MessageFormat.prototype.addFormatters = function(fmt) {
  for (var name in fmt) if (fmt.hasOwnProperty(name)) {
    this.fmt[name] = fmt[name];
  }
  return this;
};

/** Disable the validation of plural & selectordinal keys
 *
 *  Previous versions of messageformat.js allowed the use of plural &
 *  selectordinal statements with any keys; now we throw an error when a
 *  statement uses a non-numerical key that will never be matched as a
 *  pluralization category for the current locale.
 *
 *  Use this method to disable the validation and allow usage as previously.
 *  To re-enable, you'll need to create a new MessageFormat instance.
 *
 * @returns {MessageFormat} The MessageFormat instance, to allow for chaining
 *
 * @example
 * var mf = new MessageFormat('en');
 * var msg = '{X, plural, zero{no answers} one{an answer} other{# answers}}';
 *
 * mf.compile(msg);
 * // Error: Invalid key `zero` for argument `X`. Valid plural keys for this
 * //        locale are `one`, `other`, and explicit keys like `=0`.
 *
 * mf.disablePluralKeyChecks();
 * mf.compile(msg)({ X: 0 });
 * // '0 answers'
 */
MessageFormat.prototype.disablePluralKeyChecks = function() {
  this.noPluralKeyChecks = true;
  for (var lc in this.pluralFuncs) if (this.pluralFuncs.hasOwnProperty(lc)) {
    this.pluralFuncs[lc].cardinal = [];
    this.pluralFuncs[lc].ordinal = [];
  }
  return this;
};

/** Enable or disable the addition of Unicode control characters to all input
 *  to preserve the integrity of the output when mixing LTR and RTL text.
 *
 * @see http://cldr.unicode.org/development/development-process/design-proposals/bidi-handling-of-structured-text
 *
 * @memberof MessageFormat
 * @param {boolean} [enable=true]
 * @returns {MessageFormat} The MessageFormat instance, to allow for chaining
 *
 * @example
 * // upper case stands for RTL characters, output is shown as rendered
 * var mf = new MessageFormat('en');
 *
 * mf.compile('{0} >> {1} >> {2}')(['first', 'SECOND', 'THIRD']);
 * // 'first >> THIRD << SECOND'
 *
 * mf.setBiDiSupport(true);
 * mf.compile('{0} >> {1} >> {2}')(['first', 'SECOND', 'THIRD']);
 * // 'first >> SECOND >> THIRD'
 */
MessageFormat.prototype.setBiDiSupport = function(enable) {
    this.bidiSupport = !!enable || (typeof enable == 'undefined');
    return this;
};

/** According to the ICU MessageFormat spec, a `#` character directly inside a
 *  `plural` or `selectordinal` statement should be replaced by the number
 *  matching the surrounding statement. By default, messageformat.js will
 *  replace `#` signs with the value of the nearest surrounding `plural` or
 *  `selectordinal` statement.
 *
 *  Set this to true to follow the stricter ICU MessageFormat spec, and to
 *  throw a runtime error if `#` is used with non-numeric input.
 *
 * @memberof MessageFormat
 * @param {boolean} [enable=true]
 * @returns {MessageFormat} The MessageFormat instance, to allow for chaining
 *
 * @example
 * var mf = new MessageFormat('en');
 *
 * var cookieMsg = '#: {X, plural, =0{no cookies} one{a cookie} other{# cookies}}';
 * mf.compile(cookieMsg)({ X: 3 });
 * // '#: 3 cookies'
 *
 * var pastryMsg = '{X, plural, one{{P, select, cookie{a cookie} other{a pie}}} other{{P, select, cookie{# cookies} other{# pies}}}}';
 * mf.compile(pastryMsg)({ X: 3, P: 'pie' });
 * // '3 pies'
 *
 * mf.setStrictNumberSign(true);
 * mf.compile(pastryMsg)({ X: 3, P: 'pie' });
 * // '# pies'
 */
MessageFormat.prototype.setStrictNumberSign = function(enable) {
    this.strictNumberSign = !!enable || (typeof enable == 'undefined');
    this.runtime.setStrictNumber(this.strictNumberSign);
    return this;
};

/** Compile messages into storable functions
 *
 *  If `messages` is a single string including ICU MessageFormat declarations,
 *  the result of `compile()` is a function taking a single Object parameter
 *  `d` representing each of the input's defined variables.
 *
 *  If `messages` is a hierarchical structure of such strings, the output of
 *  `compile()` will match that structure, with each string replaced by its
 *  corresponding JavaScript function.
 *
 *  If the input `messages` -- and therefore the output -- of `compile()` is an
 *  object, the output object will have a `toString(global)` method that may be
 *  used to store or cache the compiled functions to disk, for later inclusion
 *  in any JS environment, without a local MessageFormat instance required. Its
 *  `global` parameters sets the name (if any) of the resulting global variable,
 *  with special handling for `export default`. If `global` does not contain a
 *  `.`, the output defaults to an UMD pattern.
 *
 *  If `locale` is not set, the first locale set in the object's constructor
 *  will be used by default; using a key at any depth of `messages` that is a
 *  declared locale will set its child elements to use that locale.
 *
 *  If `locale` is set, it is used for all messages. If the constructor
 *  declared any locales, `locale` needs to be one of them.
 *
 * @memberof MessageFormat
 * @param {string|Object} messages - The input message(s) to be compiled, in ICU MessageFormat
 * @param {string} [locale] - A locale to use for the messages
 * @returns {function|Object} The first match found for the given locale(s)
 *
 * @example
 * var mf = new MessageFormat('en');
 * var cf = mf.compile('A {TYPE} example.');
 *
 * cf({ TYPE: 'simple' })
 * // 'A simple example.'
 *
 * @example
 * var mf = new MessageFormat(['en', 'fi']);
 * var cf = mf.compile({
 *   en: { a: 'A {TYPE} example.',
 *         b: 'This is the {COUNT, selectordinal, one{#st} two{#nd} few{#rd} other{#th}} example.' },
 *   fi: { a: '{TYPE} esimerkki.',
 *         b: 'Tämä on {COUNT, selectordinal, other{#.}} esimerkki.' }
 * });
 *
 * cf.en.b({ COUNT: 2 })
 * // 'This is the 2nd example.'
 *
 * cf.fi.b({ COUNT: 2 })
 * // 'Tämä on 2. esimerkki.'
 *
 * @example
 * var fs = require('fs');
 * var mf = new MessageFormat('en');
 * var msgSet = {
 *   a: 'A {TYPE} example.',
 *   b: 'This has {COUNT, plural, one{one member} other{# members}}.',
 *   c: 'We have {P, number, percent} code coverage.'
 * };
 * var cfStr = mf.compile(msgSet).toString('module.exports');
 * fs.writeFileSync('messages.js', cfStr);
 * ...
 * var messages = require('./messages');
 *
 * messages.a({ TYPE: 'more complex' })
 * // 'A more complex example.'
 *
 * messages.b({ COUNT: 3 })
 * // 'This has 3 members.'
 */
MessageFormat.prototype.compile = function(messages, locale) {
  function _stringify(obj, level) {
    if (!level) level = 0;
    if (typeof obj != 'object') return obj;
    var o = [], indent = '';
    for (var i = 0; i < level; ++i) indent += '  ';
    for (var k in obj) o.push('\n' + indent + '  ' + Compiler.propname(k) + ': ' + _stringify(obj[k], level + 1));
    return '{' + o.join(',') + '\n' + indent + '}';
  }

  var pf;
  if (Object.keys(this.pluralFuncs).length == 0) {
    if (!locale) locale = MessageFormat.defaultLocale;
    pf = {};
    pf[locale] = getPluralFunc(locale, this.noPluralKeyChecks);
  } else if (locale) {
    pf = {};
    pf[locale] = this.pluralFuncs[locale];
    if (!pf[locale]) throw new Error('Locale ' + JSON.stringify(locale) + 'not found in ' + JSON.stringify(this.pluralFuncs) + '!');
  } else {
    pf = this.pluralFuncs;
    locale = Object.keys(pf)[0];
  }

  var compiler = new Compiler(this);
  var obj = compiler.compile(messages, locale, pf);

  if (typeof messages != 'object') {
    var fn = new Function(
        'number, plural, select, fmt', Compiler.funcname(locale),
        'return ' + obj);
    var rt = this.runtime;
    return fn(rt.number, rt.plural, rt.select, this.fmt, pf[locale]);
  }

  var rtStr = this.runtime.toString(pf, compiler) + '\n';
  var objStr = _stringify(obj);
  var result = new Function(rtStr + 'return ' + objStr)();
  if (result.hasOwnProperty('toString')) throw new Error('The top-level message key `toString` is reserved');

  result.toString = function(global) {
    switch (global || '') {
      case 'export default':
        return rtStr +  'export default ' + objStr;
      case '':
        return rtStr + 'return ' + objStr;
      default:
        if (global.indexOf('.') > -1) return rtStr + global + ' = ' + objStr;
        return rtStr + [
          '(function (root, G) {',
          '  if (typeof define === "function" && define.amd) { define(G); }',
          '  else if (typeof exports === "object") { module.exports = G; }',
          '  else { ' + Compiler.propname(global, 'root') + ' = G; }',
          '})(this, ' + objStr + ');'
        ].join('\n');
    }
  }
  return result;
}

module.exports = MessageFormat;
<commit_msg>lib/messageformat: Harmonise code examples<commit_after>var Compiler = require('./compiler');
var formatters = require('./formatters');
var Runtime = require('./runtime');

/** Utility getter/wrapper for pluralization functions from
 *  {@link http://github.com/eemeli/make-plural.js make-plural}
 *
 * @private
 */
function getPluralFunc(locale, noPluralKeyChecks) {
  var plurals = require('make-plural/umd/plurals');
  var pluralCategories = require('make-plural/umd/pluralCategories');
  for (var l = locale; l; l = l.replace(/[-_]?[^-_]*$/, '')) {
    var pf = plurals[l];
    if (pf) {
      var pc = noPluralKeyChecks ? { cardinal: [], ordinal: [] } : (pluralCategories[l] || {});
      var fn = function() { return pf.apply(this, arguments); };
      fn.toString = function() { return pf.toString(); };
      fn.cardinal = pc.cardinal;
      fn.ordinal = pc.ordinal;
      return fn;
    }
  }
  throw new Error('Localisation function not found for locale ' + JSON.stringify(locale));
}

/** Create a new message formatter
 *
 *  If `locale` is not set, calls to `compile()` will fetch the default locale
 *  each time. A string `locale` will create a single-locale MessageFormat
 *  instance, with pluralisation rules fetched from the Unicode CLDR using
 *  {@link http://github.com/eemeli/make-plural.js make-plural}.
 *
 *  Using an array of strings as `locale` will create a MessageFormat object
 *  with multi-language support, with pluralisation rules fetched as above. To
 *  select which to use, use the second parameter of `compile()`, or use message
 *  keys corresponding to your locales.
 *
 *  Using an object `locale` with all properties of type `function` allows for
 *  the use of custom/externally defined pluralisation rules.
 *
 * @class
 * @param {string|string[]|Object.<string,function>} [locale] - The locale(s) to use
 *
 * @classdesc MessageFormat-to-JavaScript compiler
 *
 * ```
 * import MessageFormat from 'messageformat'
 * ```
 */
function MessageFormat(locale) {
  this.pluralFuncs = {};
  if (locale) {
    if (typeof locale == 'string') {
      this.pluralFuncs[locale] = getPluralFunc(locale);
    } else if (Array.isArray(locale)) {
      locale.forEach(function(lc) { this.pluralFuncs[lc] = getPluralFunc(lc); }, this);
    } else if (typeof locale == 'object') {
      for (var lc in locale) if (locale.hasOwnProperty(lc)) {
        if (typeof locale[lc] != 'function') throw new Error('Expected function value for locale ' + JSON.stringify(lc));
        this.pluralFuncs[lc] = locale[lc];
      }
      this.hasCustomPluralFuncs = true;
    }
  }
  this.fmt = {};
  this.runtime = new Runtime(this);
}

/** The default locale
 *
 *  Read by `compile()` when no locale has been previously set
 *
 * @memberof MessageFormat
 * @default 'en'
 */
MessageFormat.defaultLocale = 'en';

/** Escape special characaters
 *
 *  Surround the characters `{` and `}` in the input string with 'quotes'.
 *  This will allow those characters to not be considered as MessageFormat
 *  control characters.
 *
 * @param {string} str - The input string
 * @param {boolean} [octothorpe=false] - Include `#` in the escaped characters
 * @returns {string} The escaped string
 */
MessageFormat.escape = function(str, octothorpe) {
  const esc = octothorpe ? '[#{}]' : '[{}]';
  return str.replace(new RegExp(esc, 'g'), "'$&'");
}

MessageFormat.formatters = formatters;

/**
 * Add custom formatter functions to this MessageFormat instance. See the
 * {@tutorial guide} for more details.
 *
 * The general syntax for calling a formatting function in MessageFormat is
 * `{var, fn[, arg]}`, where `var` is the variable that will be set by the
 * user code, `fn` determines the formatting function, and `arg` is an
 * optional string argument.
 *
 * In JavaScript, each formatting function is called with three parameters;
 * the variable value `v`, the current locale `lc`, and `arg` as a string, or
 * undefined if not set. `arg` will be trimmed of surrounding whitespace.
 * Formatting functions should not have side effects.
 *
 * @memberof MessageFormat
 * @param {Object.<string,function>} fmt - A map of formatting functions
 * @returns {MessageFormat} The MessageFormat instance, to allow for chaining
 *
 * @example
 * const mf = new MessageFormat('en-GB')
 * mf.addFormatters({
 *   upcase: function(v) { return v.toUpperCase() },
 *   locale: function(v, lc) { return lc },
 *   prop: function(v, lc, p) { return v[p] }
 * })
 * const messages = mf.compile({
 *   describe: 'This is {VAR, upcase}.',
 *   locale: 'The current locale is {_, locale}.',
 *   answer: 'Answer: {obj, prop, a}'
 * }
 *
 * messages.describe({ VAR: 'big' })        // 'This is BIG.'
 * messages.locale({})                      // 'The current locale is en-GB.'
 * messages.answer({ obj: {q: 3, a: 42} })  // 'Answer: 42'
 */
MessageFormat.prototype.addFormatters = function(fmt) {
  for (var name in fmt) if (fmt.hasOwnProperty(name)) {
    this.fmt[name] = fmt[name];
  }
  return this;
};

/** Disable the validation of plural & selectordinal keys
 *
 *  Previous versions of messageformat.js allowed the use of plural &
 *  selectordinal statements with any keys; now we throw an error when a
 *  statement uses a non-numerical key that will never be matched as a
 *  pluralization category for the current locale.
 *
 *  Use this method to disable the validation and allow usage as previously.
 *  To re-enable, you'll need to create a new MessageFormat instance.
 *
 * @returns {MessageFormat} The MessageFormat instance, to allow for chaining
 *
 * @example
 * const mf = new MessageFormat('en')
 * const msg = '{X, plural, zero{no answers} one{an answer} other{# answers}}'
 *
 * mf.compile(msg)
 * // Error: Invalid key `zero` for argument `X`. Valid plural keys for this
 * //        locale are `one`, `other`, and explicit keys like `=0`.
 *
 * mf.disablePluralKeyChecks()
 * mf.compile(msg)({ X: 0 })  // '0 answers'
 */
MessageFormat.prototype.disablePluralKeyChecks = function() {
  this.noPluralKeyChecks = true;
  for (var lc in this.pluralFuncs) if (this.pluralFuncs.hasOwnProperty(lc)) {
    this.pluralFuncs[lc].cardinal = [];
    this.pluralFuncs[lc].ordinal = [];
  }
  return this;
};

/** Enable or disable the addition of Unicode control characters to all input
 *  to preserve the integrity of the output when mixing LTR and RTL text.
 *
 * @see http://cldr.unicode.org/development/development-process/design-proposals/bidi-handling-of-structured-text
 *
 * @memberof MessageFormat
 * @param {boolean} [enable=true]
 * @returns {MessageFormat} The MessageFormat instance, to allow for chaining
 *
 * @example
 * // upper case stands for RTL characters, output is shown as rendered
 * const mf = new MessageFormat('en')
 *
 * mf.compile('{0} >> {1} >> {2}')(['first', 'SECOND', 'THIRD'])
 *   // 'first >> THIRD << SECOND'
 *
 * mf.setBiDiSupport(true)
 * mf.compile('{0} >> {1} >> {2}')(['first', 'SECOND', 'THIRD'])
 *   // 'first >> SECOND >> THIRD'
 */
MessageFormat.prototype.setBiDiSupport = function(enable) {
    this.bidiSupport = !!enable || (typeof enable == 'undefined');
    return this;
};

/** According to the ICU MessageFormat spec, a `#` character directly inside a
 *  `plural` or `selectordinal` statement should be replaced by the number
 *  matching the surrounding statement. By default, messageformat.js will
 *  replace `#` signs with the value of the nearest surrounding `plural` or
 *  `selectordinal` statement.
 *
 *  Set this to true to follow the stricter ICU MessageFormat spec, and to
 *  throw a runtime error if `#` is used with non-numeric input.
 *
 * @memberof MessageFormat
 * @param {boolean} [enable=true]
 * @returns {MessageFormat} The MessageFormat instance, to allow for chaining
 *
 * @example
 * const mf = new MessageFormat('en')
 * const src = {
 *   cookie: '#: {X, plural, =0{no cookies} one{a cookie} other{# cookies}}',
 *   pastry: `{X, plural,
 *     one {{P, select, cookie{a cookie} other{a pie}}}
 *     other {{P, select, cookie{# cookies} other{# pies}}}
 *   }`
 * }
 * let messages = mf.compile(src)
 *
 * messages.cookie({ X: 3 })            // '#: 3 cookies'
 * messages.pastry({ X: 3, P: 'pie' })  // '3 pies'
 *
 * mf.setStrictNumberSign(true)
 * messages = mf.compile(src)
 * messages.pastry({ X: 3, P: 'pie' })  // '# pies'
 */
MessageFormat.prototype.setStrictNumberSign = function(enable) {
    this.strictNumberSign = !!enable || (typeof enable == 'undefined');
    this.runtime.setStrictNumber(this.strictNumberSign);
    return this;
};

/** Compile messages into storable functions
 *
 *  If `messages` is a single string including ICU MessageFormat declarations,
 *  the result of `compile()` is a function taking a single Object parameter
 *  `d` representing each of the input's defined variables.
 *
 *  If `messages` is a hierarchical structure of such strings, the output of
 *  `compile()` will match that structure, with each string replaced by its
 *  corresponding JavaScript function.
 *
 *  If the input `messages` -- and therefore the output -- of `compile()` is an
 *  object, the output object will have a `toString(global)` method that may be
 *  used to store or cache the compiled functions to disk, for later inclusion
 *  in any JS environment, without a local MessageFormat instance required. Its
 *  `global` parameters sets the name (if any) of the resulting global variable,
 *  with special handling for `export default`. If `global` does not contain a
 *  `.`, the output defaults to an UMD pattern.
 *
 *  If `locale` is not set, the first locale set in the object's constructor
 *  will be used by default; using a key at any depth of `messages` that is a
 *  declared locale will set its child elements to use that locale.
 *
 *  If `locale` is set, it is used for all messages. If the constructor
 *  declared any locales, `locale` needs to be one of them.
 *
 * @memberof MessageFormat
 * @param {string|Object} messages - The input message(s) to be compiled, in ICU MessageFormat
 * @param {string} [locale] - A locale to use for the messages
 * @returns {function|Object} The first match found for the given locale(s)
 *
 * @example
 * const mf = new MessageFormat('en')
 * const msg = mf.compile('A {TYPE} example.')
 *
 * msg({ TYPE: 'simple' })  // 'A simple example.'
 *
 * @example
 * const mf = new MessageFormat(['en', 'fi'])
 * const messages = mf.compile({
 *   en: { a: 'A {TYPE} example.',
 *         b: 'This is the {COUNT, selectordinal, one{#st} two{#nd} few{#rd} other{#th}} example.' },
 *   fi: { a: '{TYPE} esimerkki.',
 *         b: 'Tämä on {COUNT, selectordinal, other{#.}} esimerkki.' }
 * })
 *
 * messages.en.b({ COUNT: 2 })  // 'This is the 2nd example.'
 * messages.fi.b({ COUNT: 2 })  // 'Tämä on 2. esimerkki.'
 *
 * @example
 * const fs = require('fs')
 * const mf = new MessageFormat('en')
 * const msgSet = {
 *   a: 'A {TYPE} example.',
 *   b: 'This has {COUNT, plural, one{one member} other{# members}}.',
 *   c: 'We have {P, number, percent} code coverage.'
 * }
 * const msgStr = mf.compile(msgSet).toString('module.exports')
 * fs.writeFileSync('messages.js', msgStr)
 *
 * ...
 *
 * const messages = require('./messages')
 *
 * messages.a({ TYPE: 'more complex' })  // 'A more complex example.'
 * messages.b({ COUNT: 3 })              // 'This has 3 members.'
 */
MessageFormat.prototype.compile = function(messages, locale) {
  function _stringify(obj, level) {
    if (!level) level = 0;
    if (typeof obj != 'object') return obj;
    var o = [], indent = '';
    for (var i = 0; i < level; ++i) indent += '  ';
    for (var k in obj) o.push('\n' + indent + '  ' + Compiler.propname(k) + ': ' + _stringify(obj[k], level + 1));
    return '{' + o.join(',') + '\n' + indent + '}';
  }

  var pf;
  if (Object.keys(this.pluralFuncs).length == 0) {
    if (!locale) locale = MessageFormat.defaultLocale;
    pf = {};
    pf[locale] = getPluralFunc(locale, this.noPluralKeyChecks);
  } else if (locale) {
    pf = {};
    pf[locale] = this.pluralFuncs[locale];
    if (!pf[locale]) throw new Error('Locale ' + JSON.stringify(locale) + 'not found in ' + JSON.stringify(this.pluralFuncs) + '!');
  } else {
    pf = this.pluralFuncs;
    locale = Object.keys(pf)[0];
  }

  var compiler = new Compiler(this);
  var obj = compiler.compile(messages, locale, pf);

  if (typeof messages != 'object') {
    var fn = new Function(
        'number, plural, select, fmt', Compiler.funcname(locale),
        'return ' + obj);
    var rt = this.runtime;
    return fn(rt.number, rt.plural, rt.select, this.fmt, pf[locale]);
  }

  var rtStr = this.runtime.toString(pf, compiler) + '\n';
  var objStr = _stringify(obj);
  var result = new Function(rtStr + 'return ' + objStr)();
  if (result.hasOwnProperty('toString')) throw new Error('The top-level message key `toString` is reserved');

  result.toString = function(global) {
    switch (global || '') {
      case 'export default':
        return rtStr +  'export default ' + objStr;
      case '':
        return rtStr + 'return ' + objStr;
      default:
        if (global.indexOf('.') > -1) return rtStr + global + ' = ' + objStr;
        return rtStr + [
          '(function (root, G) {',
          '  if (typeof define === "function" && define.amd) { define(G); }',
          '  else if (typeof exports === "object") { module.exports = G; }',
          '  else { ' + Compiler.propname(global, 'root') + ' = G; }',
          '})(this, ' + objStr + ');'
        ].join('\n');
    }
  }
  return result;
}

module.exports = MessageFormat;
<|endoftext|>


<commit_before>var inflect = require('i')();
var RSVP = require('rsvp');
var _ = require('lodash');

// constants
var MIME = {
  standard: ['application/vnd.api+json', 'application/json'],
  patch: ['application/json-patch+json']
};
var errorMessages = {
  400: 'Request was malformed.',
  403: 'Access forbidden.',
  404: 'Resource not found.',
  405: 'Method not permitted.',
  412: 'Request header "Content-Type" must be one of: ' + MIME.standard.join(', '),
  500: 'Oops, something went wrong.',
  501: 'Feature not implemented.'
};

/**
 * Setup routes for a resource, given a name and model.
 *
 * @param {String} name
 * @param {Object} model
 */
function route(name, model) {
  var _this = this;
  var router = this.router;
  var adapter = this.adapter;

  // options
  var options = this.options;

  // routes
  var collection = options.inflect ? inflect.pluralize(name) : name;
  var collectionRoute = [options.namespace, collection].join('/') + options.suffix;
  var individualRoute = [options.namespace, collection].join('/') + '/:id' + options.suffix;

  // response emitters
  var sendError = function (req, res, status, error) {
    if (!!error) console.trace(error);
    var object = {
      error: errorMessages[status],
      detail: error ? error.toString() : ''
    };
    var str = options.production ?
      JSON.stringify(object, null, null) :
      JSON.stringify(object, null, 2) + '\n';

    res.set('Content-Type', MIME.standard[1]);
    res.send(status, str);
  };
  var sendResponse = function (req, res, status, object) {
    if (status === 204) return res.send(status);

    object = object || {};
    object = appendLinks.call(_this, object);

    var str = options.production ?
      JSON.stringify(object, null, null) :
      JSON.stringify(object, null, 2) + '\n';

    res.set('Content-Type', !req.get('User-Agent') ? MIME.standard[0] : MIME.standard[1]);
    res.send(status, str);
  };
  var methodNotAllowed = function (req, res) {
    sendError(req, res, 405);
  };

  /*!
   * Do before transformation.
   *
   * @param {String} [model]
   * @param {Object} resource
   * @param {Object} request
   * @param {Object} response
   */
  var beforeTransform = function (model, resource, request, response) {
    if (arguments.length < 4) {
      response = request;
      request = resource;
      resource = model;
      model = name;
    }
    return new RSVP.Promise(function (resolve, reject) {
      if (!_this._before.hasOwnProperty(model)) return resolve(resource);
      var transform = _this._before[model].call(resource, request, response);
      if (!transform) return reject();
      resolve(transform);
    });
  };

  /*!
   * Do after transformation.
   *
   * @param {String} [model]
   * @param {Object} resource
   * @param {Object} request
   * @param {Object} response
   */
  var afterTransform = function (model, resource, request, response) {
    if (arguments.length < 4) {
      response = request;
      request = resource;
      resource = model;
      model = name;
    }
    return new RSVP.Promise(function (resolve, reject) {
      if (!_this._after.hasOwnProperty(model)) return resolve(resource);
      var transform = _this._after[model].call(resource, request, response);
      if (!transform) return reject();
      resolve(transform);
    });
  };

  var mimeCheck = function (contentType) {
    return MIME.standard.indexOf(contentType.split(';').shift().toLowerCase()) >= 0;
  };

  /*!
   * Handle creating a resource.
   */
  router.post(collectionRoute, function (req, res) {

    // header error handling
    if (!mimeCheck(req.get('content-type'))) {
      return sendError(req, res, 412);
    }

    var before = [];
    try {
      req.body[collection].forEach(function (resource) {
        before.push(beforeTransform(resource, req, res));
      });
    } catch(error) {
      return sendError(req, res, 400, error);
    }

    // do before transforms
    RSVP.all(before)

    // create the resources
    .then(function (resources) {
      return RSVP.all(resources.map(function (resource) {
        return adapter.create(model, resource);
      }));
    }, function (errors) {
      sendError(req, res, 403, errors);
    })

    // do after transforms
    .then(function (resources) {
      return RSVP.all(resources.map(function (resource) {
        return afterTransform(resource, req, res);
      }));
    }, function (error) {
      sendError(req, res, 500, error);
    })

    // send the response
    .then(function (resources) {
      if (!resources.length) {
        return sendResponse(req, res, 204);
      }
      var body = {};
      body[collection] = resources;

      var location = options.baseUrl + '/';
      location += !!options.namespace ? options.namespace + '/' : '';
      location += collection + '/' + resources.map(function (resource) {
        return resource.id;
      }).join(',');
      res.set('Location', location);

      sendResponse(req, res, 201, body);
    }, function (error) {
      sendError(req, res, 403, error);
    });
  });

  /*
   * Get a list of resources.
   */
  router.get(collectionRoute, function (req, res) {
    var ids = [];

    if (typeof req.query.ids === 'string') ids = req.query.ids.split(',');
    if (typeof req.query.ids === 'array') ids = req.query.ids;

    // get resources by IDs
    adapter.findMany(model, ids)

    // do after transforms
    .then(function (resources) {
      return RSVP.all(resources.map(function (resource) {
        return afterTransform(resource, req, res);
      }));
    }, function (error) {
      sendError(req, res, 500, error);
    })

    // send the response
    .then(function (resources) {
      var body = {};

      body[collection] = resources;
      sendResponse(req, res, 200, body);
    }, function (error) {
      sendError(req, res, 403, error);
    });

  });

  /*
   * Handle unsupported methods on a collection of resources.
   */
  router.put(collectionRoute, methodNotAllowed);
  router.patch(collectionRoute, methodNotAllowed);
  router.delete(collectionRoute, methodNotAllowed);

  /*
   * Get an individual resource, or many.
   */
  router.get(individualRoute, function (req, res) {
    var ids = req.params.id.split(',');

    // get resources by IDs
    adapter.findMany(model, ids)

    // do after transforms
    .then(function (resources) {
      if (resources.length) {
        return RSVP.all(resources.map(function (resource) {
          return afterTransform(resource, req, res);
        }));
      } else {
        sendError(req, res, 404);
      }
    }, function (error) {
      sendError(req, res, 500, error);
    })

    // send the response
    .then(function (resources) {
      var body = {};

      body[collection] = resources;
      sendResponse(req, res, 200, body);
    }, function (error) {
      sendError(req, res, 500, error);
    });
  });

  /*
   * Get the related resources of an individual resource.
   */
  router.get(individualRoute + '/:key' + options.suffix, function (req, res) {
    var id = req.params.id
    var key = req.params.key;

    // get a resource by ID
    adapter.find(model, id)

    // do after transform
    .then(function (resource) {
      return afterTransform(resource, req, res);
    }, function (error) {
      sendError(req, res, 404, error);
    })

    // change context to resource
    .then(function (resource) {
      var ids
      var relatedModel;

      try {
        ids = resource.links[key];
        ids = _.isArray(ids) ? ids : [ids];
        relatedModel = _this._schema[name][key];
        relatedModel = _.isArray(relatedModel) ? relatedModel[0] : relatedModel;
        relatedModel = _.isPlainObject(relatedModel) ? relatedModel.ref : relatedModel;
      } catch(error) {
        return sendError(req, res, 404, error);
      }

      // find related resources
      adapter.findMany(relatedModel, ids)

      // do after transforms
      .then(function (resources) {
        return RSVP.all(resources.map(function (resource) {
          return afterTransform(relatedModel, resource, req, res);
        }));
      }, function (error) {
        sendError(req, res, 500, error);
      })

      // send the response
      .then(function (resources) {
        var body = {};
        var relatedKey = options.inflect ? inflect.pluralize(relatedModel) : relatedModel;

        body[relatedKey] = resources;
        sendResponse(req, res, 200, body);
      }, function (error) {
        sendError(req, res, 403, error);
      });

    }, function (error) {
      sendError(req, res, 403, error);
    });
  });

  /*
   * Put a resource.
   */
  router.put(individualRoute, function (req, res) {
    var id = req.params.id;
    var update;

    // header error handling
    if (!mimeCheck(req.get('content-type'))) {
      return sendError(req, res, 412);
    }

    try {
      update = req.body[collection][0];
      if (!update) return sendError(req, res, 400);
    } catch(error) {
      return sendError(req, res, 400, error);
    }

    // try to find the resource by ID
    adapter.find(model, id)

    // resource found, let's update it
    .then(function () {

      // do before transform
      beforeTransform(update, req, res)

      // update the resource
      .then(function (update) {
        return adapter.update(model, id, update);
      }, function (error) {
        sendError(req, res, 403, error);
      })

      // do after transform
      .then(function (update) {
        return afterTransform(update, req, res);
      }, function (error) {
        sendError(req, res, 500, error);
      })

      // send the response
      .then(function (update) {
        var body = {};

        body[collection] = [update];
        sendResponse(req, res, 200, body);
      }, function (error) {
        sendError(req, res, 403, error);
      });

    },

    // resource not found, try to create it
    function () {

      // do before transform
      beforeTransform(update, req, res)

      // create the resource
      .then(function (resource) {
        return adapter.create(model, id, resource);
      }, function (error) {
        sendError(req, res, 403, error);
      })

      // do after transform
      .then(function (resource) {
        return afterTransform(resource, req, res);
      }, function (error) {
        sendError(req, res, 500, error);
      })

      // send the response
      .then(function (resource) {
        var body = {};

        body[collection] = [resource];
        sendResponse(req, res, 201, body);
      }, function (error) {
        sendError(req, res, 500, error);
      });

    });
  });

  /*
   * Delete a resource.
   */
  router.delete(individualRoute, function (req, res) {
    var id = req.params.id;

    // find the resource by ID
    adapter.find(model, id)

    // do before transform
    .then(function (resource) {
      return beforeTransform(resource, req, res);
    }, function (error) {
      sendError(req, res, 404, error);
    })

    // let's delete it
    .then(function () {
      adapter.delete(model, id).then(function () {
        sendResponse(req, res, 204);
      }, function (error) {
        sendError(req, res, 500, error);
      });
    },

    // resource not found
    function (error) {
      sendError(req, res, 404, error);
    });
  });

  /*
   * Patch a resource.
   */
  router.patch(individualRoute, function (req, res) {
    var id = req.params.id;
    var update = {};

    // header error handling
    if (!mimeCheck(req.get('content-type'))) {
      return sendError(req, res, 412);
    }

    try {
      // parse patch request, only 'replace' op is supported per the json-api spec
      req.body.forEach(function (operation) {
        // TODO: bulk PATCH request
        var field = operation.path.split('/').slice(3);
        var value = operation.value;
        var path = update;

        if (operation.op === 'replace') {
          field.forEach(function (key, index) {
            if (index + 1 === field.length) {
              path[key] = value;
            } else {
              path[key] = path[key] || {};
              path = path[key];
            }
          });
        }
      });
    } catch(error) {
      return sendError(req, res, 400, error);
    }

    // do before transform
    beforeTransform(update, req, res)

    // update the resource
    .then(function (update) {
      return adapter.update(model, id, update);
    }, function (error) {
      sendError(req, res, 403, error);
    })

    // do after transform
    .then(function (resource) {
      return afterTransform(resource, req, res);
    }, function (error) {
      sendError(req, res, 500, error);
    })

    // send the response
    .then(function (resource) {
      var body = {};

      body[collection] = [resource];
      sendResponse(req, res, 200, body);
    }, function (error) {
      sendError(req, res, 500, error);
    });
  });

  /*
   * POSTing a resource to a predetermined ID is not allowed,
   * since that is what PUT is for.
   */
  router.post(individualRoute, methodNotAllowed);

}

/*
 * Append a top level "links" object for hypermedia.
 *
 * @api private
 * @param {Object} body deserialized response body
 * @return {Object}
 */
function appendLinks (body) {
  var schemas = this._schema;
  var options = this.options;

  _.each(body, function (value, key) {
    if (key === 'meta') return;
    var schema = schemas[options.inflect ? inflect.singularize(key) : key];
    var associations = [];

    _.each(schema, function (value, key) {
      var type = _.isArray(value) ? value[0] : value;

      type = _.isPlainObject(type) ? type.ref : type;

      if (typeof type === 'string') {
        type = options.inflect ? inflect.pluralize(type) : type;
        associations.push({key: key, type: type});
      }
    });

    if (!associations.length) return;
    body.links = body.links || {};
    associations.forEach(function (association) {
      var name = [key, association.key].join('.');

      body.links[name] = {
        href: options.baseUrl + '/' +
          (!!options.namespace ? options.namespace + '/' : '') +
          association.type + '/{' + name + '}',
        type: association.type
      };
    });
  });
  return body;
}

/*
 * Expose the route method.
 */
module.exports = route;
<commit_msg>actually check environment string<commit_after>var inflect = require('i')();
var RSVP = require('rsvp');
var _ = require('lodash');

// constants
var MIME = {
  standard: ['application/vnd.api+json', 'application/json'],
  patch: ['application/json-patch+json']
};
var errorMessages = {
  400: 'Request was malformed.',
  403: 'Access forbidden.',
  404: 'Resource not found.',
  405: 'Method not permitted.',
  412: 'Request header "Content-Type" must be one of: ' + MIME.standard.join(', '),
  500: 'Oops, something went wrong.',
  501: 'Feature not implemented.'
};

/**
 * Setup routes for a resource, given a name and model.
 *
 * @param {String} name
 * @param {Object} model
 */
function route(name, model) {
  var _this = this;
  var router = this.router;
  var adapter = this.adapter;

  // options
  var options = this.options;

  // routes
  var collection = options.inflect ? inflect.pluralize(name) : name;
  var collectionRoute = [options.namespace, collection].join('/') + options.suffix;
  var individualRoute = [options.namespace, collection].join('/') + '/:id' + options.suffix;

  // response emitters
  var sendError = function (req, res, status, error) {
    if (!!error) console.trace(error);
    var object = {
      error: errorMessages[status],
      detail: error ? error.toString() : ''
    };
    var str = options.environment === 'production' ?
      JSON.stringify(object, null, null) :
      JSON.stringify(object, null, 2) + '\n';

    res.set('Content-Type', MIME.standard[1]);
    res.send(status, str);
  };
  var sendResponse = function (req, res, status, object) {
    if (status === 204) return res.send(status);

    object = object || {};
    object = appendLinks.call(_this, object);

    var str = options.environment === 'production' ?
      JSON.stringify(object, null, null) :
      JSON.stringify(object, null, 2) + '\n';

    res.set('Content-Type', !req.get('User-Agent') ? MIME.standard[0] : MIME.standard[1]);
    res.send(status, str);
  };
  var methodNotAllowed = function (req, res) {
    sendError(req, res, 405);
  };

  /*!
   * Do before transformation.
   *
   * @param {String} [model]
   * @param {Object} resource
   * @param {Object} request
   * @param {Object} response
   */
  var beforeTransform = function (model, resource, request, response) {
    if (arguments.length < 4) {
      response = request;
      request = resource;
      resource = model;
      model = name;
    }
    return new RSVP.Promise(function (resolve, reject) {
      if (!_this._before.hasOwnProperty(model)) return resolve(resource);
      var transform = _this._before[model].call(resource, request, response);
      if (!transform) return reject();
      resolve(transform);
    });
  };

  /*!
   * Do after transformation.
   *
   * @param {String} [model]
   * @param {Object} resource
   * @param {Object} request
   * @param {Object} response
   */
  var afterTransform = function (model, resource, request, response) {
    if (arguments.length < 4) {
      response = request;
      request = resource;
      resource = model;
      model = name;
    }
    return new RSVP.Promise(function (resolve, reject) {
      if (!_this._after.hasOwnProperty(model)) return resolve(resource);
      var transform = _this._after[model].call(resource, request, response);
      if (!transform) return reject();
      resolve(transform);
    });
  };

  var mimeCheck = function (contentType) {
    return MIME.standard.indexOf(contentType.split(';').shift().toLowerCase()) >= 0;
  };

  /*!
   * Handle creating a resource.
   */
  router.post(collectionRoute, function (req, res) {

    // header error handling
    if (!mimeCheck(req.get('content-type'))) {
      return sendError(req, res, 412);
    }

    var before = [];
    try {
      req.body[collection].forEach(function (resource) {
        before.push(beforeTransform(resource, req, res));
      });
    } catch(error) {
      return sendError(req, res, 400, error);
    }

    // do before transforms
    RSVP.all(before)

    // create the resources
    .then(function (resources) {
      return RSVP.all(resources.map(function (resource) {
        return adapter.create(model, resource);
      }));
    }, function (errors) {
      sendError(req, res, 403, errors);
    })

    // do after transforms
    .then(function (resources) {
      return RSVP.all(resources.map(function (resource) {
        return afterTransform(resource, req, res);
      }));
    }, function (error) {
      sendError(req, res, 500, error);
    })

    // send the response
    .then(function (resources) {
      if (!resources.length) {
        return sendResponse(req, res, 204);
      }
      var body = {};
      body[collection] = resources;

      var location = options.baseUrl + '/';
      location += !!options.namespace ? options.namespace + '/' : '';
      location += collection + '/' + resources.map(function (resource) {
        return resource.id;
      }).join(',');
      res.set('Location', location);

      sendResponse(req, res, 201, body);
    }, function (error) {
      sendError(req, res, 403, error);
    });
  });

  /*
   * Get a list of resources.
   */
  router.get(collectionRoute, function (req, res) {
    var ids = [];

    if (typeof req.query.ids === 'string') ids = req.query.ids.split(',');
    if (typeof req.query.ids === 'array') ids = req.query.ids;

    // get resources by IDs
    adapter.findMany(model, ids)

    // do after transforms
    .then(function (resources) {
      return RSVP.all(resources.map(function (resource) {
        return afterTransform(resource, req, res);
      }));
    }, function (error) {
      sendError(req, res, 500, error);
    })

    // send the response
    .then(function (resources) {
      var body = {};

      body[collection] = resources;
      sendResponse(req, res, 200, body);
    }, function (error) {
      sendError(req, res, 403, error);
    });

  });

  /*
   * Handle unsupported methods on a collection of resources.
   */
  router.put(collectionRoute, methodNotAllowed);
  router.patch(collectionRoute, methodNotAllowed);
  router.delete(collectionRoute, methodNotAllowed);

  /*
   * Get an individual resource, or many.
   */
  router.get(individualRoute, function (req, res) {
    var ids = req.params.id.split(',');

    // get resources by IDs
    adapter.findMany(model, ids)

    // do after transforms
    .then(function (resources) {
      if (resources.length) {
        return RSVP.all(resources.map(function (resource) {
          return afterTransform(resource, req, res);
        }));
      } else {
        sendError(req, res, 404);
      }
    }, function (error) {
      sendError(req, res, 500, error);
    })

    // send the response
    .then(function (resources) {
      var body = {};

      body[collection] = resources;
      sendResponse(req, res, 200, body);
    }, function (error) {
      sendError(req, res, 500, error);
    });
  });

  /*
   * Get the related resources of an individual resource.
   */
  router.get(individualRoute + '/:key' + options.suffix, function (req, res) {
    var id = req.params.id
    var key = req.params.key;

    // get a resource by ID
    adapter.find(model, id)

    // do after transform
    .then(function (resource) {
      return afterTransform(resource, req, res);
    }, function (error) {
      sendError(req, res, 404, error);
    })

    // change context to resource
    .then(function (resource) {
      var ids
      var relatedModel;

      try {
        ids = resource.links[key];
        ids = _.isArray(ids) ? ids : [ids];
        relatedModel = _this._schema[name][key];
        relatedModel = _.isArray(relatedModel) ? relatedModel[0] : relatedModel;
        relatedModel = _.isPlainObject(relatedModel) ? relatedModel.ref : relatedModel;
      } catch(error) {
        return sendError(req, res, 404, error);
      }

      // find related resources
      adapter.findMany(relatedModel, ids)

      // do after transforms
      .then(function (resources) {
        return RSVP.all(resources.map(function (resource) {
          return afterTransform(relatedModel, resource, req, res);
        }));
      }, function (error) {
        sendError(req, res, 500, error);
      })

      // send the response
      .then(function (resources) {
        var body = {};
        var relatedKey = options.inflect ? inflect.pluralize(relatedModel) : relatedModel;

        body[relatedKey] = resources;
        sendResponse(req, res, 200, body);
      }, function (error) {
        sendError(req, res, 403, error);
      });

    }, function (error) {
      sendError(req, res, 403, error);
    });
  });

  /*
   * Put a resource.
   */
  router.put(individualRoute, function (req, res) {
    var id = req.params.id;
    var update;

    // header error handling
    if (!mimeCheck(req.get('content-type'))) {
      return sendError(req, res, 412);
    }

    try {
      update = req.body[collection][0];
      if (!update) return sendError(req, res, 400);
    } catch(error) {
      return sendError(req, res, 400, error);
    }

    // try to find the resource by ID
    adapter.find(model, id)

    // resource found, let's update it
    .then(function () {

      // do before transform
      beforeTransform(update, req, res)

      // update the resource
      .then(function (update) {
        return adapter.update(model, id, update);
      }, function (error) {
        sendError(req, res, 403, error);
      })

      // do after transform
      .then(function (update) {
        return afterTransform(update, req, res);
      }, function (error) {
        sendError(req, res, 500, error);
      })

      // send the response
      .then(function (update) {
        var body = {};

        body[collection] = [update];
        sendResponse(req, res, 200, body);
      }, function (error) {
        sendError(req, res, 403, error);
      });

    },

    // resource not found, try to create it
    function () {

      // do before transform
      beforeTransform(update, req, res)

      // create the resource
      .then(function (resource) {
        return adapter.create(model, id, resource);
      }, function (error) {
        sendError(req, res, 403, error);
      })

      // do after transform
      .then(function (resource) {
        return afterTransform(resource, req, res);
      }, function (error) {
        sendError(req, res, 500, error);
      })

      // send the response
      .then(function (resource) {
        var body = {};

        body[collection] = [resource];
        sendResponse(req, res, 201, body);
      }, function (error) {
        sendError(req, res, 500, error);
      });

    });
  });

  /*
   * Delete a resource.
   */
  router.delete(individualRoute, function (req, res) {
    var id = req.params.id;

    // find the resource by ID
    adapter.find(model, id)

    // do before transform
    .then(function (resource) {
      return beforeTransform(resource, req, res);
    }, function (error) {
      sendError(req, res, 404, error);
    })

    // let's delete it
    .then(function () {
      adapter.delete(model, id).then(function () {
        sendResponse(req, res, 204);
      }, function (error) {
        sendError(req, res, 500, error);
      });
    },

    // resource not found
    function (error) {
      sendError(req, res, 404, error);
    });
  });

  /*
   * Patch a resource.
   */
  router.patch(individualRoute, function (req, res) {
    var id = req.params.id;
    var update = {};

    // header error handling
    if (!mimeCheck(req.get('content-type'))) {
      return sendError(req, res, 412);
    }

    try {
      // parse patch request, only 'replace' op is supported per the json-api spec
      req.body.forEach(function (operation) {
        // TODO: bulk PATCH request
        var field = operation.path.split('/').slice(3);
        var value = operation.value;
        var path = update;

        if (operation.op === 'replace') {
          field.forEach(function (key, index) {
            if (index + 1 === field.length) {
              path[key] = value;
            } else {
              path[key] = path[key] || {};
              path = path[key];
            }
          });
        }
      });
    } catch(error) {
      return sendError(req, res, 400, error);
    }

    // do before transform
    beforeTransform(update, req, res)

    // update the resource
    .then(function (update) {
      return adapter.update(model, id, update);
    }, function (error) {
      sendError(req, res, 403, error);
    })

    // do after transform
    .then(function (resource) {
      return afterTransform(resource, req, res);
    }, function (error) {
      sendError(req, res, 500, error);
    })

    // send the response
    .then(function (resource) {
      var body = {};

      body[collection] = [resource];
      sendResponse(req, res, 200, body);
    }, function (error) {
      sendError(req, res, 500, error);
    });
  });

  /*
   * POSTing a resource to a predetermined ID is not allowed,
   * since that is what PUT is for.
   */
  router.post(individualRoute, methodNotAllowed);

}

/*
 * Append a top level "links" object for hypermedia.
 *
 * @api private
 * @param {Object} body deserialized response body
 * @return {Object}
 */
function appendLinks (body) {
  var schemas = this._schema;
  var options = this.options;

  _.each(body, function (value, key) {
    if (key === 'meta') return;
    var schema = schemas[options.inflect ? inflect.singularize(key) : key];
    var associations = [];

    _.each(schema, function (value, key) {
      var type = _.isArray(value) ? value[0] : value;

      type = _.isPlainObject(type) ? type.ref : type;

      if (typeof type === 'string') {
        type = options.inflect ? inflect.pluralize(type) : type;
        associations.push({key: key, type: type});
      }
    });

    if (!associations.length) return;
    body.links = body.links || {};
    associations.forEach(function (association) {
      var name = [key, association.key].join('.');

      body.links[name] = {
        href: options.baseUrl + '/' +
          (!!options.namespace ? options.namespace + '/' : '') +
          association.type + '/{' + name + '}',
        type: association.type
      };
    });
  });
  return body;
}

/*
 * Expose the route method.
 */
module.exports = route;
<|endoftext|>


<commit_before>'use strict';

var toPositiveInteger = require('es5-ext/number/to-pos-integer')
  , primitiveSet      = require('es5-ext/object/primitive-set')
  , memoize           = require('memoizee/plain')
  , getNormalizer     = require('memoizee/normalizers/get-1')

  , forEach = Array.prototype.forEach, keys = Object.keys
  , byNum = function (a, b) { return a - b; }
  , actions = primitiveSet('activate', 'deactivate', 'insert', 'remove', 'mark', 'class')
  , getSubsteps;

getSubsteps = memoize(function (element) {
	var map = {}, defaultOrder = 0;
	forEach.call(element.querySelectorAll('.substep'), function (el) {
		var order = Number(el.dataset.order), action, data, names, classOrder;
		if (isNaN(order)) order = (defaultOrder += 0.01);
		else defaultOrder = order;
		if (!map[order]) map[order] = {};
		action = el.dataset.action;
		if (!actions[action]) action = null;
		if (action == null) {
			if (el.nodeName.toLowerCase() === 'mark') action = 'mark';
			else action = 'activate';
		}
		if (action === 'class') {
			if (!el.dataset.names) throw new TypeError('Missing names for class configuration');
			names = el.dataset.names.split(' ');
			classOrder = order;
			names.forEach(function (name) {
				data = { el: el, name: name };
				if (!map[classOrder]) map[classOrder] = {};
				if (!map[classOrder].class) map[classOrder].class = [data];
				else map[order][action].push(data);
				classOrder += 0.001;
			});
			return;
		}
		if (!map[order][action]) map[order][action] = [el];
		else map[order][action].push(el);
	});
	return keys(map).sort(byNum).map(function (order) { return map[order]; });
}, { normalizer: getNormalizer() });

module.exports = function (deck/*, options*/) {
	var activeSubstep = 0;

	deck.on('activate', function (e) {
		var substep = toPositiveInteger(e.substep);
		getSubsteps(e.slide).forEach(function (els, index) {
			var current = substep === index + 1
			  , isAfter = substep > index;
			if (els.activate) {
				els.activate.forEach(function (el) {
					el.classList[isAfter ? 'add' : 'remove']('active');
					el.classList[isAfter ? 'remove' : 'add']('inactive');
				});
			}
			if (els.deactivate) {
				els.deactivate.forEach(function (el) {
					el.classList[isAfter ? 'remove' : 'add']('active');
					el.classList[isAfter ? 'add' : 'remove']('inactive');
				});
			}
			if (els.insert) {
				els.insert.forEach(function (el) {
					el.classList[isAfter ? 'add' : 'remove']('inserted');
					el.classList[isAfter ? 'remove' : 'add']('removed');
				});
			}
			if (els.remove) {
				els.remove.forEach(function (el) {
					el.classList[isAfter ? 'remove' : 'add']('inserted');
					el.classList[isAfter ? 'add' : 'remove']('removed');
				});
			}
			if (els.mark) {
				els.mark.forEach(function (el) {
					el.classList[current ? 'add' : 'remove']('marked');
					el.classList[current ? 'remove' : 'add']('unmarked');
				});
			}
			if (els.class) {
				els.class.forEach(function (data) {
					data.el.classList[current ? 'add' : 'remove'](data.name);
				});
			}
		});
		activeSubstep = substep;
	});

	deck.on('next', function () {
		var activeSlide = deck.slide();
		if (!getSubsteps(deck.slides[activeSlide])[activeSubstep]) return;
		deck.slide(activeSlide, { substep: activeSubstep + 1 });
		return false;
	});

	deck.on('prev', function () {
		var activeSlide = deck.slide();
		if (!activeSlide) return;
		if (!activeSubstep) {
			--activeSlide;
			deck.slide(activeSlide, {
				substep: getSubsteps(deck.slides[activeSlide]).length
			});
		} else {
			deck.slide(activeSlide, { substep: activeSubstep - 1 });
		}
		return false;
	});
};
<commit_msg>Assure accuracy of decimal order<commit_after>'use strict';

var toPositiveInteger = require('es5-ext/number/to-pos-integer')
  , primitiveSet      = require('es5-ext/object/primitive-set')
  , memoize           = require('memoizee/plain')
  , getNormalizer     = require('memoizee/normalizers/get-1')

  , forEach = Array.prototype.forEach, keys = Object.keys, round = Math.round
  , byNum = function (a, b) { return a - b; }
  , actions = primitiveSet('activate', 'deactivate', 'insert', 'remove', 'mark', 'class')
  , getSubsteps;

getSubsteps = memoize(function (element) {
	var map = {}, defaultOrder = 0;
	forEach.call(element.querySelectorAll('.substep'), function (el) {
		var order = Number(el.dataset.order), action, data, names, classOrder;
		if (isNaN(order)) order = (defaultOrder += 0.01);
		else defaultOrder = order;
		if (!map[order]) map[order] = {};
		action = el.dataset.action;
		if (!actions[action]) action = null;
		if (action == null) {
			if (el.nodeName.toLowerCase() === 'mark') action = 'mark';
			else action = 'activate';
		}
		if (action === 'class') {
			if (!el.dataset.names) throw new TypeError('Missing names for class configuration');
			names = el.dataset.names.split(' ');
			classOrder = order;
			names.forEach(function (name) {
				data = { el: el, name: name };
				if (!map[classOrder]) map[classOrder] = {};
				if (!map[classOrder].class) map[classOrder].class = [data];
				else map[order][action].push(data);
				classOrder += 0.001;
				classOrder = round(classOrder * 1000) / 1000;
			});
			return;
		}
		if (!map[order][action]) map[order][action] = [el];
		else map[order][action].push(el);
	});
	return keys(map).sort(byNum).map(function (order) {
		console.log(order);
		return map[order]; });
}, { normalizer: getNormalizer() });

module.exports = function (deck/*, options*/) {
	var activeSubstep = 0;

	deck.on('activate', function (e) {
		var substep = toPositiveInteger(e.substep);
		getSubsteps(e.slide).forEach(function (els, index) {
			var current = substep === index + 1
			  , isAfter = substep > index;
			if (els.activate) {
				els.activate.forEach(function (el) {
					el.classList[isAfter ? 'add' : 'remove']('active');
					el.classList[isAfter ? 'remove' : 'add']('inactive');
				});
			}
			if (els.deactivate) {
				els.deactivate.forEach(function (el) {
					el.classList[isAfter ? 'remove' : 'add']('active');
					el.classList[isAfter ? 'add' : 'remove']('inactive');
				});
			}
			if (els.insert) {
				els.insert.forEach(function (el) {
					el.classList[isAfter ? 'add' : 'remove']('inserted');
					el.classList[isAfter ? 'remove' : 'add']('removed');
				});
			}
			if (els.remove) {
				els.remove.forEach(function (el) {
					el.classList[isAfter ? 'remove' : 'add']('inserted');
					el.classList[isAfter ? 'add' : 'remove']('removed');
				});
			}
			if (els.mark) {
				els.mark.forEach(function (el) {
					el.classList[current ? 'add' : 'remove']('marked');
					el.classList[current ? 'remove' : 'add']('unmarked');
				});
			}
			if (els.class) {
				els.class.forEach(function (data) {
					data.el.classList[current ? 'add' : 'remove'](data.name);
				});
			}
		});
		activeSubstep = substep;
	});

	deck.on('next', function () {
		var activeSlide = deck.slide();
		if (!getSubsteps(deck.slides[activeSlide])[activeSubstep]) return;
		deck.slide(activeSlide, { substep: activeSubstep + 1 });
		return false;
	});

	deck.on('prev', function () {
		var activeSlide = deck.slide();
		if (!activeSlide) return;
		if (!activeSubstep) {
			--activeSlide;
			deck.slide(activeSlide, {
				substep: getSubsteps(deck.slides[activeSlide]).length
			});
		} else {
			deck.slide(activeSlide, { substep: activeSubstep - 1 });
		}
		return false;
	});
};
<|endoftext|>