<commit_before>const Promise = require('bluebird');
const _ = require('lodash');
const uuid = require('uuid');
const crypto = require('crypto');
const keypair = require('keypair');
const ObjectID = require('bson-objectid');
const ghostBookshelf = require('./base');
const {i18n} = require('../lib/common');
const errors = require('@tryghost/errors');
const validation = require('../data/validation');
const internalContext = {context: {internal: true}};
let Settings;
let defaultSettings;

const doBlock = fn => fn();

const getMembersKey = doBlock(() => {
    let UNO_KEYPAIRINO;
    return function getKey(type) {
        if (!UNO_KEYPAIRINO) {
            UNO_KEYPAIRINO = keypair({bits: 1024});
        }
        return UNO_KEYPAIRINO[type];
    };
});

const getGhostKey = doBlock(() => {
    let UNO_KEYPAIRINO;
    return function getKey(type) {
        if (!UNO_KEYPAIRINO) {
            UNO_KEYPAIRINO = keypair({bits: 1024});
        }
        return UNO_KEYPAIRINO[type];
    };
});

// For neatness, the defaults file is split into categories.
// It's much easier for us to work with it as a single level
// instead of iterating those categories every time
function parseDefaultSettings() {
    const defaultSettingsInCategories = require('../data/schema/').defaultSettings;
    const defaultSettingsFlattened = {};

    const dynamicDefault = {
        db_hash: () => uuid.v4(),
        public_hash: () => crypto.randomBytes(15).toString('hex'),
        // @TODO: session_secret would ideally be named "admin_session_secret"
        session_secret: () => crypto.randomBytes(32).toString('hex'),
        theme_session_secret: () => crypto.randomBytes(32).toString('hex'),
        members_public_key: () => getMembersKey('public'),
        members_private_key: () => getMembersKey('private'),
        members_email_auth_secret: () => crypto.randomBytes(64).toString('hex'),
        ghost_public_key: () => getGhostKey('public'),
        ghost_private_key: () => getGhostKey('private')
    };

    _.each(defaultSettingsInCategories, function each(settings, categoryName) {
        _.each(settings, function eachSetting(setting, settingName) {
            setting.group = categoryName;
            setting.key = settingName;

            setting.getDefaultValue = function getDefaultValue() {
                const getDynamicDefault = dynamicDefault[setting.key];
                if (getDynamicDefault) {
                    return getDynamicDefault();
                } else {
                    return setting.defaultValue;
                }
            };

            defaultSettingsFlattened[settingName] = setting;
        });
    });

    return defaultSettingsFlattened;
}

function getDefaultSettings() {
    if (!defaultSettings) {
        defaultSettings = parseDefaultSettings();
    }

    return defaultSettings;
}

// Each setting is saved as a separate row in the database,
// but the overlying API treats them as a single key:value mapping
Settings = ghostBookshelf.Model.extend({

    tableName: 'settings',

    emitChange: function emitChange(event, options) {
        const eventToTrigger = 'settings' + '.' + event;
        ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
    },

    onDestroyed: function onDestroyed(model, options) {
        ghostBookshelf.Model.prototype.onDestroyed.apply(this, arguments);

        model.emitChange('deleted', options);
        model.emitChange(model._previousAttributes.key + '.' + 'deleted', options);
    },

    onCreated: function onCreated(model, response, options) {
        ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);

        model.emitChange('added', options);
        model.emitChange(model.attributes.key + '.' + 'added', options);
    },

    onUpdated: function onUpdated(model, response, options) {
        ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);

        model.emitChange('edited', options);
        model.emitChange(model.attributes.key + '.' + 'edited', options);
    },

    async onValidate(model, attr, options) {
        await ghostBookshelf.Model.prototype.onValidate.call(this, model, attr, options);

        await Settings.validators.all(model, options);

        if (typeof Settings.validators[model.get('key')] === 'function') {
            await Settings.validators[model.get('key')](model, options);
        }
    },

    format() {
        const attrs = ghostBookshelf.Model.prototype.format.apply(this, arguments);
        const settingType = attrs.type;

        if (settingType === 'boolean') {
            // CASE: Ensure we won't forward strings, otherwise model events or model interactions can fail
            if (attrs.value === '0' || attrs.value === '1') {
                attrs.value = !!+attrs.value;
            }

            // CASE: Ensure we won't forward strings, otherwise model events or model interactions can fail
            if (attrs.value === 'false' || attrs.value === 'true') {
                attrs.value = JSON.parse(attrs.value);
            }

            if (_.isBoolean(attrs.value)) {
                attrs.value = attrs.value.toString();
            }
        }

        return attrs;
    },

    parse() {
        const attrs = ghostBookshelf.Model.prototype.parse.apply(this, arguments);

        // transform "0" to false for boolean type
        const settingType = attrs.type;
        if (settingType === 'boolean' && (attrs.value === '0' || attrs.value === '1')) {
            attrs.value = !!+attrs.value;
        }

        // transform "false" to false for boolean type
        if (settingType === 'boolean' && (attrs.value === 'false' || attrs.value === 'true')) {
            attrs.value = JSON.parse(attrs.value);
        }

        return attrs;
    }
}, {
    findOne: function (data, options) {
        if (_.isEmpty(data)) {
            options = data;
        }

        // Allow for just passing the key instead of attributes
        if (!_.isObject(data)) {
            data = {key: data};
        }

        return Promise.resolve(ghostBookshelf.Model.findOne.call(this, data, options));
    },

    edit: function (data, unfilteredOptions) {
        const options = this.filterOptions(unfilteredOptions, 'edit');
        const self = this;

        if (!Array.isArray(data)) {
            data = [data];
        }

        return Promise.map(data, function (item) {
            // Accept an array of models as input
            if (item.toJSON) {
                item = item.toJSON();
            }
            if (!(_.isString(item.key) && item.key.length > 0)) {
                return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.settings.valueCannotBeBlank')}));
            }

            item = self.filterData(item);

            return Settings.forge({key: item.key}).fetch(options).then(function then(setting) {
                if (setting) {
                    // it's allowed to edit all attributes in case of importing/migrating
                    if (options.importing) {
                        return setting.save(item, options);
                    } else {
                        // If we have a value, set it.
                        if (Object.prototype.hasOwnProperty.call(item, 'value')) {
                            setting.set('value', item.value);
                        }
                        // Internal context can overwrite type (for fixture migrations)
                        if (options.context && options.context.internal && Object.prototype.hasOwnProperty.call(item, 'type')) {
                            setting.set('type', item.type);
                        }

                        // If anything has changed, save the updated model
                        if (setting.hasChanged()) {
                            return setting.save(null, options);
                        }

                        return setting;
                    }
                }

                return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.models.settings.unableToFindSetting', {key: item.key})}));
            });
        });
    },

    populateDefaults: async function populateDefaults(unfilteredOptions) {
        const options = this.filterOptions(unfilteredOptions, 'populateDefaults');
        const self = this;

        if (!options.context) {
            options.context = internalContext.context;
        }

        // this is required for sqlite to pick up the columns after db init
        await ghostBookshelf.knex.destroy();
        await ghostBookshelf.knex.initialize();

        // fetch available columns to avoid populating columns not yet created by migrations
        const columnInfo = await ghostBookshelf.knex.table('settings').columnInfo();
        const columns = Object.keys(columnInfo);

        // fetch other data that is used when inserting new settings
        const date = ghostBookshelf.knex.raw('CURRENT_TIMESTAMP');
        let owner;
        try {
            owner = await ghostBookshelf.model('User').getOwnerUser();
        } catch (e) {
            // in some tests the owner is deleted and not recreated before setup
            if (e.errorType === 'NotFoundError') {
                owner = {id: 1};
            } else {
                throw e;
            }
        }

        return this
            .findAll(options)
            .then(function checkAllSettings(allSettings) {
                const usedKeys = allSettings.models.map(function mapper(setting) {
                    return setting.get('key');
                });

                const insertOperations = [];

                _.each(getDefaultSettings(), function forEachDefault(defaultSetting, defaultSettingKey) {
                    const isMissingFromDB = usedKeys.indexOf(defaultSettingKey) === -1;
                    if (isMissingFromDB) {
                        defaultSetting.value = defaultSetting.getDefaultValue();

                        const settingValues = Object.assign({}, defaultSetting, {
                            id: ObjectID.generate(),
                            created_at: date,
                            created_by: owner.id,
                            updated_at: date,
                            updated_by: owner.id
                        });

                        insertOperations.push(
                            ghostBookshelf.knex
                                .table('settings')
                                .insert(_.pick(settingValues, columns))
                        );
                    }
                });

                if (insertOperations.length > 0) {
                    return Promise.all(insertOperations).then(function fetchAllToReturn() {
                        return self.findAll(options);
                    });
                }

                return allSettings;
            });
    },

    permissible: function permissible(modelId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
        if (hasUserPermission && hasApiKeyPermission) {
            return Promise.resolve();
        }

        return Promise.reject(new errors.NoPermissionError({
            message: i18n.t('errors.models.post.notEnoughPermission')
        }));
    },

    validators: {
        async all(model) {
            const settingName = model.get('key');
            const settingDefault = getDefaultSettings()[settingName];

            if (!settingDefault) {
                return;
            }

            // Basic validations from default-settings.json
            const validationErrors = validation.validate(
                model.get('value'),
                model.get('key'),
                settingDefault.validations,
                'settings'
            );

            if (validationErrors.length) {
                throw new errors.ValidationError(validationErrors.join('\n'));
            }
        },
        async stripe_plans(model, options) {
            const plans = JSON.parse(model.get('value'));
            for (const plan of plans) {
                // Stripe plans used to be allowed (and defaulted to!) 0 amount plans
                // this causes issues to people importing from older versions of Ghost
                // even if they don't use Members/Stripe
                // issue: https://github.com/TryGhost/Ghost/issues/12049
                if (!options.importing) {
                    // We check 100, not 1, because amounts are in fractional units
                    if (plan.amount < 100 && plan.name !== 'Complimentary') {
                        throw new errors.ValidationError({
                            message: 'Plans cannot have an amount less than 1'
                        });
                    }
                }

                if (typeof plan.name !== 'string') {
                    throw new errors.ValidationError({
                        message: 'Plan must have a name'
                    });
                }

                if (typeof plan.currency !== 'string') {
                    throw new errors.ValidationError({
                        message: 'Plan must have a currency'
                    });
                }

                if (!['year', 'month', 'week', 'day'].includes(plan.interval)) {
                    throw new errors.ValidationError({
                        message: 'Plan interval must be one of: year, month, week or day'
                    });
                }
            }
        },
        // @TODO: Maybe move some of the logic into the members service, exporting an isValidStripeKey
        // method which can be called here, cleaning up the duplication, but not removing control
        async stripe_secret_key(model) {
            const value = model.get('value');
            if (value === null) {
                return;
            }

            const secretKeyRegex = /(?:sk|rk)_(?:test|live)_[\da-zA-Z]{1,247}$/;

            if (!secretKeyRegex.test(value)) {
                throw new errors.ValidationError({
                    message: `stripe_secret_key did not match ${secretKeyRegex}`
                });
            }
        },
        async stripe_publishable_key(model) {
            const value = model.get('value');
            if (value === null) {
                return;
            }

            const secretKeyRegex = /pk_(?:test|live)_[\da-zA-Z]{1,247}$/;

            if (!secretKeyRegex.test(value)) {
                throw new errors.ValidationError({
                    message: `stripe_secret_key did not match ${secretKeyRegex}`
                });
            }
        },
        async stripe_connect_secret_key(model) {
            const value = model.get('value');
            if (value === null) {
                return;
            }

            const secretKeyRegex = /(?:sk|rk)_(?:test|live)_[\da-zA-Z]{1,247}$/;

            if (!secretKeyRegex.test(value)) {
                throw new errors.ValidationError({
                    message: `stripe_secret_key did not match ${secretKeyRegex}`
                });
            }
        },
        async stripe_connect_publishable_key(model) {
            const value = model.get('value');
            if (value === null) {
                return;
            }

            const secretKeyRegex = /pk_(?:test|live)_[\da-zA-Z]{1,247}$/;

            if (!secretKeyRegex.test(value)) {
                throw new errors.ValidationError({
                    message: `stripe_secret_key did not match ${secretKeyRegex}`
                });
            }
        }
    }
});

module.exports = {
    Settings: ghostBookshelf.model('Settings', Settings)
};
<commit_msg>Fixed settings validation error not distinguishing between publishable/secret keys<commit_after>const Promise = require('bluebird');
const _ = require('lodash');
const uuid = require('uuid');
const crypto = require('crypto');
const keypair = require('keypair');
const ObjectID = require('bson-objectid');
const ghostBookshelf = require('./base');
const {i18n} = require('../lib/common');
const errors = require('@tryghost/errors');
const validation = require('../data/validation');
const internalContext = {context: {internal: true}};
let Settings;
let defaultSettings;

const doBlock = fn => fn();

const getMembersKey = doBlock(() => {
    let UNO_KEYPAIRINO;
    return function getKey(type) {
        if (!UNO_KEYPAIRINO) {
            UNO_KEYPAIRINO = keypair({bits: 1024});
        }
        return UNO_KEYPAIRINO[type];
    };
});

const getGhostKey = doBlock(() => {
    let UNO_KEYPAIRINO;
    return function getKey(type) {
        if (!UNO_KEYPAIRINO) {
            UNO_KEYPAIRINO = keypair({bits: 1024});
        }
        return UNO_KEYPAIRINO[type];
    };
});

// For neatness, the defaults file is split into categories.
// It's much easier for us to work with it as a single level
// instead of iterating those categories every time
function parseDefaultSettings() {
    const defaultSettingsInCategories = require('../data/schema/').defaultSettings;
    const defaultSettingsFlattened = {};

    const dynamicDefault = {
        db_hash: () => uuid.v4(),
        public_hash: () => crypto.randomBytes(15).toString('hex'),
        // @TODO: session_secret would ideally be named "admin_session_secret"
        session_secret: () => crypto.randomBytes(32).toString('hex'),
        theme_session_secret: () => crypto.randomBytes(32).toString('hex'),
        members_public_key: () => getMembersKey('public'),
        members_private_key: () => getMembersKey('private'),
        members_email_auth_secret: () => crypto.randomBytes(64).toString('hex'),
        ghost_public_key: () => getGhostKey('public'),
        ghost_private_key: () => getGhostKey('private')
    };

    _.each(defaultSettingsInCategories, function each(settings, categoryName) {
        _.each(settings, function eachSetting(setting, settingName) {
            setting.group = categoryName;
            setting.key = settingName;

            setting.getDefaultValue = function getDefaultValue() {
                const getDynamicDefault = dynamicDefault[setting.key];
                if (getDynamicDefault) {
                    return getDynamicDefault();
                } else {
                    return setting.defaultValue;
                }
            };

            defaultSettingsFlattened[settingName] = setting;
        });
    });

    return defaultSettingsFlattened;
}

function getDefaultSettings() {
    if (!defaultSettings) {
        defaultSettings = parseDefaultSettings();
    }

    return defaultSettings;
}

// Each setting is saved as a separate row in the database,
// but the overlying API treats them as a single key:value mapping
Settings = ghostBookshelf.Model.extend({

    tableName: 'settings',

    emitChange: function emitChange(event, options) {
        const eventToTrigger = 'settings' + '.' + event;
        ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
    },

    onDestroyed: function onDestroyed(model, options) {
        ghostBookshelf.Model.prototype.onDestroyed.apply(this, arguments);

        model.emitChange('deleted', options);
        model.emitChange(model._previousAttributes.key + '.' + 'deleted', options);
    },

    onCreated: function onCreated(model, response, options) {
        ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);

        model.emitChange('added', options);
        model.emitChange(model.attributes.key + '.' + 'added', options);
    },

    onUpdated: function onUpdated(model, response, options) {
        ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);

        model.emitChange('edited', options);
        model.emitChange(model.attributes.key + '.' + 'edited', options);
    },

    async onValidate(model, attr, options) {
        await ghostBookshelf.Model.prototype.onValidate.call(this, model, attr, options);

        await Settings.validators.all(model, options);

        if (typeof Settings.validators[model.get('key')] === 'function') {
            await Settings.validators[model.get('key')](model, options);
        }
    },

    format() {
        const attrs = ghostBookshelf.Model.prototype.format.apply(this, arguments);
        const settingType = attrs.type;

        if (settingType === 'boolean') {
            // CASE: Ensure we won't forward strings, otherwise model events or model interactions can fail
            if (attrs.value === '0' || attrs.value === '1') {
                attrs.value = !!+attrs.value;
            }

            // CASE: Ensure we won't forward strings, otherwise model events or model interactions can fail
            if (attrs.value === 'false' || attrs.value === 'true') {
                attrs.value = JSON.parse(attrs.value);
            }

            if (_.isBoolean(attrs.value)) {
                attrs.value = attrs.value.toString();
            }
        }

        return attrs;
    },

    parse() {
        const attrs = ghostBookshelf.Model.prototype.parse.apply(this, arguments);

        // transform "0" to false for boolean type
        const settingType = attrs.type;
        if (settingType === 'boolean' && (attrs.value === '0' || attrs.value === '1')) {
            attrs.value = !!+attrs.value;
        }

        // transform "false" to false for boolean type
        if (settingType === 'boolean' && (attrs.value === 'false' || attrs.value === 'true')) {
            attrs.value = JSON.parse(attrs.value);
        }

        return attrs;
    }
}, {
    findOne: function (data, options) {
        if (_.isEmpty(data)) {
            options = data;
        }

        // Allow for just passing the key instead of attributes
        if (!_.isObject(data)) {
            data = {key: data};
        }

        return Promise.resolve(ghostBookshelf.Model.findOne.call(this, data, options));
    },

    edit: function (data, unfilteredOptions) {
        const options = this.filterOptions(unfilteredOptions, 'edit');
        const self = this;

        if (!Array.isArray(data)) {
            data = [data];
        }

        return Promise.map(data, function (item) {
            // Accept an array of models as input
            if (item.toJSON) {
                item = item.toJSON();
            }
            if (!(_.isString(item.key) && item.key.length > 0)) {
                return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.settings.valueCannotBeBlank')}));
            }

            item = self.filterData(item);

            return Settings.forge({key: item.key}).fetch(options).then(function then(setting) {
                if (setting) {
                    // it's allowed to edit all attributes in case of importing/migrating
                    if (options.importing) {
                        return setting.save(item, options);
                    } else {
                        // If we have a value, set it.
                        if (Object.prototype.hasOwnProperty.call(item, 'value')) {
                            setting.set('value', item.value);
                        }
                        // Internal context can overwrite type (for fixture migrations)
                        if (options.context && options.context.internal && Object.prototype.hasOwnProperty.call(item, 'type')) {
                            setting.set('type', item.type);
                        }

                        // If anything has changed, save the updated model
                        if (setting.hasChanged()) {
                            return setting.save(null, options);
                        }

                        return setting;
                    }
                }

                return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.models.settings.unableToFindSetting', {key: item.key})}));
            });
        });
    },

    populateDefaults: async function populateDefaults(unfilteredOptions) {
        const options = this.filterOptions(unfilteredOptions, 'populateDefaults');
        const self = this;

        if (!options.context) {
            options.context = internalContext.context;
        }

        // this is required for sqlite to pick up the columns after db init
        await ghostBookshelf.knex.destroy();
        await ghostBookshelf.knex.initialize();

        // fetch available columns to avoid populating columns not yet created by migrations
        const columnInfo = await ghostBookshelf.knex.table('settings').columnInfo();
        const columns = Object.keys(columnInfo);

        // fetch other data that is used when inserting new settings
        const date = ghostBookshelf.knex.raw('CURRENT_TIMESTAMP');
        let owner;
        try {
            owner = await ghostBookshelf.model('User').getOwnerUser();
        } catch (e) {
            // in some tests the owner is deleted and not recreated before setup
            if (e.errorType === 'NotFoundError') {
                owner = {id: 1};
            } else {
                throw e;
            }
        }

        return this
            .findAll(options)
            .then(function checkAllSettings(allSettings) {
                const usedKeys = allSettings.models.map(function mapper(setting) {
                    return setting.get('key');
                });

                const insertOperations = [];

                _.each(getDefaultSettings(), function forEachDefault(defaultSetting, defaultSettingKey) {
                    const isMissingFromDB = usedKeys.indexOf(defaultSettingKey) === -1;
                    if (isMissingFromDB) {
                        defaultSetting.value = defaultSetting.getDefaultValue();

                        const settingValues = Object.assign({}, defaultSetting, {
                            id: ObjectID.generate(),
                            created_at: date,
                            created_by: owner.id,
                            updated_at: date,
                            updated_by: owner.id
                        });

                        insertOperations.push(
                            ghostBookshelf.knex
                                .table('settings')
                                .insert(_.pick(settingValues, columns))
                        );
                    }
                });

                if (insertOperations.length > 0) {
                    return Promise.all(insertOperations).then(function fetchAllToReturn() {
                        return self.findAll(options);
                    });
                }

                return allSettings;
            });
    },

    permissible: function permissible(modelId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
        if (hasUserPermission && hasApiKeyPermission) {
            return Promise.resolve();
        }

        return Promise.reject(new errors.NoPermissionError({
            message: i18n.t('errors.models.post.notEnoughPermission')
        }));
    },

    validators: {
        async all(model) {
            const settingName = model.get('key');
            const settingDefault = getDefaultSettings()[settingName];

            if (!settingDefault) {
                return;
            }

            // Basic validations from default-settings.json
            const validationErrors = validation.validate(
                model.get('value'),
                model.get('key'),
                settingDefault.validations,
                'settings'
            );

            if (validationErrors.length) {
                throw new errors.ValidationError(validationErrors.join('\n'));
            }
        },
        async stripe_plans(model, options) {
            const plans = JSON.parse(model.get('value'));
            for (const plan of plans) {
                // Stripe plans used to be allowed (and defaulted to!) 0 amount plans
                // this causes issues to people importing from older versions of Ghost
                // even if they don't use Members/Stripe
                // issue: https://github.com/TryGhost/Ghost/issues/12049
                if (!options.importing) {
                    // We check 100, not 1, because amounts are in fractional units
                    if (plan.amount < 100 && plan.name !== 'Complimentary') {
                        throw new errors.ValidationError({
                            message: 'Plans cannot have an amount less than 1'
                        });
                    }
                }

                if (typeof plan.name !== 'string') {
                    throw new errors.ValidationError({
                        message: 'Plan must have a name'
                    });
                }

                if (typeof plan.currency !== 'string') {
                    throw new errors.ValidationError({
                        message: 'Plan must have a currency'
                    });
                }

                if (!['year', 'month', 'week', 'day'].includes(plan.interval)) {
                    throw new errors.ValidationError({
                        message: 'Plan interval must be one of: year, month, week or day'
                    });
                }
            }
        },
        // @TODO: Maybe move some of the logic into the members service, exporting an isValidStripeKey
        // method which can be called here, cleaning up the duplication, but not removing control
        async stripe_secret_key(model) {
            const value = model.get('value');
            if (value === null) {
                return;
            }

            const secretKeyRegex = /(?:sk|rk)_(?:test|live)_[\da-zA-Z]{1,247}$/;

            if (!secretKeyRegex.test(value)) {
                throw new errors.ValidationError({
                    message: `stripe_secret_key did not match ${secretKeyRegex}`
                });
            }
        },
        async stripe_publishable_key(model) {
            const value = model.get('value');
            if (value === null) {
                return;
            }

            const publishableKeyRegex = /pk_(?:test|live)_[\da-zA-Z]{1,247}$/;

            if (!publishableKeyRegex.test(value)) {
                throw new errors.ValidationError({
                    message: `stripe_publishable_key did not match ${publishableKeyRegex}`
                });
            }
        },
        async stripe_connect_secret_key(model) {
            const value = model.get('value');
            if (value === null) {
                return;
            }

            const secretKeyRegex = /(?:sk|rk)_(?:test|live)_[\da-zA-Z]{1,247}$/;

            if (!secretKeyRegex.test(value)) {
                throw new errors.ValidationError({
                    message: `stripe_secret_key did not match ${secretKeyRegex}`
                });
            }
        },
        async stripe_connect_publishable_key(model) {
            const value = model.get('value');
            if (value === null) {
                return;
            }

            const publishableKeyRegex = /pk_(?:test|live)_[\da-zA-Z]{1,247}$/;

            if (!publishableKeyRegex.test(value)) {
                throw new errors.ValidationError({
                    message: `stripe_publishable_key did not match ${publishableKeyRegex}`
                });
            }
        }
    }
});

module.exports = {
    Settings: ghostBookshelf.model('Settings', Settings)
};
<|endoftext|>


<commit_before>var lastLookup = [];
var lastSearch = [];
var bool = [true];
var lastTerm = [];
var opts = {
    lines: 13, // The number of lines to draw
    length: 7, // The length of each line
    width: 4, // The line thickness
    radius: 10, // The radius of the inner circle
    corners: 1, // Corner roundness (0..1)
    rotate: 0, // The rotation offset
    color: '#000', // #rgb or #rrggbb
    speed: 1, // Rounds per second
    trail: 60, // Afterglow percentage
    shadow: false, // Whether to render a shadow
    hwaccel: false, // Whether to use hardware acceleration
    className: 'spinner', // The CSS class to assign to the spinner
    zIndex: 2e9, // The z-index (defaults to 2000000000)
    top: 'auto', // Top position relative to parent in px
    left: 'auto' // Left position relative to parent in px
};
/*
* Search the yelp api
*/
var searchYelp = function (term, near, category_filters, offset) {
    var target = document.getElementById('columnCenter');
    $('.columnCenter').empty();
    var spinner = new Spinner(opts).spin(target);
    var opt_int;
    var opt_cat;
    if (offset == undefined) {
        opt_int = 0;
    } else {
        opt_int = offset;
    }
    if (category_filters == undefined) {
        opt_cat = "";
    } else {
        opt_cat = category_filters;
    }
    if (near && term){
        searchQuery(term, near, opt_cat, opt_int);
    } else {
        if(bool[0]) {
            spinner.stop();
            bool = [];
            bool.push(false);
            $('#middle').append('
' +
                                '<button type="button" onclick="bool.pop(); bool.push(true);" class="close" data-dismiss="alert">×</button>'+
                                'Error! Please include the appropriate info in the appropriate fields.'+
                                '
');
        }
    }
};

/*
*
*/
var getBookmarks = function(term) {
    lastTerm.push(term);
    var bookmarks = searchFavorites(term);
    browseFavorites(bookmarks);
};

/*
* Browses Favorites
*/
var browseFavorites = function(data) {
    var current = $('.active1').text();
    if (current != 'Lookup'){
        lastLookup.push(data);
        $('.active1').remove();
        $('.columnCenter').empty();
        $('.breadcrumb').empty();
        $('.breadcrumb').append('
' + 'Home' + ' /
');
        $('.breadcrumb').append('
Lookup
 ');
        history.pushState({}, 'Yelptastic- Browse Favorite', '#bookmarksearch');
    }
    $('.columnCenter').empty();

    browseBookmarks(data, 0);
};

/*
* Displays the view for searching yelp.
*/
var addFavoritesView = function(data, total, opt_term, opt_location, opt_cat, opt_offset) {
    //$('.breadcrumbs').removeClass('.active');
    var current = $('.active1').text();
    $('.alert').remove();
    bool = [];
    bool.push(true);

    lastSearch.push(data);
    lastSearch.push(total);
    lastSearch.push(opt_term);
    lastSearch.push(opt_location);
    lastSearch.push(opt_cat);
    lastSearch.push(opt_offset);
    if (current != 'Search'){
        $('.active1').remove();
        $('.columnCenter').empty();
        $('.breadcrumb').empty();
        $('.breadcrumb').append('
' + 'Home' + ' /
');
        $('.breadcrumb').append('
Search
 ');
        history.pushState({}, 'Yelptastic- Yelp Search', '#yelpsearch');
    }
    $('.columnLeft').empty();
    $('.columnCenter').empty();

    var allcategories = $.getJSON("scripts/categories.json", function(y){
        parseYelpCategories(opt_term, opt_location, y);
    });

    addQuery(data, opt_offset, total, opt_term, opt_location, opt_cat);
};

/*
* Takes in the results and appends them to the thumbnails grids
*/
var addQuery = function (results, opt_int, opt_total, opt_term, opt_location, opt_cat) {
    $('.columnCenter').empty();
    $thumbnailswrapper = $('

');
    var data = [];
    for (var i = 0; i < results.length; i ++) {
        var image_url;
        $button = $('<button>Favorite</button>');
        if(results[i].image_url) {
            image_url = results[i].image_url;
        } else {
            image_url = 'http://s3-media3.ak.yelpcdn.com/assets/2/www/img/305e17fe6ed8/gfx/blank_biz_medium_sq.png';
        }
        data.push({
            name:results[i].name,
            image:image_url,
            quote:results[i].snippet_text,
            ratingImage:results[i].rating_img_url,
            button:'Favorite',
            object:JSON.stringify(results[i]),
            url:results[i].url,
            phonenumber:results[i].display_phone,
            n:i.toString(),
            views:results[i].review_count,
            distance:results[i].distance,
            address:results[i].location.display_address[0] +" " + results[i].location.display_address[1]+" " + results[i].location.display_address[2]
        });
    }
    var source = $('#results').html();
    var template = Handlebars.compile(source);
    var string = template(data);
    var target = document.getElementById('columnCenter');
    var spinner = new Spinner(opts).stop(target);

    if(results.length > 0){
        $thumbnailswrapper.append(string);
    }
    else{
        $("#columnCenter").text("Sorry, there are no businesses to display.");
    }

    if (opt_total > 20) {
        $buttonRow = $('

');

        if (opt_int >= 20) {
            $see_prev = $('<button class="btn" value="Previous 20" name="prevButton">Previous 20</button>');
            $see_prev.click(function() {
                searchYelp(opt_term, opt_location, opt_cat, opt_int-20);
            });
            $buttonRow.append($see_prev);
        }
        if (opt_int + 20 <= opt_total) {
            $see_all = $('<button class="btn" value="Next 20" name="nextButton">Next 20</button>');
            $see_all.click(function() {
                searchYelp(opt_term, opt_location, opt_cat, opt_int + 20);
            });
            $buttonRow.append($see_all);
        }
        $thumbnailswrapper.append($buttonRow);
    }
    $('.columnCenter').append($thumbnailswrapper);
    window.scroll();
};

/*
* Takes in the results and appends them to the thumbnails grids
*/
var browseBookmarks = function (results, opt_int) {
    $('.columnCenter').empty();
    var target = document.getElementById('columnCenter');
    var spinner = new Spinner(opts).spin(target);
    $thumbnailswrapper = $('

');
    $buttonRow = $('

');
    var data = [];
    var image_url;
    if (results.length >=20) {
        for (var i = opt_int; i < opt_int + 20; i ++) {
            if(results[i].image_url) {
                image_url = results[i].image_url;
            } else {
                image_url = 'http://s3-media3.ak.yelpcdn.com/assets/2/www/img/305e17fe6ed8/gfx/blank_biz_medium_sq.png';
            }
            $button = $('<button>Favorite</button>');
            alert('results[i].location.display_address[0]');
            data.push({
                name:results[i].name,
                image:image_url,
                quote:results[i].snippet_text,
                ratingImage:results[i].rating_img_url,
                tags:results[i].tags,
                object:JSON.stringify(results[i]),
                url:results[i].url,
                notes:results[i].notes,
                views:results[i].review_count,
                distance:results[i].distance,
                address:results[i].location.display_address[0] +" " + results[i].location.display_address[1]+" "+ results[i].location.display_address[2]

            });
        }
    } else {
        for (var i = opt_int; i < results.length; i ++) {
            if(results[i].image_url) {
                image_url = results[i].image_url;
            } else {
                image_url = 'http://s3-media3.ak.yelpcdn.com/assets/2/www/img/305e17fe6ed8/gfx/blank_biz_medium_sq.png';
            }
            $button = $('<button>Favorite</button>');
            data.push({
                name:results[i].name,
                image:image_url,
                quote:results[i].snippet_text,
                ratingImage:results[i].rating_img_url,
                tags:results[i].tags,
                url:results[i].url,
                notes:results[i].notes,
                object:JSON.stringify(results[i]),
                views:results[i].review_count,
                distance:results[i].distance,
                address:results[i].location.display_address[0] +" " + results[i].location.display_address[1]+" " + results[i].location.display_address[2]
            });
        }
    }
    var source = $('#browse').html();
    var template = Handlebars.compile(source);
    var string = template(data);
    spinner.stop();
    $thumbnailswrapper.append(string);
    if (data.length >= 20) {
        if (opt_int >= 20) {
            $see_prev = $('<button class="btn" value="Previous" name="nextButton">Previous 20</button>');
            $see_prev.click(function() {
                addQuery(results, opt_int-1);
            });
            $buttonRow.append($see_prev);
        }
        if (opt_int + 20 <= data.length) {
            $see_all = $('<button class="btn" value="Next" name="prevButton">Next 20</button>');
            $see_all.click(function() {
                addQuery(results, opt_int + 20);
            });
            $buttonRow.append($see_all);
        }
    }
    var source1 = $('#filts').html();
    var template1 = Handlebars.compile(source1);
    var string1 = template1({funcname:'filterFavorites', results:JSON.stringify(results)});
    alert('Printing String: ' + string1);
    $('.columnRight').empty();
    $('.columnRight').append(string1);
    $thumbnailswrapper.append($buttonRow);
    $('.columnCenter').append($thumbnailswrapper);
    $('#pop').popover();
};

/*
* Function to send the bookmarks to the backend
*/
var saveFavorite = function (jsonString, tags1, notes1) {
    var object1 = jsonString;
    saveBookmark(object1, tags1, notes1);
};

var parseYelpCategories = function (term, location, categories) {

    var categories_wrapper = '
';
    for (var i in categories)
    {
        var genCategory = i;
        var genCategoryTag = i;
        genCategoryTag = genCategoryTag.replace(/ /g, "_");
        genCategoryTag = genCategoryTag.replace(/&/g, "And");
        categories_wrapper = categories_wrapper + '
';
        categories_wrapper = categories_wrapper + '
';
        categories_wrapper = categories_wrapper + '';
        categories_wrapper = categories_wrapper + genCategory;
        categories_wrapper = categories_wrapper + '';
        categories_wrapper = categories_wrapper + '
';
        categories_wrapper = categories_wrapper + '
';
        categories_wrapper = categories_wrapper + '
';

        for (var j in categories[i])
        {
            var subCategory = j;
            var subCategoryTag = categories[i][j];
            if (typeof categories[i][j] != "string")
            {
                subCategoryTag = subCategory;
                subCategoryTag = subCategoryTag.replace(/ /g, "_");
                subCategoryTag = subCategoryTag.replace(/&/g, "And");
            }

            categories_wrapper = categories_wrapper + '
';
            categories_wrapper = categories_wrapper + '
';
            categories_wrapper = categories_wrapper + '';

            categories_wrapper = categories_wrapper + subCategory;
            categories_wrapper = categories_wrapper + '';
            categories_wrapper = categories_wrapper + '
';

            if (typeof categories[i][j] != "string")
            {
                categories_wrapper = categories_wrapper + '
';
                categories_wrapper = categories_wrapper + '
';

                for (var k in categories[i][j])
                {
                    var subSubCategory = k;
                    var subSubCategoryTag = categories[i][j][k];

                    categories_wrapper = categories_wrapper + '
';
                    categories_wrapper = categories_wrapper + '
';
                    categories_wrapper = categories_wrapper + '';
                    categories_wrapper = categories_wrapper + subSubCategory;
                    categories_wrapper = categories_wrapper + '';
                    categories_wrapper = categories_wrapper + '
';
                    categories_wrapper = categories_wrapper + '
';
                }

                categories_wrapper = categories_wrapper + '
';
                categories_wrapper = categories_wrapper + '
';
            }
            categories_wrapper = categories_wrapper + '
';
        }

        categories_wrapper = categories_wrapper + '
';
        categories_wrapper = categories_wrapper + '
';
        categories_wrapper = categories_wrapper + '
';
    }
    categories_wrapper = categories_wrapper + "
";
    console.log(categories_wrapper);
    $('.columnLeft').append(categories_wrapper);
};
<commit_msg>Add message when no bookmarks can be displayed.<commit_after>var lastLookup = [];
var lastSearch = [];
var bool = [true];
var lastTerm = [];
var opts = {
    lines: 13, // The number of lines to draw
    length: 7, // The length of each line
    width: 4, // The line thickness
    radius: 10, // The radius of the inner circle
    corners: 1, // Corner roundness (0..1)
    rotate: 0, // The rotation offset
    color: '#000', // #rgb or #rrggbb
    speed: 1, // Rounds per second
    trail: 60, // Afterglow percentage
    shadow: false, // Whether to render a shadow
    hwaccel: false, // Whether to use hardware acceleration
    className: 'spinner', // The CSS class to assign to the spinner
    zIndex: 2e9, // The z-index (defaults to 2000000000)
    top: 'auto', // Top position relative to parent in px
    left: 'auto' // Left position relative to parent in px
};
/*
* Search the yelp api
*/
var searchYelp = function (term, near, category_filters, offset) {
    var target = document.getElementById('columnCenter');
    $('.columnCenter').empty();
    var spinner = new Spinner(opts).spin(target);
    var opt_int;
    var opt_cat;
    if (offset == undefined) {
        opt_int = 0;
    } else {
        opt_int = offset;
    }
    if (category_filters == undefined) {
        opt_cat = "";
    } else {
        opt_cat = category_filters;
    }
    if (near && term){
        searchQuery(term, near, opt_cat, opt_int);
    } else {
        if(bool[0]) {
            spinner.stop();
            bool = [];
            bool.push(false);
            $('#middle').append('
' +
                                '<button type="button" onclick="bool.pop(); bool.push(true);" class="close" data-dismiss="alert">×</button>'+
                                'Error! Please include the appropriate info in the appropriate fields.'+
                                '
');
        }
    }
};

/*
*
*/
var getBookmarks = function(term) {
    lastTerm.push(term);
    var bookmarks = searchFavorites(term);
    browseFavorites(bookmarks);
};

/*
* Browses Favorites
*/
var browseFavorites = function(data) {
    var current = $('.active1').text();
    if (current != 'Lookup'){
        lastLookup.push(data);
        $('.active1').remove();
        $('.columnCenter').empty();
        $('.breadcrumb').empty();
        $('.breadcrumb').append('
' + 'Home' + ' /
');
        $('.breadcrumb').append('
Lookup
 ');
        history.pushState({}, 'Yelptastic- Browse Favorite', '#bookmarksearch');
    }
    $('.columnCenter').empty();

    browseBookmarks(data, 0);
};

/*
* Displays the view for searching yelp.
*/
var addFavoritesView = function(data, total, opt_term, opt_location, opt_cat, opt_offset) {
    //$('.breadcrumbs').removeClass('.active');
    var current = $('.active1').text();
    $('.alert').remove();
    bool = [];
    bool.push(true);

    lastSearch.push(data);
    lastSearch.push(total);
    lastSearch.push(opt_term);
    lastSearch.push(opt_location);
    lastSearch.push(opt_cat);
    lastSearch.push(opt_offset);
    if (current != 'Search'){
        $('.active1').remove();
        $('.columnCenter').empty();
        $('.breadcrumb').empty();
        $('.breadcrumb').append('
' + 'Home' + ' /
');
        $('.breadcrumb').append('
Search
 ');
        history.pushState({}, 'Yelptastic- Yelp Search', '#yelpsearch');
    }
    $('.columnLeft').empty();
    $('.columnCenter').empty();

    var allcategories = $.getJSON("scripts/categories.json", function(y){
        parseYelpCategories(opt_term, opt_location, y);
    });

    addQuery(data, opt_offset, total, opt_term, opt_location, opt_cat);
};

/*
* Takes in the results and appends them to the thumbnails grids
*/
var addQuery = function (results, opt_int, opt_total, opt_term, opt_location, opt_cat) {
    $('.columnCenter').empty();
    $thumbnailswrapper = $('

');
    var data = [];
    for (var i = 0; i < results.length; i ++) {
        var image_url;
        $button = $('<button>Favorite</button>');
        if(results[i].image_url) {
            image_url = results[i].image_url;
        } else {
            image_url = 'http://s3-media3.ak.yelpcdn.com/assets/2/www/img/305e17fe6ed8/gfx/blank_biz_medium_sq.png';
        }
        data.push({
            name:results[i].name,
            image:image_url,
            quote:results[i].snippet_text,
            ratingImage:results[i].rating_img_url,
            button:'Favorite',
            object:JSON.stringify(results[i]),
            url:results[i].url,
            phonenumber:results[i].display_phone,
            n:i.toString(),
            views:results[i].review_count,
            distance:results[i].distance,
            address:results[i].location.display_address[0] +" " + results[i].location.display_address[1]+" " + results[i].location.display_address[2]
        });
    }
    var source = $('#results').html();
    var template = Handlebars.compile(source);
    var string = template(data);
    var target = document.getElementById('columnCenter');
    var spinner = new Spinner(opts).stop(target);

    if(results.length > 0){
        $thumbnailswrapper.append(string);
    }
    else{
        $("#columnCenter").text("Sorry, there are no businesses to display.");
    }

    if (opt_total > 20) {
        $buttonRow = $('

');

        if (opt_int >= 20) {
            $see_prev = $('<button class="btn" value="Previous 20" name="prevButton">Previous 20</button>');
            $see_prev.click(function() {
                searchYelp(opt_term, opt_location, opt_cat, opt_int-20);
            });
            $buttonRow.append($see_prev);
        }
        if (opt_int + 20 <= opt_total) {
            $see_all = $('<button class="btn" value="Next 20" name="nextButton">Next 20</button>');
            $see_all.click(function() {
                searchYelp(opt_term, opt_location, opt_cat, opt_int + 20);
            });
            $buttonRow.append($see_all);
        }
        $thumbnailswrapper.append($buttonRow);
    }
    $('.columnCenter').append($thumbnailswrapper);
    window.scroll();
};

/*
* Takes in the results and appends them to the thumbnails grids
*/
var browseBookmarks = function (results, opt_int) {
    $('.columnCenter').empty();
    var target = document.getElementById('columnCenter');
    var spinner = new Spinner(opts).spin(target);
    $thumbnailswrapper = $('

');
    $buttonRow = $('

');
    var data = [];
    var image_url;
    if (results.length >=20) {
        for (var i = opt_int; i < opt_int + 20; i ++) {
            if(results[i].image_url) {
                image_url = results[i].image_url;
            } else {
                image_url = 'http://s3-media3.ak.yelpcdn.com/assets/2/www/img/305e17fe6ed8/gfx/blank_biz_medium_sq.png';
            }
            $button = $('<button>Favorite</button>');
            alert('results[i].location.display_address[0]');
            data.push({
                name:results[i].name,
                image:image_url,
                quote:results[i].snippet_text,
                ratingImage:results[i].rating_img_url,
                tags:results[i].tags,
                object:JSON.stringify(results[i]),
                url:results[i].url,
                notes:results[i].notes,
                views:results[i].review_count,
                distance:results[i].distance,
                address:results[i].location.display_address[0] +" " + results[i].location.display_address[1]+" "+ results[i].location.display_address[2]

            });
        }
    } else {
        for (var i = opt_int; i < results.length; i ++) {
            if(results[i].image_url) {
                image_url = results[i].image_url;
            } else {
                image_url = 'http://s3-media3.ak.yelpcdn.com/assets/2/www/img/305e17fe6ed8/gfx/blank_biz_medium_sq.png';
            }
            $button = $('<button>Favorite</button>');
            data.push({
                name:results[i].name,
                image:image_url,
                quote:results[i].snippet_text,
                ratingImage:results[i].rating_img_url,
                tags:results[i].tags,
                url:results[i].url,
                notes:results[i].notes,
                object:JSON.stringify(results[i]),
                views:results[i].review_count,
                distance:results[i].distance,
                address:results[i].location.display_address[0] +" " + results[i].location.display_address[1]+" " + results[i].location.display_address[2]
            });
        }
    }
    var source = $('#browse').html();
    var template = Handlebars.compile(source);
    var string = template(data);
    spinner.stop();

    if(results.length > 0){
        $thumbnailswrapper.append(string);
    }
    else{
        $("#columnCenter").text("Sorry, there are no favorites to display.");
    }

    if (data.length >= 20) {
        if (opt_int >= 20) {
            $see_prev = $('<button class="btn" value="Previous" name="nextButton">Previous 20</button>');
            $see_prev.click(function() {
                addQuery(results, opt_int-1);
            });
            $buttonRow.append($see_prev);
        }
        if (opt_int + 20 <= data.length) {
            $see_all = $('<button class="btn" value="Next" name="prevButton">Next 20</button>');
            $see_all.click(function() {
                addQuery(results, opt_int + 20);
            });
            $buttonRow.append($see_all);
        }
    }
    var source1 = $('#filts').html();
    var template1 = Handlebars.compile(source1);
    var string1 = template1({funcname:'filterFavorites', results:JSON.stringify(results)});
    alert('Printing String: ' + string1);
    $('.columnRight').empty();
    $('.columnRight').append(string1);
    $thumbnailswrapper.append($buttonRow);
    $('.columnCenter').append($thumbnailswrapper);
    $('#pop').popover();
};

/*
* Function to send the bookmarks to the backend
*/
var saveFavorite = function (jsonString, tags1, notes1) {
    var object1 = jsonString;
    saveBookmark(object1, tags1, notes1);
};

var parseYelpCategories = function (term, location, categories) {

    var categories_wrapper = '
';
    for (var i in categories)
    {
        var genCategory = i;
        var genCategoryTag = i;
        genCategoryTag = genCategoryTag.replace(/ /g, "_");
        genCategoryTag = genCategoryTag.replace(/&/g, "And");
        categories_wrapper = categories_wrapper + '
';
        categories_wrapper = categories_wrapper + '
';
        categories_wrapper = categories_wrapper + '';
        categories_wrapper = categories_wrapper + genCategory;
        categories_wrapper = categories_wrapper + '';
        categories_wrapper = categories_wrapper + '
';
        categories_wrapper = categories_wrapper + '
';
        categories_wrapper = categories_wrapper + '
';

        for (var j in categories[i])
        {
            var subCategory = j;
            var subCategoryTag = categories[i][j];
            if (typeof categories[i][j] != "string")
            {
                subCategoryTag = subCategory;
                subCategoryTag = subCategoryTag.replace(/ /g, "_");
                subCategoryTag = subCategoryTag.replace(/&/g, "And");
            }

            categories_wrapper = categories_wrapper + '
';
            categories_wrapper = categories_wrapper + '
';
            categories_wrapper = categories_wrapper + '';

            categories_wrapper = categories_wrapper + subCategory;
            categories_wrapper = categories_wrapper + '';
            categories_wrapper = categories_wrapper + '
';

            if (typeof categories[i][j] != "string")
            {
                categories_wrapper = categories_wrapper + '
';
                categories_wrapper = categories_wrapper + '
';

                for (var k in categories[i][j])
                {
                    var subSubCategory = k;
                    var subSubCategoryTag = categories[i][j][k];

                    categories_wrapper = categories_wrapper + '
';
                    categories_wrapper = categories_wrapper + '
';
                    categories_wrapper = categories_wrapper + '';
                    categories_wrapper = categories_wrapper + subSubCategory;
                    categories_wrapper = categories_wrapper + '';
                    categories_wrapper = categories_wrapper + '
';
                    categories_wrapper = categories_wrapper + '
';
                }

                categories_wrapper = categories_wrapper + '
';
                categories_wrapper = categories_wrapper + '
';
            }
            categories_wrapper = categories_wrapper + '
';
        }

        categories_wrapper = categories_wrapper + '
';
        categories_wrapper = categories_wrapper + '
';
        categories_wrapper = categories_wrapper + '
';
    }
    categories_wrapper = categories_wrapper + "
";
    console.log(categories_wrapper);
    $('.columnLeft').append(categories_wrapper);
};
<|endoftext|>


<commit_before>import render from "./render";

function propertyDepsResolver(expr, {indent}) {
  // e.g. foo.bar
  const exprWithCheck = expr.replace(/([a-zA-Z_$][0-9a-zA-Z_$]*)\s*\.\s*([a-zA-Z_$][0-9a-zA-Z_$]*)\s*(?=[^(])/, (match, obj, prop) => {
    return `__checkDep(${obj}, "${prop}")`;
  });

  return render(indent)`
    () => {
      const __deps = [];
      const __checkDep = (obj, name) => {
        console.log("Checking deps for", name);
        if (obj != null && obj.isKonomiComponent) {
          __deps.push([obj, name]);
        }
        return obj[name];
      };
      ${exprWithCheck}();
      return __deps;
    }
  `;
}

function emitProperty(tree, {indent}) {
  const expr = () => {
    switch (tree.expr.type) {
      case "jsExpr":
        return `() => { return ${tree.expr.content} }`;
      case "jsBlock":
        return render(indent + 1)`
          () => {
            ${tree.expr.content}
          }
        `;
    }
  }();

  // TODO: resolve dependencies
  return render(indent)`
    this.bindProperty(
      "${tree.name}",
      ${propertyDepsResolver(expr, {indent: indent + 1})}(),
      ${expr}
    );
  `;
}

function emitEventListener(tree, {indent}) {
  const funcNames = {
    "on": "on",
    "prepend": "prependListener",
  };

  return render(indent)`
    this.${funcNames[tree.type]}(${tree.event.content}, () => {
      ${tree.block.content}
    });
  `;
}

function getId(members, ids) {
  const idTree = members.find(t => t.type === "id");
  if (idTree) {
    if (ids.indexOf(idTree.name) >= 0) {
      throw new Error(`ID "${idTree.name}" already used`);
    }
  }
  const id = idTree ? idTree.name : `__component_${ids.length}`;
  ids.push(id);
  return id;
}

function emitComponent(tree, {ids, className, indent}) {
  const id = getId(tree.members, ids);
  className = className || `Class_${id}`;

  const addProperties =
    tree.members.filter(t => t.type === "property")
      .map(t => render(indent)`
        ${className}.addProperty("${t.name}");
      `)
      .join("");

  return render(indent)`
    class ${className} extends ${tree.name} {
      constructor() {
        super();
        ${emitMembers(tree.members, {ids, indent: indent + 2})}
      }
    }
    ${addProperties}
    ${id} = new ${className}();
  `
}

function emitMembers(members, {ids, indent}) {
  const componentTrees = members.filter(t => t.type === "component");
  const propertyTrees = members.filter(t => t.type === "property");
  const eventListenerTrees = members.filter(t => t.type === "on" || t.type === "prepend");

  const components = componentTrees.map(t => emitComponent(t, {ids, indent})).join("");
  const properties = propertyTrees.map(t => emitProperty(t, {indent: indent + 1})).join("");
  const eventListeners = eventListenerTrees.map(t => emitEventListener(t, {indent})).join("");

  return render(indent)`
    ${components}
    ${eventListeners}
    this.on("link", () => {
      ${properties}
    });
  `;
}

function emitComponentDefinition(tree, {indent}) {
  const ids = [];

  const component = emitComponent(tree.component, {ids, className: tree.name, indent: indent + 1});
  const scope = ids.map(id => render(indent)`
    let ${id};\n
  `).join("");

  return render(indent)`
    const ${tree.name} = () => {
      ${scope}
      ${component}
      return ${tree.name};
    }();
  `;
}

function emitRoot(tree) {
  switch (tree.type) {
    case "jsBlock": {
      return tree.content;
    }
    case "componentDefinition": {
      return emitComponentDefinition(tree, {indent: 0});
    }
  }
}

export default
function emit(trees) {
  return trees.map(emitRoot).join("\n");
}
<commit_msg>Improve indent<commit_after>import render from "./render";

function emitPropertyDeps(expr, {indent}) {
  // e.g. foo.bar
  const exprWithCheck = expr.replace(/([a-zA-Z_$][0-9a-zA-Z_$]*)\s*\.\s*([a-zA-Z_$][0-9a-zA-Z_$]*)\s*(?=[^(])/, (match, obj, prop) => {
    return `__checkDep(${obj}, "${prop}")`;
  });

  return render(indent)`
    () => {
      const __deps = [];
      const __checkDep = (obj, name) => {
        console.log("Checking deps for", name);
        if (obj != null && obj.isKonomiComponent) {
          __deps.push([obj, name]);
        }
        return obj[name];
      };
      ${exprWithCheck}();
      return __deps;
    }()
  `;
}

function emitProperty(tree, {indent}) {
  const expr = () => {
    switch (tree.expr.type) {
      case "jsExpr":
        return `() => { return ${tree.expr.content} }`;
      case "jsBlock":
        return render(indent + 1)`
          () => {
            ${tree.expr.content}
          }
        `;
    }
  }();

  // TODO: resolve dependencies
  return render(indent)`
    this.bindProperty(
      "${tree.name}",
      ${emitPropertyDeps(expr, {indent: indent + 1})},
      ${expr}
    );
  `;
}

function emitEventListener(tree, {indent}) {
  const funcNames = {
    "on": "on",
    "prepend": "prependListener",
  };

  return render(indent)`
    this.${funcNames[tree.type]}(${tree.event.content}, () => {
      ${tree.block.content}
    });
  `;
}

function getId(members, ids) {
  const idTree = members.find(t => t.type === "id");
  if (idTree) {
    if (ids.indexOf(idTree.name) >= 0) {
      throw new Error(`ID "${idTree.name}" already used`);
    }
  }
  const id = idTree ? idTree.name : `__component_${ids.length}`;
  ids.push(id);
  return id;
}

function emitComponent(tree, {ids, className, indent}) {
  const id = getId(tree.members, ids);
  className = className || `Class_${id}`;

  const addProperties =
    tree.members.filter(t => t.type === "property")
      .map(t => render(indent)`
        ${className}.addProperty("${t.name}");
      `)
      .join("");

  return render(indent)`
    class ${className} extends ${tree.name} {
      constructor() {
        super();
        ${emitMembers(tree.members, {ids, indent: indent + 2})}
      }
    }
    ${addProperties}
    ${id} = new ${className}();
  `
}

function emitMembers(members, {ids, indent}) {
  const componentTrees = members.filter(t => t.type === "component");
  const propertyTrees = members.filter(t => t.type === "property");
  const eventListenerTrees = members.filter(t => t.type === "on" || t.type === "prepend");

  const components = componentTrees.map(t => emitComponent(t, {ids, indent})).join("");
  const properties = propertyTrees.map(t => emitProperty(t, {indent: indent + 1})).join("");
  const eventListeners = eventListenerTrees.map(t => emitEventListener(t, {indent})).join("");

  return render(indent)`
    ${components}
    ${eventListeners}
    this.on("link", () => {
      ${properties}
    });
  `;
}

function emitComponentDefinition(tree, {indent}) {
  const ids = [];

  const component = emitComponent(tree.component, {ids, className: tree.name, indent: indent + 1});
  const scope = ids.map(id => render(indent)`
    let ${id};\n
  `).join("");

  return render(indent)`
    const ${tree.name} = () => {
      ${scope}
      ${component}
      return ${tree.name};
    }();
  `;
}

function emitRoot(tree) {
  switch (tree.type) {
    case "jsBlock": {
      return tree.content;
    }
    case "componentDefinition": {
      return emitComponentDefinition(tree, {indent: 0});
    }
  }
}

export default
function emit(trees) {
  return trees.map(emitRoot).join("\n");
}
<|endoftext|>


<commit_before>/*
zoomwall.js

The MIT License (MIT)

Copyright (c) 2014 Eric Leong

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

var zoomwall = {

	create: function(blocks) {
		zoomwall.resize(blocks.children);

		blocks.classList.remove('loading');
		// shrink blocks if an empty space is clicked
		blocks.addEventListener('click', function() {
			if (this.children && this.children.length > 0) {
				zoomwall.shrink(this.children[0]);
			}
		});

		// add click listeners to blocks
		for (var i = 0; i < blocks.children.length; i++) {
			blocks.children[i].addEventListener('click', zoomwall.animate);
		}
	},

	resizeRow: function(row, width) {
		if (row && row.length > 1) {
			for (var i in row) {
				row[i].style.width = (parseInt(window.getComputedStyle(row[i]).width, 10) / width * 100) + '%';
				row[i].style.height = 'auto';
			}
		}
	},

	calcRowWidth: function(row) {
		var width = 0;

		for (var i in row) {
			width += parseInt(window.getComputedStyle(row[i]).width, 10);
		}

		return width;
	},

	resize: function(blocks) {
		var row = [];
		var top = -1;

		for (var c = 0; c < blocks.length; c++) {
			var block = blocks[c];

			if (block) {
				if (top == -1) {
					top = block.offsetTop;

				} else if (block.offsetTop != top) {
					zoomwall.resizeRow(row, zoomwall.calcRowWidth(row));

					row = [];
					top = block.offsetTop;
				}

				row.push(block);
			}
		}

		zoomwall.resizeRow(row, zoomwall.calcRowWidth(row));
	},

	reset: function(block) {
		block.style.transform = 'translate(0, 0) scale(1)';
		block.style.webkitTransform = 'translate(0, 0) scale(1)';
		block.classList.remove('active');
	},

	shrink: function(block) {
		block.parentNode.classList.remove('lightbox');

		// reset all blocks
		zoomwall.reset(block);

		var prev = block.previousElementSibling;
		while (prev) {
			zoomwall.reset(prev);
			prev = prev.previousElementSibling;
		}

		var next = block.nextElementSibling;
		while (next) {
			zoomwall.reset(next);
			next = next.nextElementSibling;
		}

		// swap images
		if (block.dataset.lowres) {
			block.src = block.dataset.lowres;
		}
	},

	expand: function(block) {

		block.classList.add('active');
		block.parentNode.classList.add('lightbox');

		// parent dimensions
		var parentStyle = window.getComputedStyle(block.parentNode);

		var parentWidth = parseInt(parentStyle.width, 10);
		var parentHeight = parseInt(parentStyle.height, 10);

		var parentTop = block.parentNode.getBoundingClientRect().top;

		// block dimensions
		var blockStyle = window.getComputedStyle(block);

		var blockWidth = parseInt(blockStyle.width, 10);
		var blockHeight = parseInt(blockStyle.height, 10);

		// determine maximum height
		var targetHeight = window.innerHeight;

		if (parentHeight < window.innerHeight) {
			targetHeight = parentHeight;
		} else if (parentTop > 0) {
			targetHeight -= parentTop;
		}

		// swap images
		if (block.dataset.highres) {
			block.dataset.lowres = block.src;
			block.src = block.dataset.highres;
		}

		// determine what blocks are on this row
		var row = [];
		row.push(block);

		var next = block.nextElementSibling;

		while (next && next.offsetTop == block.offsetTop) {
			row.push(next);

			next = next.nextElementSibling;
		}

		var prev = block.previousElementSibling;

		while (prev && prev.offsetTop == block.offsetTop) {
			row.unshift(prev);

			prev = prev.previousElementSibling;
		}

		// calculate scale
		var scale = targetHeight / blockHeight;

		if (blockWidth * scale > parentWidth) {
			scale = parentWidth / blockWidth;
		}

		// determine offset
		var offsetY = block.getBoundingClientRect().top;

		if (parentWidth < window.innerHeight) {
			offsetY -= targetHeight / 2 - blockHeight * scale / 2;
		}

		if (parentTop > 0) {
			offsetY -= parentTop;
		}

		var leftOffsetX = 0;  // shift in current row

		for (var i = 0; i < row.length && row[i] != block; i++) {
			leftOffsetX += parseInt(window.getComputedStyle(row[i]).width, 10) * scale;
		}

		leftOffsetX = parentWidth / 2 - blockWidth * scale / 2 - leftOffsetX;

		var rightOffsetX = 0;  // shift in current row

		for (var i = row.length - 1; i >= 0 && row[i] != block; i--) {
			rightOffsetX += parseInt(window.getComputedStyle(row[i]).width, 10) * scale;
		}

		rightOffsetX = parentWidth / 2 - blockWidth * scale / 2 - rightOffsetX;

		// determine Y offset
		var itemOffset = 0; // offset due to scaling of previous items
		var prevWidth = 0;

		// transform current row
		for (var i = 0; i < row.length; i++) {
			itemOffset += (prevWidth * scale - prevWidth);
			prevWidth = parseInt(window.getComputedStyle(row[i]).width, 10);

			var percentageOffsetX = (itemOffset + leftOffsetX) / prevWidth * 100;
			var percentageOffsetY = -offsetY / parseInt(window.getComputedStyle(row[i]).height, 10) * 100;

			row[i].style.transformOrigin = '0% 0%';
			row[i].style.webkitTransformOrigin = '0% 0%';
			row[i].style.transform = 'translate(' + percentageOffsetX.toFixed(8) + '%, ' + percentageOffsetY.toFixed(8) + '%) scale(' + scale.toFixed(8) + ')';
			row[i].style.webkitTransform = 'translate(' + percentageOffsetX.toFixed(8) + '%, ' + percentageOffsetY.toFixed(8) + '%) scale(' + scale.toFixed(8) + ')';
		}

		// transform items after
		var nextOffsetY = blockHeight * (scale - 1) - offsetY;
		var prevHeight;
		itemOffset = 0; // offset due to scaling of previous items
		prevWidth = 0;

		var next = row[row.length - 1].nextElementSibling;
		var nextRowTop = -1;

		while (next) {
			var curTop = next.offsetTop;

			if (curTop == nextRowTop) {
				itemOffset += prevWidth * scale - prevWidth;
			} else {

				if (nextRowTop != -1) {
					itemOffset = 0;
					nextOffsetY += prevHeight * (scale - 1);
				}

				nextRowTop = curTop;
			}

			prevWidth = parseInt(window.getComputedStyle(next).width, 10);
			prevHeight = parseInt(window.getComputedStyle(next).height, 10);

			var percentageOffsetX = (itemOffset + leftOffsetX) / prevWidth * 100;
			var percentageOffsetY = nextOffsetY / prevHeight * 100;

			next.style.transformOrigin = '0% 0%';
			next.style.webkitTransformOrigin = '0% 0%';
			next.style.transform = 'translate(' + percentageOffsetX.toFixed(8) + '%, ' + percentageOffsetY.toFixed(8) + '%) scale(' + scale.toFixed(8) + ')';
			next.style.webkitTransform = 'translate(' + percentageOffsetX.toFixed(8) + '%, ' + percentageOffsetY.toFixed(8) + '%) scale(' + scale.toFixed(8) + ')';

			next = next.nextElementSibling;
		}

		// transform items before
		var prevOffsetY = -offsetY;
		itemOffset = 0; // offset due to scaling of previous items
		prevWidth = 0;

		var prev = row[0].previousElementSibling;
		var prevRowTop = -1;

		while (prev) {
			var curTop = prev.offsetTop;

			if (curTop == prevRowTop) {
				itemOffset -= prevWidth * scale - prevWidth;
			} else {
				itemOffset = 0;
				prevOffsetY -= parseInt(window.getComputedStyle(prev).height, 10) * (scale - 1);
				prevRowTop = curTop;
			}

			prevWidth = parseInt(window.getComputedStyle(prev).width, 10);

			var percentageOffsetX = (itemOffset - rightOffsetX) / prevWidth * 100;
			var percentageOffsetY = prevOffsetY / parseInt(window.getComputedStyle(prev).height, 10) * 100;

			prev.style.transformOrigin = '100% 0%';
			prev.style.webkitTransformOrigin = '100% 0%';
			prev.style.transform = 'translate(' + percentageOffsetX.toFixed(8) + '%, ' + percentageOffsetY.toFixed(8) + '%) scale(' + scale.toFixed(8) + ')';
			prev.style.webkitTransform = 'translate(' + percentageOffsetX.toFixed(8) + '%, ' + percentageOffsetY.toFixed(8) + '%) scale(' + scale.toFixed(8) + ')';

			prev = prev.previousElementSibling;
		}
	},

	animate: function(e) {
		if (this.parentNode.classList.contains('lightbox')) {
			zoomwall.shrink(this);
		} else {
			zoomwall.expand(this);
		}

		e.stopPropagation();
	}
};<commit_msg>Fix typo.<commit_after>/*
zoomwall.js

The MIT License (MIT)

Copyright (c) 2014 Eric Leong

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

var zoomwall = {

	create: function(blocks) {
		zoomwall.resize(blocks.children);

		blocks.classList.remove('loading');
		// shrink blocks if an empty space is clicked
		blocks.addEventListener('click', function() {
			if (this.children && this.children.length > 0) {
				zoomwall.shrink(this.children[0]);
			}
		});

		// add click listeners to blocks
		for (var i = 0; i < blocks.children.length; i++) {
			blocks.children[i].addEventListener('click', zoomwall.animate);
		}
	},

	resizeRow: function(row, width) {
		if (row && row.length > 1) {
			for (var i in row) {
				row[i].style.width = (parseInt(window.getComputedStyle(row[i]).width, 10) / width * 100) + '%';
				row[i].style.height = 'auto';
			}
		}
	},

	calcRowWidth: function(row) {
		var width = 0;

		for (var i in row) {
			width += parseInt(window.getComputedStyle(row[i]).width, 10);
		}

		return width;
	},

	resize: function(blocks) {
		var row = [];
		var top = -1;

		for (var c = 0; c < blocks.length; c++) {
			var block = blocks[c];

			if (block) {
				if (top == -1) {
					top = block.offsetTop;

				} else if (block.offsetTop != top) {
					zoomwall.resizeRow(row, zoomwall.calcRowWidth(row));

					row = [];
					top = block.offsetTop;
				}

				row.push(block);
			}
		}

		zoomwall.resizeRow(row, zoomwall.calcRowWidth(row));
	},

	reset: function(block) {
		block.style.transform = 'translate(0, 0) scale(1)';
		block.style.webkitTransform = 'translate(0, 0) scale(1)';
		block.classList.remove('active');
	},

	shrink: function(block) {
		block.parentNode.classList.remove('lightbox');

		// reset all blocks
		zoomwall.reset(block);

		var prev = block.previousElementSibling;
		while (prev) {
			zoomwall.reset(prev);
			prev = prev.previousElementSibling;
		}

		var next = block.nextElementSibling;
		while (next) {
			zoomwall.reset(next);
			next = next.nextElementSibling;
		}

		// swap images
		if (block.dataset.lowres) {
			block.src = block.dataset.lowres;
		}
	},

	expand: function(block) {

		block.classList.add('active');
		block.parentNode.classList.add('lightbox');

		// parent dimensions
		var parentStyle = window.getComputedStyle(block.parentNode);

		var parentWidth = parseInt(parentStyle.width, 10);
		var parentHeight = parseInt(parentStyle.height, 10);

		var parentTop = block.parentNode.getBoundingClientRect().top;

		// block dimensions
		var blockStyle = window.getComputedStyle(block);

		var blockWidth = parseInt(blockStyle.width, 10);
		var blockHeight = parseInt(blockStyle.height, 10);

		// determine maximum height
		var targetHeight = window.innerHeight;

		if (parentHeight < window.innerHeight) {
			targetHeight = parentHeight;
		} else if (parentTop > 0) {
			targetHeight -= parentTop;
		}

		// swap images
		if (block.dataset.highres) {
			block.dataset.lowres = block.src;
			block.src = block.dataset.highres;
		}

		// determine what blocks are on this row
		var row = [];
		row.push(block);

		var next = block.nextElementSibling;

		while (next && next.offsetTop == block.offsetTop) {
			row.push(next);

			next = next.nextElementSibling;
		}

		var prev = block.previousElementSibling;

		while (prev && prev.offsetTop == block.offsetTop) {
			row.unshift(prev);

			prev = prev.previousElementSibling;
		}

		// calculate scale
		var scale = targetHeight / blockHeight;

		if (blockWidth * scale > parentWidth) {
			scale = parentWidth / blockWidth;
		}

		// determine offset
		var offsetY = block.getBoundingClientRect().top;

		if (parentHeight < window.innerHeight) {
			offsetY -= targetHeight / 2 - blockHeight * scale / 2;
		}

		if (parentTop > 0) {
			offsetY -= parentTop;
		}

		var leftOffsetX = 0;  // shift in current row

		for (var i = 0; i < row.length && row[i] != block; i++) {
			leftOffsetX += parseInt(window.getComputedStyle(row[i]).width, 10) * scale;
		}

		leftOffsetX = parentWidth / 2 - blockWidth * scale / 2 - leftOffsetX;

		var rightOffsetX = 0;  // shift in current row

		for (var i = row.length - 1; i >= 0 && row[i] != block; i--) {
			rightOffsetX += parseInt(window.getComputedStyle(row[i]).width, 10) * scale;
		}

		rightOffsetX = parentWidth / 2 - blockWidth * scale / 2 - rightOffsetX;

		// determine Y offset
		var itemOffset = 0; // offset due to scaling of previous items
		var prevWidth = 0;

		// transform current row
		for (var i = 0; i < row.length; i++) {
			itemOffset += (prevWidth * scale - prevWidth);
			prevWidth = parseInt(window.getComputedStyle(row[i]).width, 10);

			var percentageOffsetX = (itemOffset + leftOffsetX) / prevWidth * 100;
			var percentageOffsetY = -offsetY / parseInt(window.getComputedStyle(row[i]).height, 10) * 100;

			row[i].style.transformOrigin = '0% 0%';
			row[i].style.webkitTransformOrigin = '0% 0%';
			row[i].style.transform = 'translate(' + percentageOffsetX.toFixed(8) + '%, ' + percentageOffsetY.toFixed(8) + '%) scale(' + scale.toFixed(8) + ')';
			row[i].style.webkitTransform = 'translate(' + percentageOffsetX.toFixed(8) + '%, ' + percentageOffsetY.toFixed(8) + '%) scale(' + scale.toFixed(8) + ')';
		}

		// transform items after
		var nextOffsetY = blockHeight * (scale - 1) - offsetY;
		var prevHeight;
		itemOffset = 0; // offset due to scaling of previous items
		prevWidth = 0;

		var next = row[row.length - 1].nextElementSibling;
		var nextRowTop = -1;

		while (next) {
			var curTop = next.offsetTop;

			if (curTop == nextRowTop) {
				itemOffset += prevWidth * scale - prevWidth;
			} else {

				if (nextRowTop != -1) {
					itemOffset = 0;
					nextOffsetY += prevHeight * (scale - 1);
				}

				nextRowTop = curTop;
			}

			prevWidth = parseInt(window.getComputedStyle(next).width, 10);
			prevHeight = parseInt(window.getComputedStyle(next).height, 10);

			var percentageOffsetX = (itemOffset + leftOffsetX) / prevWidth * 100;
			var percentageOffsetY = nextOffsetY / prevHeight * 100;

			next.style.transformOrigin = '0% 0%';
			next.style.webkitTransformOrigin = '0% 0%';
			next.style.transform = 'translate(' + percentageOffsetX.toFixed(8) + '%, ' + percentageOffsetY.toFixed(8) + '%) scale(' + scale.toFixed(8) + ')';
			next.style.webkitTransform = 'translate(' + percentageOffsetX.toFixed(8) + '%, ' + percentageOffsetY.toFixed(8) + '%) scale(' + scale.toFixed(8) + ')';

			next = next.nextElementSibling;
		}

		// transform items before
		var prevOffsetY = -offsetY;
		itemOffset = 0; // offset due to scaling of previous items
		prevWidth = 0;

		var prev = row[0].previousElementSibling;
		var prevRowTop = -1;

		while (prev) {
			var curTop = prev.offsetTop;

			if (curTop == prevRowTop) {
				itemOffset -= prevWidth * scale - prevWidth;
			} else {
				itemOffset = 0;
				prevOffsetY -= parseInt(window.getComputedStyle(prev).height, 10) * (scale - 1);
				prevRowTop = curTop;
			}

			prevWidth = parseInt(window.getComputedStyle(prev).width, 10);

			var percentageOffsetX = (itemOffset - rightOffsetX) / prevWidth * 100;
			var percentageOffsetY = prevOffsetY / parseInt(window.getComputedStyle(prev).height, 10) * 100;

			prev.style.transformOrigin = '100% 0%';
			prev.style.webkitTransformOrigin = '100% 0%';
			prev.style.transform = 'translate(' + percentageOffsetX.toFixed(8) + '%, ' + percentageOffsetY.toFixed(8) + '%) scale(' + scale.toFixed(8) + ')';
			prev.style.webkitTransform = 'translate(' + percentageOffsetX.toFixed(8) + '%, ' + percentageOffsetY.toFixed(8) + '%) scale(' + scale.toFixed(8) + ')';

			prev = prev.previousElementSibling;
		}
	},

	animate: function(e) {
		if (this.parentNode.classList.contains('lightbox')) {
			zoomwall.shrink(this);
		} else {
			zoomwall.expand(this);
		}

		e.stopPropagation();
	}
};<|endoftext|>


<commit_before>'use strict';

import Cache from './cache';
import { copyAll } from './utils';

/**
 * There are two ways to use Models. One is to use them as stateless references, that is to say, whenever you need
 * a model just instantiate a new one.
 *
 * The other method is to pretend as though they are stateful, and create them once and reuse them as objects. You can
 * use the `invalidate` method if you want your state to resync with the remote. This is useful if you have some sort
 * of operation where the user can edit model fields, and you want to submit the changes later.
 *
 * Is there any use case for automatic invalidation?
 */

export default class Model {
    /**
     * For subclasses, call super() with a function argument, which will provide the this argument.
     * This is due primarily to the no-this-before-super rule for derived classes in ES6.
     */
    constructor (fn) {
        this.$local = false;
        if (fn) { fn(this) }
        this.$loaded = false;
        Cache.get(this);
    }

    setLocal (v) {
        this.$local = v;
    }

    /**
     * This method communicates with the remote server and performs the API calls necessary to get the data.
     * It should call onLoad on each model. The mapFromArray function from utils will be very useful.
     */
    static loadFromRemote (...models) {
        return false;
    }

    isLoaded () {
        return this.$loaded;
    }

    // TODO: perpetual is actually useless since it includes every cache hit...better to do remotes only?
    afterLoad (perpetual=false) {
        return new Promise((resolve, reject) => {
            if (this.$loaded || this.$local) {
                resolve(this);
            }

            if (!(this.$loaded || this.$local) || perpetual) {
                Cache.waitFor(this, () => {
                    resolve(this);
                    return perpetual;
                });
                Cache.get(this);
            }
            // TODO: what is timeout?
        });
    }

    /**
     * Method called after data is received from the remote server.
     * Subclasses should, like the constructor, call super() last.
     */
    fromRemote (data) {
        this.$loaded = true;
        Cache.set(this, data);
    }

    fromCache (data) {
        copyAll(this, data);
    }

    invalidate () {
        this.$loaded = false;
        Cache.invalidate(this);
        Cache.get(this);
    }

    /**
     * Get the UUID for this model. For most applications this should equal/correspond to the schema + primary key of
     * the database object. Returning `false` implies that this model instance is not uniquely identifiable and
     * therefore uncacheable!
     */
    get uuid () {
        return false;
    }
}
<commit_msg>Remove perpetual flag from Model.afterLoad() which was useless anyways.<commit_after>'use strict';

import Cache from './cache';
import { copyAll } from './utils';

/**
 * There are two ways to use Models. One is to use them as stateless references, that is to say, whenever you need
 * a model just instantiate a new one.
 *
 * The other method is to pretend as though they are stateful, and create them once and reuse them as objects. You can
 * use the `invalidate` method if you want your state to resync with the remote. This is useful if you have some sort
 * of operation where the user can edit model fields, and you want to submit the changes later.
 *
 * Is there any use case for automatic invalidation?
 */

export default class Model {
    /**
     * For subclasses, call super() with a function argument, which will provide the this argument.
     * This is due primarily to the no-this-before-super rule for derived classes in ES6.
     */
    constructor (fn) {
        this.$local = false;
        if (fn) { fn(this) }
        this.$loaded = false;
        Cache.get(this);
    }

    setLocal (v) {
        this.$local = v;
    }

    /**
     * This method communicates with the remote server and performs the API calls necessary to get the data.
     * It should call onLoad on each model. The mapFromArray function from utils will be very useful.
     */
    static loadFromRemote (...models) {
        return false;
    }

    isLoaded () {
        return this.$loaded;
    }

    afterLoad () {
        return new Promise((resolve, reject) => {
            if (this.$loaded || this.$local) {
                resolve(this);
            } else {
                Cache.waitFor(this, () => { resolve(this); });
                Cache.get(this);
            }
            // TODO: what is timeout?
        });
    }

    /**
     * Method called after data is received from the remote server.
     * Subclasses should, like the constructor, call super() last.
     */
    fromRemote (data) {
        this.$loaded = true;
        Cache.set(this, data);
    }

    fromCache (data) {
        copyAll(this, data);
    }

    invalidate () {
        this.$loaded = false;
        Cache.invalidate(this);
        Cache.get(this);
    }

    /**
     * Get the UUID for this model. For most applications this should equal/correspond to the schema + primary key of
     * the database object. Returning `false` implies that this model instance is not uniquely identifiable and
     * therefore uncacheable!
     */
    get uuid () {
        return false;
    }
}
<|endoftext|>